├── LICENSE.md ├── stubs ├── factory.stub ├── request.stub ├── seed.stub ├── service.stub ├── migrations.stub ├── AuthServiceProvider.stub ├── model.stub ├── dto.stub ├── resource.stub ├── controller.stub ├── provider.stub └── policy.stub ├── config ├── code-generator.php └── laravel-api-generator.php ├── src ├── EntitiesGenerator │ ├── DTOGenerator.php │ ├── Domain │ │ └── Domain.php │ ├── FactoryGenerator.php │ ├── ModelGenerator.php │ ├── PolicyGenerator.php │ ├── RequestGenerator.php │ ├── ResourceGenerator.php │ ├── SeederGenerator.php │ ├── ServiceGenerator.php │ ├── ControllerGenerator.php │ ├── MigrationGenerator.php │ ├── Presentation │ │ └── Presentation.php │ ├── Infrastructure │ │ └── Infrastucture.php │ ├── AbstractGenerator.php │ └── ModelGeneratorRefactored.php ├── Contracts │ ├── ApiGenerationServiceInterface.php │ └── GeneratorInterface.php ├── Exceptions │ └── CodeGeneratorException.php ├── Support │ ├── StubLoader.php │ ├── FieldParser.php │ └── JsonParser.php ├── ValueObjects │ ├── RelationshipDefinition.php │ ├── EntityDefinition.php │ └── FieldDefinition.php ├── Providers │ └── CodeGeneratorServiceProvider.php ├── Console │ └── Commands │ │ ├── MakeApiWithDiagram.php │ │ ├── MakeApiCommand.php │ │ ├── DeleteFullApi.php │ │ ├── InstallPackageCommand.php │ │ └── MakeApi.php └── Services │ └── ApiGenerationService.php ├── phpstan.neon ├── phpunit.xml ├── tests ├── TestCase.php ├── Feature │ └── MakeApiCommandTest.php └── Unit │ └── ValueObjects │ └── FieldDefinitionTest.php ├── deploy.bat ├── composer.json ├── .github └── workflows │ └── ci.yml ├── config.php ├── CONTRIBUTING.md ├── data.json ├── CHANGELOG.md ├── README_NEW.md └── README.md /LICENSE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/factory.stub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/request.stub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/seed.stub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/service.stub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/migrations.stub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/code-generator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/AuthServiceProvider.stub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/DTOGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/Domain/Domain.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/FactoryGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/ModelGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/PolicyGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/RequestGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/ResourceGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/SeederGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/ServiceGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/ControllerGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/MigrationGenerator.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/Presentation/Presentation.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/Infrastructure/Infrastucture.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stubs/model.stub: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function toArray(Request $request): array 17 | { 18 | return [ 19 | {{fields}} 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | - tests 6 | 7 | ignoreErrors: 8 | - '#Call to an undefined method Illuminate\\Contracts\\Foundation\\Application::#' 9 | - '#Access to an undefined property Illuminate\\Contracts\\Foundation\\Application::#' 10 | - '#Undefined function#' 11 | 12 | checkMissingIterableValueType: false 13 | checkGenericClassInNonGenericObjectType: false 14 | 15 | excludePaths: 16 | - vendor/* 17 | 18 | reportUnmatchedIgnoredErrors: false -------------------------------------------------------------------------------- /src/Contracts/ApiGenerationServiceInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Contracts/GeneratorInterface.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testing'); 27 | $app['config']->set('database.connections.testing', [ 28 | 'driver' => 'sqlite', 29 | 'database' => ':memory:', 30 | 'prefix' => '', 31 | ]); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Exceptions/CodeGeneratorException.php: -------------------------------------------------------------------------------- 1 | artisan('make:fullapi', [ 20 | 'name' => $name, 21 | '--fields' => $fields 22 | ])->assertSuccessful(); 23 | 24 | // Assert 25 | $this->assertFileExists(app_path("Models/{$name}.php")); 26 | $this->assertFileExists(app_path("Http/Controllers/{$name}Controller.php")); 27 | $this->assertFileExists(app_path("Services/{$name}Service.php")); 28 | $this->assertFileExists(app_path("DTO/{$name}DTO.php")); 29 | } 30 | 31 | /** @test */ 32 | public function it_requires_fields_option() 33 | { 34 | $this->artisan('make:fullapi', [ 35 | 'name' => 'Post' 36 | ])->assertFailed(); 37 | } 38 | 39 | /** @test */ 40 | public function it_creates_valid_model_with_fillable() 41 | { 42 | // Arrange 43 | $name = 'Post'; 44 | $fields = 'title:string,content:text'; 45 | 46 | // Act 47 | $this->artisan('make:fullapi', [ 48 | 'name' => $name, 49 | '--fields' => $fields 50 | ]); 51 | 52 | // Assert 53 | $modelPath = app_path("Models/{$name}.php"); 54 | $this->assertStringContainsString( 55 | "protected \$fillable = ['title', 'content'];", 56 | file_get_contents($modelPath) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Support/StubLoader.php: -------------------------------------------------------------------------------- 1 | getStubPath($stubName); 24 | 25 | if (!File::exists($stubPath)) { 26 | throw CodeGeneratorException::fileNotFound($stubPath); 27 | } 28 | 29 | $content = File::get($stubPath); 30 | 31 | return $this->replacePlaceholders($content, $replacements); 32 | } 33 | 34 | /** 35 | * Get the full path to a stub file. 36 | */ 37 | private function getStubPath(string $stubName): string 38 | { 39 | $stubName = str_replace(self::STUB_EXTENSION, '', $stubName); 40 | return $this->stubsPath . DIRECTORY_SEPARATOR . $stubName . self::STUB_EXTENSION; 41 | } 42 | 43 | /** 44 | * Replace placeholders in stub content. 45 | */ 46 | private function replacePlaceholders(string $content, array $replacements): string 47 | { 48 | foreach ($replacements as $placeholder => $value) { 49 | $content = str_replace("{{$placeholder}}", $value, $content); 50 | } 51 | 52 | return $content; 53 | } 54 | 55 | /** 56 | * Check if a stub exists. 57 | */ 58 | public function exists(string $stubName): bool 59 | { 60 | return File::exists($this->getStubPath($stubName)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Support/FieldParser.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function parseFieldsString(string $fields): array 18 | { 19 | if (empty(trim($fields))) { 20 | throw new InvalidArgumentException('Fields string cannot be empty'); 21 | } 22 | 23 | $fieldsArray = []; 24 | 25 | foreach (explode(',', $fields) as $field) { 26 | $parts = explode(':', trim($field)); 27 | 28 | if (count($parts) !== 2) { 29 | throw new InvalidArgumentException("Invalid field format: {$field}. Expected format: field:type"); 30 | } 31 | 32 | $fieldName = trim($parts[0]); 33 | $fieldType = trim(strtolower($parts[1])); 34 | 35 | if (empty($fieldName) || empty($fieldType)) { 36 | throw new InvalidArgumentException("Field name and type cannot be empty: {$field}"); 37 | } 38 | 39 | $fieldsArray[$fieldName] = $fieldType; 40 | } 41 | 42 | return $fieldsArray; 43 | } 44 | 45 | /** 46 | * Validate field type. 47 | */ 48 | public static function isValidType(string $type): bool 49 | { 50 | $allowedTypes = [ 51 | 'string', 'integer', 'int', 'boolean', 'bool', 'text', 'float', 52 | 'decimal', 'json', 'date', 'datetime', 'timestamp', 'time', 53 | 'uuid', 'UUID', 'bigint' 54 | ]; 55 | 56 | return in_array($type, $allowedTypes, true); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /deploy.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo === Laravel API Generator - Deployment Script === 3 | echo. 4 | 5 | REM Check if we're in the right directory 6 | if not exist "composer.json" ( 7 | echo Error: composer.json not found. Make sure you're in the package root directory. 8 | pause 9 | exit /b 1 10 | ) 11 | 12 | echo 1. Checking Git status... 13 | git status 14 | 15 | echo. 16 | echo 2. Adding all files... 17 | git add . 18 | 19 | echo. 20 | echo 3. Creating commit... 21 | git commit -m "feat: Major refactoring v3.0.0 - Clean architecture with Value Objects, Services, and improved generators 22 | 23 | - Implemented clean architecture with Value Objects (EntityDefinition, FieldDefinition, RelationshipDefinition) 24 | - Added professional Service Layer pattern with dependency injection 25 | - Created extensible generator system with AbstractGenerator 26 | - Improved JSON parsing with better error handling 27 | - Added comprehensive type safety with PHP 8.1+ features 28 | - Fixed model generation with proper relationships and inheritance 29 | - Enhanced stub system with better placeholder handling 30 | - Added professional error handling with custom exceptions 31 | - Improved documentation and contributing guidelines 32 | - Added GitHub Actions CI/CD pipeline 33 | - Updated PHPStan configuration to level 8 34 | - Added comprehensive testing structure" 35 | 36 | echo. 37 | echo 4. Creating version tag... 38 | git tag -a v3.0.0 -m "Release v3.0.0: Major refactoring with clean architecture" 39 | 40 | echo. 41 | echo 5. Pushing to repository... 42 | git push origin main 43 | git push origin --tags 44 | 45 | echo. 46 | echo 6. Deployment completed! 47 | echo. 48 | echo Next steps: 49 | echo - Check GitHub for the new tag: https://github.com/your-username/laravel-api-generator/tags 50 | echo - Check Packagist for the new version: https://packagist.org/packages/nameless/laravel-api-generator 51 | echo - The package should be available for installation with: composer require nameless/laravel-api-generator:^3.0 52 | echo. 53 | pause 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nameless/laravel-api-generator", 3 | "description": "A professional Laravel API generator that automatically creates complete API structures with clean architecture, type safety, and best practices", 4 | "version": "3.0.1", 5 | "type": "library", 6 | "license": "MIT", 7 | "keywords": [ 8 | "laravel", 9 | "api", 10 | "generator", 11 | "crud", 12 | "clean-architecture", 13 | "dto", 14 | "service-layer", 15 | "artisan", 16 | "code-generator" 17 | ], 18 | "authors": [ 19 | { 20 | "name": "Mbassi Loic Aron", 21 | "email": "loicmbassi5@email.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.1", 27 | "laravel/framework": "^10.0 || ^11.0", 28 | "dedoc/scramble": "^0.11.31" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^10.0", 32 | "orchestra/testbench": "^8.0 || ^9.0", 33 | "phpstan/phpstan": "^2.1", 34 | "phpstan/phpstan-phpunit": "^2.0", 35 | "laravel/pint": "^1.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "nameless\\CodeGenerator\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "nameless\\CodeGenerator\\Tests\\": "tests/" 45 | } 46 | }, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "nameless\\CodeGenerator\\Providers\\CodeGeneratorServiceProvider" 51 | ] 52 | } 53 | }, 54 | "config": { 55 | "optimize-autoloader": true, 56 | "preferred-install": "dist", 57 | "sort-packages": true 58 | }, 59 | "scripts": { 60 | "test": "vendor/bin/phpunit", 61 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 62 | "analyse": "vendor/bin/phpstan analyse", 63 | "format": "vendor/bin/pint" 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true 67 | } 68 | -------------------------------------------------------------------------------- /stubs/controller.stub: -------------------------------------------------------------------------------- 1 | service->getAll(); 27 | return {{modelName}}Resource::collection(${{pluralName}}); 28 | } 29 | 30 | /** 31 | * Store a newly created resource in storage. 32 | */ 33 | public function store({{modelName}}Request $request) 34 | { 35 | $dto = {{modelName}}DTO::fromRequest($request); 36 | ${{modelNameLower}} = $this->service->create($dto); 37 | return new {{modelName}}Resource(${{modelNameLower}}); 38 | } 39 | 40 | /** 41 | * Display the specified resource. 42 | */ 43 | public function show({{modelName}} ${{modelNameLower}}) 44 | { 45 | return new {{modelName}}Resource(${{modelNameLower}}); 46 | } 47 | 48 | /** 49 | * Update the specified resource in storage. 50 | */ 51 | public function update({{modelName}}Request $request, {{modelName}} ${{modelNameLower}}) 52 | { 53 | $dto = {{modelName}}DTO::fromRequest($request); 54 | $updated{{modelName}} = $this->service->update(${{modelNameLower}}, $dto); 55 | return new {{modelName}}Resource($updated{{modelName}}); 56 | } 57 | 58 | /** 59 | * Remove the specified resource from storage. 60 | */ 61 | public function destroy({{modelName}} ${{modelNameLower}}) 62 | { 63 | $this->service->delete(${{modelNameLower}}); 64 | return response(null, 204); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ValueObjects/RelationshipDefinition.php: -------------------------------------------------------------------------------- 1 | validateType($type); 21 | $this->validateRelatedModel($relatedModel); 22 | $this->validateRole($role); 23 | } 24 | 25 | private function validateType(string $type): void 26 | { 27 | $allowedTypes = ['oneToOne', 'oneToMany', 'manyToOne', 'manyToMany']; 28 | 29 | if (!in_array($type, $allowedTypes, true)) { 30 | throw new InvalidArgumentException("Invalid relationship type: {$type}"); 31 | } 32 | } 33 | 34 | private function validateRelatedModel(string $relatedModel): void 35 | { 36 | if (empty(trim($relatedModel))) { 37 | throw new InvalidArgumentException('Related model cannot be empty'); 38 | } 39 | } 40 | 41 | private function validateRole(string $role): void 42 | { 43 | if (empty(trim($role))) { 44 | throw new InvalidArgumentException('Role cannot be empty'); 45 | } 46 | } 47 | 48 | public function getEloquentMethod(): string 49 | { 50 | return match ($this->type) { 51 | 'oneToOne' => 'hasOne', 52 | 'oneToMany' => 'hasMany', 53 | 'manyToOne' => 'belongsTo', 54 | 'manyToMany' => 'belongsToMany', 55 | }; 56 | } 57 | 58 | public function requiresForeignKey(): bool 59 | { 60 | return in_array($this->type, ['manyToOne', 'oneToOne'], true); 61 | } 62 | 63 | public function getForeignKeyName(): string 64 | { 65 | if ($this->foreignKey !== null) { 66 | return $this->foreignKey; 67 | } 68 | 69 | return Str::snake($this->role) . '_id'; 70 | } 71 | 72 | public function getMethodName(): string 73 | { 74 | return Str::camel($this->role); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stubs/provider.stub: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 24 | $this->commands([ 25 | \nameless\CodeGenerator\Console\Commands\MakeApi::class, // Utiliser l'ancienne commande pour l'instant 26 | DeleteFullApi::class, 27 | MakeApiWithDiagram::class, 28 | InstallPackageCommand::class, 29 | ]); 30 | } 31 | } 32 | 33 | public function register(): void 34 | { 35 | $this->registerServices(); 36 | $this->registerGenerators(); 37 | 38 | // Register Scramble for API documentation 39 | $this->app->register(\Dedoc\Scramble\ScrambleServiceProvider::class); 40 | } 41 | 42 | private function registerServices(): void 43 | { 44 | // Register StubLoader 45 | $this->app->singleton(StubLoader::class, function () { 46 | return new StubLoader(__DIR__ . '/../../stubs'); 47 | }); 48 | 49 | // Register JsonParser 50 | $this->app->singleton(JsonParser::class); 51 | 52 | // Register main API generation service 53 | $this->app->singleton(ApiGenerationServiceInterface::class, function ($app) { 54 | return new ApiGenerationService( 55 | $app->make('code_generator.generators'), 56 | $app->make(JsonParser::class) 57 | ); 58 | }); 59 | } 60 | 61 | private function registerGenerators(): void 62 | { 63 | $this->app->singleton('code_generator.generators', function ($app) { 64 | return collect([ 65 | $app->make(ModelGeneratorRefactored::class), 66 | // Add other generators here as they are created 67 | ]); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/AbstractGenerator.php: -------------------------------------------------------------------------------- 1 | generateContent($definition); 26 | $outputPath = $this->getOutputPath($definition); 27 | 28 | $this->ensureDirectoryExists($outputPath); 29 | 30 | if (!File::put($outputPath, $content)) { 31 | throw CodeGeneratorException::fileCreationFailed($outputPath); 32 | } 33 | 34 | return true; 35 | } catch (\Exception $e) { 36 | throw CodeGeneratorException::generationFailed($this->getType(), $e->getMessage()); 37 | } 38 | } 39 | 40 | /** 41 | * Check if the generator supports the given entity definition. 42 | */ 43 | public function supports(EntityDefinition $definition): bool 44 | { 45 | return true; // Default implementation supports all entities 46 | } 47 | 48 | /** 49 | * Generate the content for the file. 50 | */ 51 | abstract protected function generateContent(EntityDefinition $definition): string; 52 | 53 | /** 54 | * Get the stub name for this generator. 55 | */ 56 | abstract protected function getStubName(): string; 57 | 58 | /** 59 | * Get replacements for the stub. 60 | */ 61 | abstract protected function getReplacements(EntityDefinition $definition): array; 62 | 63 | /** 64 | * Ensure the directory exists for the output path. 65 | */ 66 | protected function ensureDirectoryExists(string $filePath): void 67 | { 68 | $directory = dirname($filePath); 69 | 70 | if (!File::isDirectory($directory)) { 71 | File::makeDirectory($directory, 0755, true); 72 | } 73 | } 74 | 75 | /** 76 | * Load and process stub with replacements. 77 | */ 78 | protected function processStub(EntityDefinition $definition): string 79 | { 80 | $replacements = $this->getReplacements($definition); 81 | return $this->stubLoader->load($this->getStubName(), $replacements); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | php-version: [8.1, 8.2, 8.3] 16 | laravel-version: [10.*, 11.*] 17 | 18 | name: PHP ${{ matrix.php-version }} - Laravel ${{ matrix.laravel-version }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup PHP 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ matrix.php-version }} 27 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv 28 | coverage: xdebug 29 | 30 | - name: Cache Composer dependencies 31 | uses: actions/cache@v3 32 | with: 33 | path: ~/.composer/cache/files 34 | key: dependencies-laravel-${{ matrix.laravel-version }}-php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }} 35 | 36 | - name: Install dependencies 37 | run: | 38 | composer require "laravel/framework:${{ matrix.laravel-version }}" --no-interaction --no-update 39 | composer install --prefer-dist --no-interaction --no-suggest 40 | 41 | - name: Run tests 42 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 43 | 44 | - name: Upload coverage to Codecov 45 | uses: codecov/codecov-action@v3 46 | with: 47 | file: ./coverage.xml 48 | fail_ci_if_error: true 49 | 50 | static-analysis: 51 | runs-on: ubuntu-latest 52 | 53 | name: Static Analysis 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - name: Setup PHP 59 | uses: shivammathur/setup-php@v2 60 | with: 61 | php-version: 8.2 62 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv 63 | 64 | - name: Install dependencies 65 | run: composer install --prefer-dist --no-interaction --no-suggest 66 | 67 | - name: Run PHPStan 68 | run: vendor/bin/phpstan analyse --error-format=github 69 | 70 | code-style: 71 | runs-on: ubuntu-latest 72 | 73 | name: Code Style 74 | 75 | steps: 76 | - uses: actions/checkout@v4 77 | 78 | - name: Setup PHP 79 | uses: shivammathur/setup-php@v2 80 | with: 81 | php-version: 8.2 82 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv 83 | 84 | - name: Install dependencies 85 | run: composer install --prefer-dist --no-interaction --no-suggest 86 | 87 | - name: Check code style 88 | run: vendor/bin/pint --test 89 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'name' => 'Person', 7 | 'attributes' => 8 | array ( 9 | 0 => 10 | array ( 11 | 'name' => 'name', 12 | 'type' => 'string', 13 | ), 14 | 1 => 15 | array ( 16 | 'name' => 'phonenumber', 17 | 'type' => 'string', 18 | ), 19 | 2 => 20 | array ( 21 | 'name' => 'emailaddress', 22 | 'type' => 'string', 23 | ), 24 | 3 => 25 | array ( 26 | 'name' => 'address', 27 | 'type' => 'Address', 28 | ), 29 | ), 30 | ), 31 | 1 => 32 | array ( 33 | 'name' => 'Student', 34 | 'attributes' => 35 | array ( 36 | 0 => 37 | array ( 38 | 'name' => 'studentnumber', 39 | 'type' => 'integer', 40 | ), 41 | 1 => 42 | array ( 43 | 'name' => 'averagemark', 44 | 'type' => 'integer', 45 | ), 46 | ), 47 | ), 48 | 2 => 49 | array ( 50 | 'name' => 'Professor', 51 | 'attributes' => 52 | array ( 53 | 0 => 54 | array ( 55 | 'name' => 'name', 56 | 'type' => 'void', 57 | ), 58 | 1 => 59 | array ( 60 | 'name' => 'staffnumber', 61 | 'type' => 'integer', 62 | ), 63 | 2 => 64 | array ( 65 | 'name' => 'yearsofservice', 66 | 'type' => 'integer', 67 | ), 68 | 3 => 69 | array ( 70 | 'name' => 'numberofclasses', 71 | 'type' => 'integer', 72 | ), 73 | ), 74 | ), 75 | 3 => 76 | array ( 77 | 'name' => 'Address', 78 | 'attributes' => 79 | array ( 80 | 0 => 81 | array ( 82 | 'name' => 'street', 83 | 'type' => 'string', 84 | ), 85 | 1 => 86 | array ( 87 | 'name' => 'city', 88 | 'type' => 'string', 89 | ), 90 | 2 => 91 | array ( 92 | 'name' => 'state', 93 | 'type' => 'string', 94 | ), 95 | 3 => 96 | array ( 97 | 'name' => 'postalcode', 98 | 'type' => 'integer', 99 | ), 100 | 4 => 101 | array ( 102 | 'name' => 'country', 103 | 'type' => 'string', 104 | ), 105 | ), 106 | ), 107 | 4 => 108 | array ( 109 | 'name' => 'Adresses', 110 | 'attributes' => 111 | array ( 112 | 0 => 113 | array ( 114 | 'name' => 'city', 115 | 'type' => 'string', 116 | ), 117 | 1 => 118 | array ( 119 | 'name' => 'state', 120 | 'type' => 'string', 121 | ), 122 | 2 => 123 | array ( 124 | 'name' => 'postalcode', 125 | 'type' => 'integer', 126 | ), 127 | 3 => 128 | array ( 129 | 'name' => 'country', 130 | 'type' => 'string', 131 | ), 132 | ), 133 | ), 134 | ); 135 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeApiWithDiagram.php: -------------------------------------------------------------------------------- 1 | error("Le fichier class_data est introuvable."); 21 | return; 22 | } 23 | 24 | $this->info("Lecture du fichier JSON..."); 25 | $jsonData = file_get_contents($jsonFilePath); 26 | $this->classes = json_decode($jsonData, true); 27 | 28 | if (json_last_error() !== JSON_ERROR_NONE) { 29 | $this->error("Erreur de décodage JSON : " . json_last_error_msg()); 30 | return; 31 | } 32 | 33 | $this->info("Extraction des données JSON..."); 34 | $this->jsonExtractionToArray(); 35 | 36 | $this->info("Génération des API avec Artisan..."); 37 | $this->runFullApi(); 38 | } 39 | 40 | /** 41 | * Extraire et formater les données JSON dans un tableau compatible. 42 | */ 43 | public function jsonExtractionToArray() 44 | { 45 | $this->classes = array_map(function ($class) { 46 | return [ 47 | 'name' => ucfirst($class['name']), 48 | 'attributes' => array_map(function ($attribute) { 49 | return [ 50 | 'name' => $attribute['name'], 51 | 'type' => match (strtolower($attribute['type'])) { 52 | 'integer' => 'int', 53 | 'bigint' => 'int', 54 | 'str', 'text' => 'string', 55 | 'boolean' => 'bool', 56 | default => $attribute['type'], 57 | }, 58 | ]; 59 | }, $class['attributes']), 60 | ]; 61 | }, $this->classes); 62 | } 63 | 64 | /** 65 | * Parcourir les classes et exécuter les commandes Artisan pour générer les API. 66 | */ 67 | public function runFullApi() 68 | { 69 | foreach ($this->classes as $class) { 70 | $className = ucfirst($class['name']); 71 | $fields = []; 72 | 73 | foreach ($class['attributes'] as $attribute) { 74 | $fields[] = "{$attribute['name']}:{$attribute['type']}"; 75 | } 76 | 77 | $fieldsString = implode(',', $fields); 78 | echo "Ici les parametres : ".$fieldsString. "\n"; 79 | 80 | try { 81 | Artisan::call("make:fullapi {$className} --fields={$fieldsString}"); 82 | 83 | $this->info("API pour la classe $className générée avec succès !"); 84 | } catch (\Exception $e) { 85 | $this->error("Erreur lors de la génération de l'API pour la classe $className : " . $e->getMessage()); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Unit/ValueObjects/FieldDefinitionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('email', $field->name); 21 | $this->assertEquals('string', $field->type); 22 | $this->assertTrue($field->nullable); 23 | } 24 | 25 | public function test_throws_exception_for_empty_field_name(): void 26 | { 27 | $this->expectException(InvalidArgumentException::class); 28 | $this->expectExceptionMessage('Field name cannot be empty'); 29 | 30 | new FieldDefinition(name: '', type: 'string'); 31 | } 32 | 33 | public function test_throws_exception_for_invalid_field_name(): void 34 | { 35 | $this->expectException(InvalidArgumentException::class); 36 | $this->expectExceptionMessage('Invalid field name: 123invalid'); 37 | 38 | new FieldDefinition(name: '123invalid', type: 'string'); 39 | } 40 | 41 | public function test_throws_exception_for_unsupported_type(): void 42 | { 43 | $this->expectException(InvalidArgumentException::class); 44 | $this->expectExceptionMessage('Unsupported field type: unsupported'); 45 | 46 | new FieldDefinition(name: 'field', type: 'unsupported'); 47 | } 48 | 49 | public function test_gets_correct_database_type(): void 50 | { 51 | $stringField = new FieldDefinition('name', 'string'); 52 | $this->assertEquals('string', $stringField->getDatabaseType()); 53 | 54 | $intField = new FieldDefinition('age', 'integer'); 55 | $this->assertEquals('integer', $intField->getDatabaseType()); 56 | 57 | $boolField = new FieldDefinition('active', 'boolean'); 58 | $this->assertEquals('boolean', $boolField->getDatabaseType()); 59 | } 60 | 61 | public function test_gets_correct_php_type(): void 62 | { 63 | $stringField = new FieldDefinition('name', 'string'); 64 | $this->assertEquals('string', $stringField->getPhpType()); 65 | 66 | $intField = new FieldDefinition('age', 'integer'); 67 | $this->assertEquals('int', $intField->getPhpType()); 68 | 69 | $boolField = new FieldDefinition('active', 'boolean'); 70 | $this->assertEquals('bool', $boolField->getPhpType()); 71 | } 72 | 73 | public function test_gets_correct_validation_rule(): void 74 | { 75 | $stringField = new FieldDefinition('name', 'string'); 76 | $this->assertEquals('sometimes|string|max:255', $stringField->getValidationRule()); 77 | 78 | $requiredField = new FieldDefinition('email', 'string', false); 79 | $this->assertEquals('required|string|max:255', $requiredField->getValidationRule()); 80 | } 81 | 82 | public function test_gets_correct_fake_value(): void 83 | { 84 | $stringField = new FieldDefinition('name', 'string'); 85 | $this->assertEquals('fake()->word()', $stringField->getFakeValue()); 86 | 87 | $intField = new FieldDefinition('age', 'integer'); 88 | $this->assertEquals('fake()->randomNumber()', $intField->getFakeValue()); 89 | 90 | $uuidField = new FieldDefinition('id', 'uuid'); 91 | $this->assertEquals('fake()->uuid()', $uuidField->getFakeValue()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ValueObjects/EntityDefinition.php: -------------------------------------------------------------------------------- 1 | $fields 15 | * @param Collection $relationships 16 | */ 17 | public function __construct( 18 | public string $name, 19 | public Collection $fields, 20 | public Collection $relationships, 21 | public ?string $parent = null, 22 | public array $options = [] 23 | ) { 24 | $this->validateName($name); 25 | $this->validateFields($fields); 26 | $this->validateRelationships($relationships); 27 | } 28 | 29 | private function validateName(string $name): void 30 | { 31 | if (empty(trim($name))) { 32 | throw new InvalidArgumentException('Entity name cannot be empty'); 33 | } 34 | 35 | if (!preg_match('/^[A-Z][a-zA-Z0-9]*$/', $name)) { 36 | throw new InvalidArgumentException("Invalid entity name: {$name}. Must start with uppercase letter."); 37 | } 38 | } 39 | 40 | private function validateFields(Collection $fields): void 41 | { 42 | $fieldNames = $fields->pluck('name')->toArray(); 43 | $duplicates = array_filter(array_count_values($fieldNames), fn($count) => $count > 1); 44 | 45 | if (!empty($duplicates)) { 46 | throw new InvalidArgumentException('Duplicate field names found: ' . implode(', ', array_keys($duplicates))); 47 | } 48 | } 49 | 50 | private function validateRelationships(Collection $relationships): void 51 | { 52 | $relationshipRoles = $relationships->pluck('role')->toArray(); 53 | $duplicates = array_filter(array_count_values($relationshipRoles), fn($count) => $count > 1); 54 | 55 | if (!empty($duplicates)) { 56 | throw new InvalidArgumentException('Duplicate relationship roles found: ' . implode(', ', array_keys($duplicates))); 57 | } 58 | } 59 | 60 | public function getTableName(): string 61 | { 62 | return Str::plural(Str::snake($this->name)); 63 | } 64 | 65 | public function getPluralName(): string 66 | { 67 | return Str::plural(Str::lower($this->name)); 68 | } 69 | 70 | public function getNameLower(): string 71 | { 72 | return Str::lower($this->name); 73 | } 74 | 75 | public function getNameCamel(): string 76 | { 77 | return Str::camel($this->name); 78 | } 79 | 80 | public function getFillableFields(): array 81 | { 82 | $fillable = $this->fields->pluck('name')->toArray(); 83 | 84 | // Add foreign keys from relationships 85 | $foreignKeys = $this->relationships 86 | ->filter(fn(RelationshipDefinition $rel) => $rel->requiresForeignKey()) 87 | ->map(fn(RelationshipDefinition $rel) => $rel->getForeignKeyName()) 88 | ->toArray(); 89 | 90 | return array_merge($fillable, $foreignKeys); 91 | } 92 | 93 | public function getFieldsArray(): array 94 | { 95 | return $this->fields->mapWithKeys(function (FieldDefinition $field) { 96 | return [$field->name => $field->type]; 97 | })->toArray(); 98 | } 99 | 100 | public function hasRelationships(): bool 101 | { 102 | return $this->relationships->isNotEmpty(); 103 | } 104 | 105 | public function getRelationshipsByType(string $type): Collection 106 | { 107 | return $this->relationships->filter( 108 | fn(RelationshipDefinition $rel) => $rel->type === $type 109 | ); 110 | } 111 | 112 | public function hasParent(): bool 113 | { 114 | return $this->parent !== null; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/ValueObjects/FieldDefinition.php: -------------------------------------------------------------------------------- 1 | validateName($name); 20 | $this->validateType($type); 21 | } 22 | 23 | private function validateName(string $name): void 24 | { 25 | if (empty(trim($name))) { 26 | throw new InvalidArgumentException('Field name cannot be empty'); 27 | } 28 | 29 | if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) { 30 | throw new InvalidArgumentException("Invalid field name: {$name}"); 31 | } 32 | } 33 | 34 | private function validateType(string $type): void 35 | { 36 | $allowedTypes = [ 37 | 'string', 'integer', 'int', 'boolean', 'bool', 'text', 'float', 38 | 'decimal', 'json', 'date', 'datetime', 'timestamp', 'time', 39 | 'uuid', 'UUID', 'bigint' 40 | ]; 41 | 42 | if (!in_array($type, $allowedTypes, true)) { 43 | throw new InvalidArgumentException("Unsupported field type: {$type}"); 44 | } 45 | } 46 | 47 | public function getDatabaseType(): string 48 | { 49 | return match ($this->type) { 50 | 'string' => 'string', 51 | 'integer', 'int' => 'integer', 52 | 'boolean', 'bool' => 'boolean', 53 | 'text' => 'text', 54 | 'float' => 'decimal', 55 | 'decimal' => 'decimal', 56 | 'json' => 'json', 57 | 'date', 'datetime', 'timestamp', 'time' => 'timestamp', 58 | 'uuid', 'UUID' => 'uuid', 59 | 'bigint' => 'bigInteger', 60 | default => 'string' 61 | }; 62 | } 63 | 64 | public function getPhpType(): string 65 | { 66 | return match ($this->type) { 67 | 'string', 'text', 'uuid', 'UUID' => 'string', 68 | 'integer', 'int', 'bigint' => 'int', 69 | 'boolean', 'bool' => 'bool', 70 | 'float', 'decimal' => 'float', 71 | 'json' => 'array', 72 | 'date', 'datetime', 'timestamp', 'time' => '\DateTimeInterface', 73 | default => 'string' 74 | }; 75 | } 76 | 77 | public function getValidationRule(): string 78 | { 79 | if (!empty($this->validationRules)) { 80 | return implode('|', $this->validationRules); 81 | } 82 | 83 | $rule = match ($this->type) { 84 | 'string' => 'string|max:255', 85 | 'integer', 'int', 'bigint' => 'integer', 86 | 'boolean', 'bool' => 'boolean', 87 | 'text' => 'string', 88 | 'uuid', 'UUID' => 'uuid', 89 | 'float', 'decimal' => 'numeric', 90 | 'json' => 'json', 91 | 'date', 'datetime', 'timestamp' => 'date', 92 | default => 'string' 93 | }; 94 | 95 | return $this->nullable ? "sometimes|{$rule}" : "required|{$rule}"; 96 | } 97 | 98 | public function getFakeValue(): string 99 | { 100 | return match ($this->type) { 101 | 'string' => "fake()->word()", 102 | 'integer', 'int', 'bigint' => "fake()->randomNumber()", 103 | 'boolean', 'bool' => "fake()->boolean()", 104 | 'text' => "fake()->sentence()", 105 | 'uuid', 'UUID' => "fake()->uuid()", 106 | 'float', 'decimal' => "fake()->randomFloat(2, 1, 1000)", 107 | 'json' => "json_encode(['key' => 'value'])", 108 | 'date', 'datetime', 'timestamp', 'time' => "fake()->dateTime()", 109 | default => "fake()->word()" 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeApiCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 32 | 33 | if (empty($name)) { 34 | return $this->handleJsonGeneration(); 35 | } 36 | 37 | return $this->handleSingleEntityGeneration($name); 38 | } catch (CodeGeneratorException $e) { 39 | $this->error($e->getMessage()); 40 | return self::FAILURE; 41 | } catch (\Exception $e) { 42 | $this->error("An unexpected error occurred: {$e->getMessage()}"); 43 | return self::FAILURE; 44 | } 45 | } 46 | 47 | /** 48 | * Handle generation from JSON file. 49 | */ 50 | private function handleJsonGeneration(): int 51 | { 52 | $this->warn("No entity name provided. Using JSON file for generation..."); 53 | 54 | $jsonFilePath = base_path('class_data.json'); 55 | 56 | if (!File::exists($jsonFilePath)) { 57 | $this->error("JSON file not found: {$jsonFilePath}"); 58 | return self::FAILURE; 59 | } 60 | 61 | $jsonData = File::get($jsonFilePath); 62 | 63 | $this->info("Generating APIs from JSON data..."); 64 | $this->apiGenerationService->generateFromJson($jsonData); 65 | 66 | $this->info("API generation completed successfully!"); 67 | return self::SUCCESS; 68 | } 69 | 70 | /** 71 | * Handle generation for a single entity. 72 | */ 73 | private function handleSingleEntityGeneration(string $name): int 74 | { 75 | $fieldsOption = $this->option('fields'); 76 | 77 | if (!$fieldsOption) { 78 | $this->error('You must specify fields with the --fields option. Example: --fields="name:string,age:integer"'); 79 | return self::FAILURE; 80 | } 81 | 82 | $fieldsArray = FieldParser::parseFieldsString($fieldsOption); 83 | $definition = $this->createEntityDefinition($name, $fieldsArray); 84 | 85 | $this->info("Generating complete API for: {$name}"); 86 | $this->apiGenerationService->generateCompleteApi($definition); 87 | 88 | $this->info("API generation completed successfully!"); 89 | return self::SUCCESS; 90 | } 91 | 92 | /** 93 | * Create EntityDefinition from parsed fields. 94 | */ 95 | private function createEntityDefinition(string $name, array $fieldsArray): EntityDefinition 96 | { 97 | $fields = collect($fieldsArray)->map(function ($type, $fieldName) { 98 | return new FieldDefinition( 99 | name: $fieldName, 100 | type: $type 101 | ); 102 | }); 103 | 104 | return new EntityDefinition( 105 | name: ucfirst($name), 106 | fields: $fields, 107 | relationships: collect() // No relationships for single entity generation 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/EntitiesGenerator/ModelGeneratorRefactored.php: -------------------------------------------------------------------------------- 1 | name}.php"); 27 | } 28 | 29 | /** 30 | * Generate the content for the file. 31 | */ 32 | protected function generateContent(EntityDefinition $definition): string 33 | { 34 | return $this->processStub($definition); 35 | } 36 | 37 | /** 38 | * Get the stub name for this generator. 39 | */ 40 | protected function getStubName(): string 41 | { 42 | return 'model'; 43 | } 44 | 45 | /** 46 | * Get replacements for the stub. 47 | */ 48 | protected function getReplacements(EntityDefinition $definition): array 49 | { 50 | return [ 51 | 'modelName' => $definition->name, 52 | 'fillable' => $this->generateFillableArray($definition), 53 | 'relationships' => $this->generateRelationships($definition), 54 | 'parentClass' => $this->getParentClass($definition), 55 | 'imports' => $this->generateImports($definition), 56 | ]; 57 | } 58 | 59 | /** 60 | * Generate fillable array string. 61 | */ 62 | private function generateFillableArray(EntityDefinition $definition): string 63 | { 64 | $fillable = $definition->getFillableFields(); 65 | $fillableString = "'" . implode("', '", $fillable) . "'"; 66 | 67 | return "protected \$fillable = [{$fillableString}];"; 68 | } 69 | 70 | /** 71 | * Generate relationship methods. 72 | */ 73 | private function generateRelationships(EntityDefinition $definition): string 74 | { 75 | if (!$definition->hasRelationships()) { 76 | return ''; 77 | } 78 | 79 | $methods = []; 80 | 81 | foreach ($definition->relationships as $relationship) { 82 | $methods[] = $this->generateRelationshipMethod($relationship); 83 | } 84 | 85 | return implode("\n\n", $methods); 86 | } 87 | 88 | /** 89 | * Generate a single relationship method. 90 | */ 91 | private function generateRelationshipMethod(RelationshipDefinition $relationship): string 92 | { 93 | $methodName = $relationship->getMethodName(); 94 | $eloquentMethod = $relationship->getEloquentMethod(); 95 | $relatedModel = $relationship->relatedModel; 96 | 97 | return " public function {$methodName}() 98 | { 99 | return \$this->{$eloquentMethod}({$relatedModel}::class); 100 | }"; 101 | } 102 | 103 | /** 104 | * Get parent class for inheritance. 105 | */ 106 | private function getParentClass(EntityDefinition $definition): string 107 | { 108 | return $definition->hasParent() ? $definition->parent : 'Model'; 109 | } 110 | 111 | /** 112 | * Generate imports based on relationships and parent class. 113 | */ 114 | private function generateImports(EntityDefinition $definition): string 115 | { 116 | $imports = ['use Illuminate\Database\Eloquent\Model;']; 117 | 118 | if ($definition->hasParent()) { 119 | $imports[] = "use App\\Models\\{$definition->parent};"; 120 | } 121 | 122 | // Add imports for related models 123 | $relatedModels = $definition->relationships 124 | ->pluck('relatedModel') 125 | ->unique() 126 | ->map(fn($model) => "use App\\Models\\{$model};") 127 | ->toArray(); 128 | 129 | return implode("\n", array_merge($imports, $relatedModels)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Support/JsonParser.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function parseJsonToEntities(string $jsonData): Collection 22 | { 23 | $data = json_decode($jsonData, true); 24 | 25 | if (json_last_error() !== JSON_ERROR_NONE) { 26 | throw CodeGeneratorException::invalidJsonData(json_last_error_msg()); 27 | } 28 | 29 | // Handle different JSON formats 30 | $classes = $this->normalizeJsonData($data); 31 | 32 | return collect($classes)->map(function (array $classData) { 33 | return $this->createEntityDefinition($classData); 34 | }); 35 | } 36 | 37 | /** 38 | * Normalize JSON data to consistent format. 39 | */ 40 | private function normalizeJsonData(array $data): array 41 | { 42 | // Handle wrapped data format 43 | if (isset($data['data']) && is_array($data['data'])) { 44 | return [$data]; 45 | } 46 | 47 | // Handle array of entities 48 | if (is_array($data) && !isset($data['name'])) { 49 | return $data; 50 | } 51 | 52 | // Handle single entity 53 | return [$data]; 54 | } 55 | 56 | /** 57 | * Create EntityDefinition from array data. 58 | */ 59 | private function createEntityDefinition(array $classData): EntityDefinition 60 | { 61 | $class = isset($classData['data']) ? $classData['data'] : $classData; 62 | 63 | $name = ucfirst($class['name']); 64 | $parent = isset($class['parent']) ? ucfirst($class['parent']) : null; 65 | 66 | $fields = $this->parseFields($class['attributes'] ?? []); 67 | $relationships = $this->parseRelationships($class); 68 | 69 | return new EntityDefinition( 70 | name: $name, 71 | fields: $fields, 72 | relationships: $relationships, 73 | parent: $parent 74 | ); 75 | } 76 | 77 | /** 78 | * Parse fields from attributes array. 79 | * 80 | * @return Collection 81 | */ 82 | private function parseFields(array $attributes): Collection 83 | { 84 | return collect($attributes)->map(function (array $attribute) { 85 | return new FieldDefinition( 86 | name: $attribute['name'], 87 | type: $this->normalizeType($attribute['_type']) 88 | ); 89 | }); 90 | } 91 | 92 | /** 93 | * Parse relationships from class data. 94 | * 95 | * @return Collection 96 | */ 97 | private function parseRelationships(array $classData): Collection 98 | { 99 | $relationships = collect(); 100 | 101 | $relationTypes = [ 102 | 'oneToOneRelationships' => 'oneToOne', 103 | 'oneToManyRelationships' => 'oneToMany', 104 | 'manyToOneRelationships' => 'manyToOne', 105 | 'manyToManyRelationships' => 'manyToMany', 106 | ]; 107 | 108 | foreach ($relationTypes as $key => $type) { 109 | if (isset($classData[$key]) && is_array($classData[$key])) { 110 | foreach ($classData[$key] as $relation) { 111 | $relationships->push(new RelationshipDefinition( 112 | type: $type, 113 | relatedModel: ucfirst($relation['comodel']), 114 | role: $relation['role'] 115 | )); 116 | } 117 | } 118 | } 119 | 120 | return $relationships; 121 | } 122 | 123 | /** 124 | * Normalize type names. 125 | */ 126 | private function normalizeType(string $type): string 127 | { 128 | return match (strtolower($type)) { 129 | 'integer', 'long' => 'int', 130 | 'bigint' => 'int', 131 | 'str', 'string', 'text', 'java.time.offsetdatetime', 'java.time.localdate' => 'string', 132 | 'boolean' => 'bool', 133 | 'java.math.bigdecimal' => 'float', 134 | 'java.util.map' => 'json', 135 | default => $type, 136 | }; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Services/ApiGenerationService.php: -------------------------------------------------------------------------------- 1 | $generators 20 | */ 21 | public function __construct( 22 | private readonly Collection $generators, 23 | private readonly JsonParser $jsonParser 24 | ) {} 25 | 26 | /** 27 | * Generate a complete API for the given entity. 28 | */ 29 | public function generateCompleteApi(EntityDefinition $definition): bool 30 | { 31 | try { 32 | // Generate route first 33 | $this->generateApiRoute($definition); 34 | 35 | // Generate all components using registered generators 36 | foreach ($this->generators as $generator) { 37 | if ($generator->supports($definition)) { 38 | $generator->generate($definition); 39 | } 40 | } 41 | 42 | return true; 43 | } catch (\Exception $e) { 44 | throw CodeGeneratorException::generationFailed('API', $e->getMessage()); 45 | } 46 | } 47 | 48 | /** 49 | * Generate APIs from JSON data. 50 | */ 51 | public function generateFromJson(string $jsonData): bool 52 | { 53 | $entities = $this->jsonParser->parseJsonToEntities($jsonData); 54 | 55 | foreach ($entities as $entity) { 56 | $this->generateCompleteApi($entity); 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * Delete a complete API for the given entity. 64 | */ 65 | public function deleteCompleteApi(string $entityName): bool 66 | { 67 | // Implementation for deleting generated files 68 | // This would involve removing all generated files for the entity 69 | 70 | $filesToDelete = [ 71 | app_path("Models/{$entityName}.php"), 72 | app_path("Http/Controllers/{$entityName}Controller.php"), 73 | app_path("Http/Requests/{$entityName}Request.php"), 74 | app_path("Http/Resources/{$entityName}Resource.php"), 75 | app_path("Services/{$entityName}Service.php"), 76 | app_path("DTO/{$entityName}DTO.php"), 77 | app_path("Policies/{$entityName}Policy.php"), 78 | database_path("factories/{$entityName}Factory.php"), 79 | database_path("seeders/{$entityName}Seeder.php"), 80 | ]; 81 | 82 | foreach ($filesToDelete as $file) { 83 | if (File::exists($file)) { 84 | File::delete($file); 85 | } 86 | } 87 | 88 | // Remove migration files 89 | $tableName = Str::plural(Str::snake($entityName)); 90 | $migrations = glob(database_path("migrations/*_create_{$tableName}_table.php")); 91 | foreach ($migrations as $migration) { 92 | File::delete($migration); 93 | } 94 | 95 | // Remove route from api.php 96 | $this->removeApiRoute($entityName); 97 | 98 | return true; 99 | } 100 | 101 | /** 102 | * Generate API route for the entity. 103 | */ 104 | private function generateApiRoute(EntityDefinition $definition): void 105 | { 106 | $route = "Route::apiResource('{$definition->getPluralName()}', App\\Http\\Controllers\\{$definition->name}Controller::class);"; 107 | $apiFilePath = base_path('routes/api.php'); 108 | $phpHeader = "name}.php"); 158 | } 159 | 160 | protected function generateContent(EntityDefinition $definition): string 161 | { 162 | return $this->processStub($definition); 163 | } 164 | 165 | protected function getStubName(): string 166 | { 167 | return 'custom'; 168 | } 169 | 170 | protected function getReplacements(EntityDefinition $definition): array 171 | { 172 | return [ 173 | 'className' => $definition->name, 174 | // ... other replacements 175 | ]; 176 | } 177 | } 178 | ``` 179 | 180 | ## Documentation 181 | 182 | - Update README.md for new features 183 | - Add PHPDoc comments to all public methods 184 | - Include code examples in documentation 185 | - Update CHANGELOG.md 186 | 187 | ## Testing 188 | 189 | Run the test suite: 190 | 191 | ```bash 192 | # Run all tests 193 | composer test 194 | 195 | # Run with coverage 196 | composer test-coverage 197 | 198 | # Run static analysis 199 | composer analyse 200 | 201 | # Format code 202 | composer format 203 | ``` 204 | 205 | ## Questions? 206 | 207 | If you have questions about contributing, please: 208 | 209 | 1. Check existing issues and documentation 210 | 2. Create a new issue with the "question" label 211 | 3. Join our discussions 212 | 213 | Thank you for contributing! 214 | -------------------------------------------------------------------------------- /config/laravel-api-generator.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../stubs', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Generated Files Configuration 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you can configure the paths and namespaces for generated files. 24 | | These paths are relative to the Laravel application root. 25 | | 26 | */ 27 | 'paths' => [ 28 | 'models' => 'app/Models', 29 | 'controllers' => 'app/Http/Controllers', 30 | 'requests' => 'app/Http/Requests', 31 | 'resources' => 'app/Http/Resources', 32 | 'services' => 'app/Services', 33 | 'dto' => 'app/DTO', 34 | 'policies' => 'app/Policies', 35 | 'factories' => 'database/factories', 36 | 'seeders' => 'database/seeders', 37 | 'migrations' => 'database/migrations', 38 | ], 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Namespaces Configuration 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Configure the namespaces for generated classes. 46 | | 47 | */ 48 | 'namespaces' => [ 49 | 'models' => 'App\\Models', 50 | 'controllers' => 'App\\Http\\Controllers', 51 | 'requests' => 'App\\Http\\Requests', 52 | 'resources' => 'App\\Http\\Resources', 53 | 'services' => 'App\\Services', 54 | 'dto' => 'App\\DTO', 55 | 'policies' => 'App\\Policies', 56 | ], 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Default Field Types 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Define the default field types and their corresponding database types, 64 | | validation rules, and factory values. 65 | | 66 | */ 67 | 'field_types' => [ 68 | 'string' => [ 69 | 'database' => 'string', 70 | 'php' => 'string', 71 | 'validation' => 'string|max:255', 72 | 'factory' => 'fake()->word()', 73 | ], 74 | 'text' => [ 75 | 'database' => 'text', 76 | 'php' => 'string', 77 | 'validation' => 'string', 78 | 'factory' => 'fake()->sentence()', 79 | ], 80 | 'integer' => [ 81 | 'database' => 'integer', 82 | 'php' => 'int', 83 | 'validation' => 'integer', 84 | 'factory' => 'fake()->randomNumber()', 85 | ], 86 | 'boolean' => [ 87 | 'database' => 'boolean', 88 | 'php' => 'bool', 89 | 'validation' => 'boolean', 90 | 'factory' => 'fake()->boolean()', 91 | ], 92 | 'float' => [ 93 | 'database' => 'decimal', 94 | 'php' => 'float', 95 | 'validation' => 'numeric', 96 | 'factory' => 'fake()->randomFloat(2, 1, 1000)', 97 | ], 98 | 'json' => [ 99 | 'database' => 'json', 100 | 'php' => 'array', 101 | 'validation' => 'json', 102 | 'factory' => 'json_encode([\'key\' => \'value\'])', 103 | ], 104 | 'date' => [ 105 | 'database' => 'date', 106 | 'php' => '\DateTimeInterface', 107 | 'validation' => 'date', 108 | 'factory' => 'fake()->date()', 109 | ], 110 | 'datetime' => [ 111 | 'database' => 'datetime', 112 | 'php' => '\DateTimeInterface', 113 | 'validation' => 'date', 114 | 'factory' => 'fake()->dateTime()', 115 | ], 116 | 'timestamp' => [ 117 | 'database' => 'timestamp', 118 | 'php' => '\DateTimeInterface', 119 | 'validation' => 'date', 120 | 'factory' => 'fake()->dateTime()', 121 | ], 122 | 'uuid' => [ 123 | 'database' => 'uuid', 124 | 'php' => 'string', 125 | 'validation' => 'uuid', 126 | 'factory' => 'fake()->uuid()', 127 | ], 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Generator Configuration 133 | |-------------------------------------------------------------------------- 134 | | 135 | | Configure which generators should be enabled and their order of execution. 136 | | 137 | */ 138 | 'generators' => [ 139 | 'model' => true, 140 | 'migration' => true, 141 | 'controller' => true, 142 | 'request' => true, 143 | 'resource' => true, 144 | 'service' => true, 145 | 'dto' => true, 146 | 'policy' => true, 147 | 'factory' => true, 148 | 'seeder' => true, 149 | ], 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | API Route Configuration 154 | |-------------------------------------------------------------------------- 155 | | 156 | | Configure how API routes should be generated. 157 | | 158 | */ 159 | 'routes' => [ 160 | 'file' => 'routes/api.php', 161 | 'prefix' => 'api', 162 | 'middleware' => ['api'], 163 | ], 164 | ]; 165 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "person", 4 | "type": "class", 5 | "attributes": [ 6 | { 7 | "visibility": "public", 8 | "name": "name", 9 | "type": "str" 10 | }, 11 | { 12 | "visibility": "public", 13 | "name": "phonenumber", 14 | "type": "str" 15 | }, 16 | { 17 | "visibility": "public", 18 | "name": "emailaddress", 19 | "type": "str" 20 | }, 21 | { 22 | "visibility": "private", 23 | "type": "Address", 24 | "name": "address" 25 | } 26 | ], 27 | "methods": [ 28 | { 29 | "visibility": "public", 30 | "name": "purchaseparkingpass", 31 | "type": "void", 32 | "args": [] 33 | } 34 | ], 35 | "aggregations": [], 36 | "compositions": [], 37 | "import_list": true 38 | }, 39 | { 40 | "name": "student", 41 | "type": "class", 42 | "attributes": [ 43 | { 44 | "visibility": "public", 45 | "name": "studentnumber", 46 | "type": "int" 47 | }, 48 | { 49 | "visibility": "public", 50 | "name": "averagemark", 51 | "type": "int" 52 | } 53 | ], 54 | "methods": [ 55 | { 56 | "visibility": "public", 57 | "name": "iseligibletoenroll", 58 | "type": "bool", 59 | "args": [ 60 | { 61 | "type": "Object", 62 | "name": "str" 63 | } 64 | ] 65 | }, 66 | { 67 | "visibility": "public", 68 | "name": "getseminarstaken", 69 | "type": "int", 70 | "args": [ 71 | { 72 | "type": "string", 73 | "name": "papa" 74 | } 75 | ] 76 | } 77 | ], 78 | "aggregations": [], 79 | "compositions": [], 80 | "parent": "person", 81 | "import_list": true 82 | }, 83 | { 84 | "name": "professor", 85 | "type": "class", 86 | "attributes": [ 87 | { 88 | "visibility": "public", 89 | "name": "name", 90 | "type": "void" 91 | }, 92 | { 93 | "visibility": "protected", 94 | "name": "staffnumber", 95 | "type": "int" 96 | }, 97 | { 98 | "visibility": "private", 99 | "name": "yearsofservice", 100 | "type": "int" 101 | }, 102 | { 103 | "visibility": "public", 104 | "name": "numberofclasses", 105 | "type": "int" 106 | } 107 | ], 108 | "methods": [], 109 | "aggregations": [], 110 | "compositions": [ 111 | { 112 | "visibility": "private", 113 | "type": "string", 114 | "name": "adressess" 115 | } 116 | ], 117 | "parent": "person", 118 | "import_list": true 119 | }, 120 | { 121 | "name": "address", 122 | "type": "class", 123 | "attributes": [ 124 | { 125 | "visibility": "public", 126 | "name": "street", 127 | "type": "str" 128 | }, 129 | { 130 | "visibility": "public", 131 | "name": "city", 132 | "type": "str" 133 | }, 134 | { 135 | "visibility": "public", 136 | "name": "state", 137 | "type": "str" 138 | }, 139 | { 140 | "visibility": "public", 141 | "name": "postalcode", 142 | "type": "int" 143 | }, 144 | { 145 | "visibility": "public", 146 | "name": "country", 147 | "type": "str" 148 | } 149 | ], 150 | "methods": [ 151 | { 152 | "visibility": "private", 153 | "name": "validate", 154 | "type": "bool", 155 | "args": [] 156 | }, 157 | { 158 | "visibility": "public", 159 | "name": "outputaslabel", 160 | "type": "str", 161 | "args": [] 162 | } 163 | ], 164 | "aggregations": [], 165 | "compositions": [] 166 | }, 167 | { 168 | "name": "adresses", 169 | "type": "class", 170 | "attributes": [ 171 | { 172 | "visibility": "public", 173 | "name": "city", 174 | "type": "str" 175 | }, 176 | { 177 | "visibility": "public", 178 | "name": "state", 179 | "type": "str" 180 | }, 181 | { 182 | "visibility": "public", 183 | "name": "postalcode", 184 | "type": "int" 185 | }, 186 | { 187 | "visibility": "public", 188 | "name": "country", 189 | "type": "str" 190 | } 191 | ], 192 | "methods": [ 193 | { 194 | "visibility": "private", 195 | "name": "validate", 196 | "type": "bool", 197 | "args": [] 198 | }, 199 | { 200 | "visibility": "public", 201 | "name": "outputaslabel", 202 | "type": "str", 203 | "args": [] 204 | } 205 | ], 206 | "aggregations": [], 207 | "compositions": [] 208 | } 209 | ] -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-api-generator` will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.0.1] - 2025-06-28 9 | 10 | ### 📚 Documentation & Polish 11 | 12 | ### Updated 13 | - **Documentation Improvements** 14 | - Completely updated README.md with modern features and architecture examples 15 | - Added comprehensive usage examples with new syntax 16 | - Improved installation and quick start guides 17 | - Added architecture overview with Service Layer and DTO examples 18 | - Enhanced field types documentation 19 | - Added configuration and testing sections 20 | 21 | ### Fixed 22 | - Minor documentation formatting and consistency issues 23 | - Updated examples to reflect current v3.0+ architecture 24 | 25 | ### Added 26 | - Better code examples showing modern PHP 8.1+ features 27 | - Enhanced Quick Start section with single entity and bulk generation examples 28 | - Comprehensive field types and relationship documentation 29 | 30 | ## [3.0.0] - 2025-06-28 31 | 32 | ### 🚀 Major Refactoring - Clean Architecture Implementation 33 | 34 | This is a major release that completely refactors the package architecture for better maintainability, extensibility, and professionalism. 35 | 36 | ### Added 37 | - **Clean Architecture Implementation** 38 | - Value Objects for domain modeling (EntityDefinition, FieldDefinition, RelationshipDefinition) 39 | - Service Layer pattern with proper dependency injection 40 | - Contracts/Interfaces for better testability 41 | - Professional error handling with custom exceptions 42 | 43 | - **Enhanced Generator System** 44 | - AbstractGenerator base class for extensibility 45 | - Improved stub system with better placeholder handling 46 | - Support for complex relationships and inheritance 47 | - Type-safe field definitions and validation 48 | 49 | - **Developer Experience** 50 | - Comprehensive PHPDoc comments 51 | - PHPStan level 8 static analysis 52 | - GitHub Actions CI/CD pipeline 53 | - Professional contributing guidelines 54 | - Comprehensive test structure 55 | 56 | - **New Features** 57 | - JSON parser with robust error handling 58 | - Field parser with validation 59 | - Stub loader system 60 | - Professional configuration system 61 | 62 | ### Changed 63 | - **Architecture**: Complete rewrite using clean architecture principles 64 | - **Type Safety**: Full PHP 8.1+ type declarations with readonly properties 65 | - **Error Handling**: Professional exception handling throughout 66 | - **Code Quality**: SOLID principles compliance 67 | - **Documentation**: Complete rewrite with professional formatting 68 | 69 | ### Fixed 70 | - **Model Generation**: Fixed issues with relationships and inheritance 71 | - **JSON Parsing**: Better handling of different JSON formats 72 | - **Stub Processing**: Resolved placeholder replacement issues 73 | - **Field Types**: Improved type mapping and validation 74 | 75 | ### Technical Improvements 76 | - **PHP 8.1+ Features**: Constructor property promotion, readonly classes, match expressions 77 | - **Dependency Injection**: Proper DI container usage throughout 78 | - **Static Analysis**: PHPStan level 8 compliance 79 | - **Code Style**: Laravel Pint formatting 80 | - **Testing**: Comprehensive test structure 81 | 82 | ### Breaking Changes 83 | ⚠️ **This is a major version with breaking changes** 84 | - Namespace changes for better organization 85 | - Service provider restructuring 86 | - Command signature improvements 87 | - Configuration format updates 88 | - 🏗️ **Complete Architecture Refactoring** 89 | - Value Objects for type-safe domain modeling 90 | - Service Layer pattern implementation 91 | - Dependency Injection container integration 92 | - Clean Architecture principles 93 | - SOLID principles compliance 94 | 95 | - 🔧 **New Features** 96 | - Professional DTO generation with readonly classes 97 | - Enhanced JSON parsing with relationship support 98 | - Configurable field types and validation rules 99 | - Extensible generator system 100 | - Custom exceptions and error handling 101 | 102 | - 📁 **Improved Project Structure** 103 | - Contracts/Interfaces for better testability 104 | - Support classes for utilities 105 | - Organized generators by responsibility 106 | - Professional documentation 107 | 108 | - 🚀 **Enhanced Code Generation** 109 | - Type-safe PHP 8.1+ code generation 110 | - Improved stub system with better templating 111 | - Relationship handling (One-to-One, One-to-Many, Many-to-Many) 112 | - Foreign key management 113 | - Fillable properties automation 114 | 115 | - 🧪 **Quality Improvements** 116 | - PHPStan level 8 compliance 117 | - Comprehensive test structure 118 | - GitHub Actions CI/CD pipeline 119 | - Code style with Laravel Pint 120 | - Professional documentation 121 | 122 | ### Changed 123 | - **Breaking**: Refactored entire codebase for clean architecture 124 | - **Breaking**: Updated minimum PHP version to 8.1 125 | - **Breaking**: New namespace structure 126 | - Improved error messages and validation 127 | - Enhanced JSON data parsing logic 128 | - Better relationship detection and handling 129 | 130 | ### Fixed 131 | - Model generation with proper inheritance handling 132 | - Duplicate import statements 133 | - Foreign key generation issues 134 | - Stub placeholder processing 135 | - Route generation and management 136 | 137 | ### Removed 138 | - Legacy code patterns 139 | - Outdated stub formats 140 | - Unnecessary dependencies 141 | 142 | ## [2.0.6] - Previous Version 143 | - Legacy implementation with basic functionality 144 | 145 | --- 146 | 147 | ## Migration Guide from 2.x to 3.0 148 | 149 | ### What's Changed 150 | 1. **PHP Version**: Minimum PHP 8.1 required 151 | 2. **Architecture**: Complete refactoring to clean architecture 152 | 3. **Type Safety**: Full type declarations throughout 153 | 4. **Better Error Handling**: Custom exceptions with clear messages 154 | 155 | ### How to Upgrade 156 | 1. Update your PHP version to 8.1+ 157 | 2. Update the package: `composer update nameless/laravel-api-generator` 158 | 3. Clear your cache: `php artisan cache:clear` 159 | 4. Re-generate your APIs to benefit from new features 160 | 161 | The package maintains backward compatibility for the main commands: 162 | - `php artisan make:fullapi EntityName --fields="field:type"` 163 | - `php artisan make:fullapi` (JSON mode) 164 | -------------------------------------------------------------------------------- /src/Console/Commands/DeleteFullApi.php: -------------------------------------------------------------------------------- 1 | argument('name'); 19 | if (empty($name)) { 20 | $this->warn("Aucun nom fourni. Utilisation du nom par défaut du fichier JSON."); 21 | $jsonFilePath = base_path('class_data.json'); 22 | if (!file_exists($jsonFilePath)) { 23 | $this->error("Le fichier class_data.json est introuvable."); 24 | return; 25 | } 26 | 27 | $this->info("Lecture du fichier JSON..."); 28 | $jsonData = file_get_contents($jsonFilePath); 29 | $this->classes = json_decode($jsonData, true); 30 | 31 | if (json_last_error() !== JSON_ERROR_NONE) { 32 | $this->error("Erreur de décodage JSON : " . json_last_error_msg()); 33 | return; 34 | } 35 | 36 | $this->info("Extraction des données JSON..."); 37 | $this->jsonExtractionToArray(); 38 | 39 | $this->info("Delete API with diagram..."); 40 | $this->runDeleteApiWithDiagram(); 41 | return; 42 | } 43 | $pluralName = Str::plural(Str::snake($name)); 44 | $className = Str::studly($name); 45 | 46 | $this->info("Suppression des fichiers pour : {$name}"); 47 | 48 | // Supprimer le modèle 49 | $this->deleteFile(app_path("Models/{$className}.php"), "Modèle"); 50 | 51 | // Supprimer les migrations 52 | $this->deleteFilesByPattern(database_path('migrations'), "*_create_{$pluralName}_table.php", "Migration"); 53 | 54 | // Supprimer le service 55 | $this->deleteFile(app_path("Services/{$className}Service.php"), "Service"); 56 | 57 | // Supprimer la policy 58 | $this->deleteFile(app_path("Policies/{$className}Policy.php"), "Policy"); 59 | $this->removeFromAuthServiceProvider($className); 60 | 61 | // Supprimer le contrôleur 62 | $this->deleteFile(app_path("Http/Controllers/{$className}Controller.php"), "Contrôleur"); 63 | 64 | // Supprimer la resource 65 | $this->deleteFile(app_path("Http/Resources/{$className}Resource.php"), "Resource"); 66 | 67 | // Supprimer la requête 68 | $this->deleteFile(app_path("Http/Requests/{$className}Request.php"), "Requête"); 69 | 70 | // Supprimer le seeder 71 | $this->deleteFile(database_path("seeders/{$className}Seeder.php"), "Seeder"); 72 | 73 | // Supprimer le factory 74 | $this->deleteFile(database_path("factories/{$className}Factory.php"), "Factory"); 75 | 76 | // Supprimer le DTO 77 | $this->deleteFile(app_path("DTO/{$className}DTO.php"), "DTO"); 78 | 79 | $this->info("Tous les fichiers associés à {$name} ont été supprimés."); 80 | } 81 | 82 | /** 83 | * Extraire et formater les données JSON dans un tableau compatible. 84 | */ 85 | public function jsonExtractionToArray() 86 | { 87 | $this->classes = array_map(function ($class) { 88 | return [ 89 | 'name' => ucfirst($class['name']), 90 | 'attributes' => array_map(function ($attribute) { 91 | return [ 92 | 'name' => $attribute['name'], 93 | '_type' => match (strtolower($attribute['_type'])) { 94 | 'integer' => 'int', 95 | 'bigint' => 'int', 96 | 'str', 'text' => 'string', 97 | 'boolean' => 'bool', 98 | default => $attribute['_type'], 99 | }, 100 | ]; 101 | }, $class['attributes']), 102 | ]; 103 | }, $this->classes); 104 | } 105 | 106 | /** 107 | * Parcourir les classes et exécuter les commandes Artisan pour générer les API. 108 | */ 109 | public function runDeleteApiWithDiagram() 110 | { 111 | foreach ($this->classes as $class) { 112 | $className = ucfirst($class['name']); 113 | 114 | echo "Ici les parametres : ".$className. "\n"; 115 | 116 | try { 117 | Artisan::call("delete:fullapi {$className}"); 118 | 119 | $this->info("API pour la classe $className générée avec succès !"); 120 | } catch (\Exception $e) { 121 | $this->error("Erreur lors de la génération de l'API pour la classe $className : " . $e->getMessage()); 122 | } 123 | } 124 | } 125 | 126 | private function deleteFile($filePath, $type) 127 | { 128 | if (File::exists($filePath)) { 129 | File::delete($filePath); 130 | $this->info("{$type} supprimé : {$filePath}"); 131 | } else { 132 | $this->warn("{$type} introuvable : {$filePath}"); 133 | } 134 | } 135 | 136 | private function deleteFilesByPattern($directory, $pattern, $type) 137 | { 138 | $files = File::glob("{$directory}/{$pattern}"); 139 | if ($files) { 140 | foreach ($files as $file) { 141 | File::delete($file); 142 | $this->info("{$type} supprimé : {$file}"); 143 | } 144 | } else { 145 | $this->warn("Aucun fichier {$type} correspondant au motif : {$pattern}"); 146 | } 147 | } 148 | 149 | private function removeFromAuthServiceProvider($className) 150 | { 151 | $providerPath = app_path('Providers/AuthServiceProvider.php'); 152 | 153 | if (!file_exists($providerPath)) { 154 | $this->warn("AuthServiceProvider n'existe pas."); 155 | return; 156 | } 157 | $content = file_get_contents($providerPath); 158 | 159 | // Supprimer les imports 160 | $content = preg_replace("/use App\\\\Models\\\\{$className};\n/", '', $content); 161 | $content = preg_replace("/use App\\\\Policies\\\\{$className}Policy;\n/", '', $content); 162 | 163 | // Supprimer le mapping de la policy 164 | $content = preg_replace("/\s*{$className}::class => {$className}Policy::class,/", '', $content); 165 | 166 | file_put_contents($providerPath, $content); 167 | $this->info("Policy supprimée de AuthServiceProvider"); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /README_NEW.md: -------------------------------------------------------------------------------- 1 | # Laravel API Generator 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/nameless/laravel-api-generator.svg)](https://packagist.org/packages/nameless/laravel-api-generator) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/nameless/laravel-api-generator.svg)](https://packagist.org/packages/nameless/laravel-api-generator) 5 | [![License](https://img.shields.io/packagist/l/nameless/laravel-api-generator.svg)](https://packagist.org/packages/nameless/laravel-api-generator) 6 | 7 | **Laravel API Generator** is a professional, enterprise-grade Laravel package that generates complete API structures following best practices and clean architecture principles. 8 | 9 | ## 🚀 Features 10 | 11 | ### ✨ Complete API Generation 12 | - **Models** with proper relationships and fillable properties 13 | - **RESTful Controllers** with full CRUD operations 14 | - **Service Layer** implementation for business logic 15 | - **Data Transfer Objects (DTOs)** for type-safe data handling 16 | - **Form Request Validations** with intelligent rules 17 | - **API Resources** for consistent response formatting 18 | - **Policies** for authorization 19 | - **Database Factories** with realistic fake data 20 | - **Seeders** for test data generation 21 | - **Migrations** with proper foreign keys and constraints 22 | 23 | ### 🏗️ Architecture & Design Patterns 24 | - **Clean Architecture** with separated concerns 25 | - **Repository Pattern** with service layer 26 | - **Value Objects** for domain modeling 27 | - **Dependency Injection** throughout 28 | - **SOLID Principles** compliance 29 | - **Type Safety** with PHP 8.1+ features 30 | 31 | ### 🔧 Advanced Features 32 | - **JSON Schema Support** for bulk generation 33 | - **Relationship Management** (One-to-One, One-to-Many, Many-to-Many) 34 | - **Inheritance Support** for model hierarchies 35 | - **Custom Field Types** with validation rules 36 | - **Extensible Generator System** 37 | - **Professional Error Handling** 38 | 39 | ## 📦 Installation 40 | 41 | ```bash 42 | composer require nameless/laravel-api-generator 43 | ``` 44 | 45 | The package automatically registers its service provider. 46 | 47 | ## 🎯 Quick Start 48 | 49 | ### Single Entity Generation 50 | 51 | Generate a complete API for a single entity: 52 | 53 | ```bash 54 | php artisan make:fullapi User --fields="name:string,email:string,age:integer,is_active:boolean" 55 | ``` 56 | 57 | This creates: 58 | - `app/Models/User.php` 59 | - `app/Http/Controllers/UserController.php` 60 | - `app/Http/Requests/UserRequest.php` 61 | - `app/Http/Resources/UserResource.php` 62 | - `app/Services/UserService.php` 63 | - `app/DTO/UserDTO.php` 64 | - `app/Policies/UserPolicy.php` 65 | - `database/factories/UserFactory.php` 66 | - `database/seeders/UserSeeder.php` 67 | - `database/migrations/xxxx_create_users_table.php` 68 | - API routes in `routes/api.php` 69 | 70 | ### Bulk Generation from JSON 71 | 72 | Create a `class_data.json` file in your project root: 73 | 74 | ```json 75 | [ 76 | { 77 | "name": "User", 78 | "attributes": [ 79 | {"name": "name", "_type": "string"}, 80 | {"name": "email", "_type": "string"}, 81 | {"name": "email_verified_at", "_type": "timestamp"} 82 | ], 83 | "oneToManyRelationships": [ 84 | {"role": "posts", "comodel": "Post"} 85 | ] 86 | }, 87 | { 88 | "name": "Post", 89 | "attributes": [ 90 | {"name": "title", "_type": "string"}, 91 | {"name": "content", "_type": "text"}, 92 | {"name": "published_at", "_type": "timestamp"} 93 | ], 94 | "manyToOneRelationships": [ 95 | {"role": "user", "comodel": "User"} 96 | ] 97 | } 98 | ] 99 | ``` 100 | 101 | Then run: 102 | 103 | ```bash 104 | php artisan make:fullapi 105 | ``` 106 | 107 | ## 🏗️ Architecture Overview 108 | 109 | ### Service Layer Pattern 110 | 111 | The generated code follows the Service Layer pattern for better organization: 112 | 113 | ```php 114 | class UserController extends Controller 115 | { 116 | public function __construct( 117 | private readonly UserService $service 118 | ) {} 119 | 120 | public function store(UserRequest $request) 121 | { 122 | $dto = UserDTO::fromRequest($request); 123 | $user = $this->service->create($dto); 124 | return new UserResource($user); 125 | } 126 | } 127 | ``` 128 | 129 | ### Data Transfer Objects 130 | 131 | Type-safe data handling with DTOs: 132 | 133 | ```php 134 | readonly class UserDTO 135 | { 136 | public function __construct( 137 | public ?string $name, 138 | public ?string $email, 139 | public ?int $age, 140 | public ?bool $is_active, 141 | ) {} 142 | 143 | public static function fromRequest(Request $request): self 144 | { 145 | return new self( 146 | name: $request->get('name'), 147 | email: $request->get('email'), 148 | age: $request->get('age'), 149 | is_active: $request->get('is_active'), 150 | ); 151 | } 152 | } 153 | ``` 154 | 155 | ## 🛠️ Advanced Usage 156 | 157 | ### Custom Field Types 158 | 159 | Supported field types: 160 | - `string` - VARCHAR(255) 161 | - `text` - TEXT 162 | - `integer`/`int` - INTEGER 163 | - `bigint` - BIG INTEGER 164 | - `boolean`/`bool` - BOOLEAN 165 | - `float`/`decimal` - DECIMAL 166 | - `json` - JSON 167 | - `date` - DATE 168 | - `datetime` - DATETIME 169 | - `timestamp` - TIMESTAMP 170 | - `uuid` - UUID 171 | 172 | ### Relationship Types 173 | 174 | The generator supports all Laravel relationship types: 175 | 176 | - **One-to-One**: `oneToOneRelationships` 177 | - **One-to-Many**: `oneToManyRelationships` 178 | - **Many-to-One**: `manyToOneRelationships` 179 | - **Many-to-Many**: `manyToManyRelationships` 180 | 181 | ### Model Inheritance 182 | 183 | Support for model inheritance: 184 | 185 | ```json 186 | { 187 | "name": "AdminUser", 188 | "parent": "User", 189 | "attributes": [ 190 | {"name": "permissions", "_type": "json"} 191 | ] 192 | } 193 | ``` 194 | 195 | ## 🔧 Configuration 196 | 197 | ### Custom Stubs 198 | 199 | You can customize the generated code by publishing and modifying the stubs: 200 | 201 | ```bash 202 | php artisan vendor:publish --tag=laravel-api-generator-stubs 203 | ``` 204 | 205 | ### Service Registration 206 | 207 | The package automatically registers all generators and services through dependency injection. 208 | 209 | ## 🧪 Testing 210 | 211 | ```bash 212 | composer test 213 | ``` 214 | 215 | ## 📖 API Documentation 216 | 217 | The package integrates with [Scramble](https://github.com/dedoc/scramble) for automatic API documentation generation. 218 | 219 | ## 🤝 Contributing 220 | 221 | Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. 222 | 223 | ## 🔒 Security 224 | 225 | If you discover any security-related issues, please email the maintainer instead of using the issue tracker. 226 | 227 | ## 📄 License 228 | 229 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 230 | 231 | ## 🏆 Credits 232 | 233 | - **Author**: Mbassi Loic Aron 234 | - **Email**: loicmbassi5@email.com 235 | 236 | ## 📚 Changelog 237 | 238 | Please see [CHANGELOG.md](CHANGELOG.md) for more information on what has changed recently. 239 | 240 | --- 241 | 242 | ## 💡 Why Choose Laravel API Generator? 243 | 244 | ✅ **Professional Architecture** - Built with enterprise-grade patterns 245 | ✅ **Type Safety** - Full PHP 8.1+ type declarations 246 | ✅ **Clean Code** - SOLID principles and clean architecture 247 | ✅ **Extensible** - Easy to extend with custom generators 248 | ✅ **Well Tested** - Comprehensive test suite 249 | ✅ **Documentation** - Complete API documentation generation 250 | ✅ **Best Practices** - Follows Laravel and PHP best practices 251 | 252 | Transform your Laravel development workflow with professional API generation! 253 | -------------------------------------------------------------------------------- /src/Console/Commands/InstallPackageCommand.php: -------------------------------------------------------------------------------- 1 | info('Installing API Generator Package...'); 26 | // $this->installBreeze(); 27 | // Demander si l'utilisateur veut un starter kit d'authentification 28 | $wantsAuth = $this->confirm('Would you like to install an authentication starter kit?', true); 29 | 30 | if ($wantsAuth) { 31 | $authChoice = $this->choice( 32 | 'Which authentication starter kit would you prefer?', 33 | [ 34 | 'breeze' => 'Laravel Breeze (Lightweight, minimal)', 35 | 'ui' => 'Laravel UI (Traditional Bootstrap)', 36 | 'none' => 'No authentication starter kit' 37 | ], 38 | 'breeze' 39 | ); 40 | 41 | if ($authChoice === 'breeze') { 42 | $this->installBreezeWithOptions(); 43 | } elseif ($authChoice === 'ui') { 44 | $this->installLaravelUI(); 45 | } 46 | }else{ 47 | $this->info('No authentication starter kit installed.'); 48 | $this->call('install:api'); 49 | 50 | } 51 | 52 | $this->publishScrambleConfig(); 53 | 54 | // Ajouter le provider de Scramble dans config/app.php si pas déjà présent 55 | $this->registerScrambleProvider(); 56 | 57 | // Publier les autres fichiers de configuration de votre package si nécessaire 58 | $this->publishPackageConfig(); 59 | //installation des 60 | 61 | $this->info('Installation completed! 🎉'); 62 | 63 | // $this->call('install:api'); 64 | 65 | $this->showPostInstallationMessage(); 66 | } finally { 67 | self::$isRunning = false; 68 | } 69 | 70 | 71 | 72 | } 73 | 74 | protected function publishScrambleConfig() 75 | { 76 | $this->info('Publishing Scramble configuration...'); 77 | $this->call('vendor:publish', [ 78 | '--provider' => 'Dedoc\Scramble\ScrambleServiceProvider', 79 | '--force' => true 80 | ]); 81 | } 82 | 83 | protected function registerScrambleProvider() 84 | { 85 | $this->info('Registering Scramble Service Provider...'); 86 | 87 | $config_app = config_path('app.php'); 88 | $provider = \Dedoc\Scramble\ScrambleServiceProvider::class; 89 | 90 | if (File::exists($config_app)) { 91 | $contents = File::get($config_app); 92 | 93 | if (!str_contains($contents, $provider)) { 94 | $providers = str_replace( 95 | 'providers\' => [', 96 | 'providers\' => [' . PHP_EOL . ' ' . $provider . '::class,', 97 | $contents 98 | ); 99 | 100 | File::put($config_app, $providers); 101 | $this->info('Scramble Service Provider registered successfully.'); 102 | } else { 103 | $this->info('Scramble Service Provider already registered.'); 104 | } 105 | } 106 | } 107 | 108 | protected function publishPackageConfig() 109 | { 110 | $this->info('Publishing API Generator configuration...'); 111 | //Execute this commande : php artisan install:api 112 | 113 | $this->call('vendor:publish', [ 114 | '--provider' => 'nameless\CodeGenerator\Providers\CodeGeneratorServiceProvider', 115 | '--tag' => 'config', 116 | '--force' => true 117 | ]); 118 | } 119 | 120 | protected function showPostInstallationMessage() 121 | { 122 | $this->info(''); 123 | $this->info('🚀 API Generator has been installed successfully!'); 124 | $this->info(''); 125 | $this->info('Next steps:'); 126 | $this->info('1. Review the configuration in config/scramble.php'); 127 | $this->info('2. Review the configuration in config/api-generator.php'); 128 | $this->info('3. Start generating your API with: php artisan api:generate {name}'); 129 | $this->info(''); 130 | $this->info('Documentation: https://github.com/Nameless0l/laravel-api-generator'); 131 | } 132 | 133 | protected function installBreeze() 134 | { 135 | $this->info('Installing Laravel Breeze API...'); 136 | 137 | // Installation de Breeze via Composer 138 | $process = new Process(['composer', 'require', 'laravel/breeze', '--dev']); 139 | $process->setTimeout(null); 140 | 141 | $process->run(function ($type, $buffer) { 142 | $this->output->write($buffer); 143 | }); 144 | 145 | if ($process->isSuccessful()) { 146 | $this->info('Laravel Breeze installed successfully!'); 147 | 148 | // Clear configuration cache 149 | $this->call('config:clear'); 150 | 151 | // Clear and rebuild cached Composer autoload files 152 | $composerDump = new Process(['composer', 'dump-autoload']); 153 | $composerDump->run(); 154 | 155 | // Try installing Breeze with error handling 156 | try { 157 | $this->call('breeze:install', [ 158 | 'stack' => 'api', 159 | '--api' => true, 160 | '--pest' => false 161 | ]); 162 | } catch (\Exception $e) { 163 | $this->warn('Could not run breeze:install automatically. Please run the following commands manually:'); 164 | $this->info('php artisan breeze:install api --api'); 165 | $this->info('php artisan migrate'); 166 | return; 167 | } 168 | 169 | // Run migrations if Breeze installation was successful 170 | $this->call('migrate'); 171 | 172 | $this->info('Laravel Breeze API configuration completed!'); 173 | } else { 174 | $this->error('Failed to install Laravel Breeze'); 175 | $this->error($process->getErrorOutput()); 176 | } 177 | } 178 | 179 | protected function installBreezeWithOptions() 180 | { 181 | $this->info('Installing Laravel Breeze...'); 182 | 183 | // Installation de Breeze via Composer 184 | $process = new Process(['composer', 'require', 'laravel/breeze', '--dev']); 185 | $process->setTimeout(null); 186 | $process->run(function ($type, $buffer) { 187 | $this->output->write($buffer); 188 | }); 189 | 190 | if ($process->isSuccessful()) { 191 | $this->info('Laravel Breeze installed successfully!'); 192 | 193 | $this->info('Laravel Breeze configuration completed!'); 194 | } else { 195 | $this->error('Failed to install Laravel Breeze'); 196 | $this->error($process->getErrorOutput()); 197 | } 198 | } 199 | protected function installLaravelUI() 200 | { 201 | $this->info('Installing Laravel UI...'); 202 | 203 | $process = new Process(['composer', 'require', 'laravel/ui', '--dev']); 204 | $process->setTimeout(null); 205 | $process->run(function ($type, $buffer) { 206 | $this->output->write($buffer); 207 | }); 208 | 209 | if ($process->isSuccessful()) { 210 | $frontend = $this->choice( 211 | 'Which frontend would you like to use?', 212 | [ 213 | 'bootstrap' => 'Bootstrap', 214 | 'vue' => 'Vue.js', 215 | 'react' => 'React' 216 | ], 217 | 'bootstrap' 218 | ); 219 | 220 | $this->call('ui', [$frontend, '--auth']); 221 | 222 | $this->info('Installing and building frontend dependencies...'); 223 | $this->runProcess(['npm', 'install']); 224 | $this->runProcess(['npm', 'run', 'dev']); 225 | 226 | if ($this->confirm('Would you like to run migrations now?', true)) { 227 | $this->call('migrate'); 228 | } 229 | 230 | $this->info('Laravel UI installed successfully!'); 231 | } 232 | } 233 | 234 | protected function runProcess(array $command) 235 | { 236 | $process = new Process($command); 237 | $process->setTimeout(null); 238 | $process->run(function ($type, $buffer) { 239 | $this->output->write($buffer); 240 | }); 241 | return $process->isSuccessful(); 242 | } 243 | 244 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel API Generator 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/nameless/laravel-api-generator.svg)](https://packagist.org/packages/nameless/laravel-api-generator) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/nameless/laravel-api-generator.svg)](https://packagist.org/packages/nameless/laravel-api-generator) 5 | [![License](https://img.shields.io/packagist/l/nameless/laravel-api-generator.svg)](https://packagist.org/packages/nameless/laravel-api-generator) 6 | 7 | **Laravel API Generator** is a professional, enterprise-grade Laravel package that generates complete API structures following best practices and clean architecture principles. 8 | 9 | --- 10 | 11 | ## 🚀 Features 12 | 13 | ### ✨ Complete API Generation 14 | - **Models** with proper relationships and fillable properties 15 | - **RESTful Controllers** with full CRUD operations 16 | - **Service Layer** implementation for business logic 17 | - **Data Transfer Objects (DTOs)** for type-safe data handling 18 | - **Form Request Validations** with intelligent rules 19 | - **API Resources** for consistent response formatting 20 | - **Policies** for authorization 21 | - **Database Factories** with realistic fake data 22 | - **Seeders** for test data generation 23 | - **Migrations** with proper foreign keys and constraints 24 | 25 | ### 🏗️ Architecture & Design Patterns 26 | - **Clean Architecture** with separated concerns 27 | - **Repository Pattern** with service layer 28 | - **Value Objects** for domain modeling 29 | - **Dependency Injection** throughout 30 | - **SOLID Principles** compliance 31 | - **Type Safety** with PHP 8.1+ features 32 | 33 | ### 🔧 Advanced Features 34 | - **JSON Schema Support** for bulk generation 35 | - **Relationship Management** (One-to-One, One-to-Many, Many-to-Many) 36 | - **Inheritance Support** for model hierarchies 37 | - **Custom Field Types** with validation rules 38 | - **Extensible Generator System** 39 | - **Professional Error Handling** 40 | - **Delete generated API structures** with a single command 41 | 42 | --- 43 | 44 | ## 📦 Installation 45 | 46 | You can install the package via Composer: 47 | 48 | ```bash 49 | composer require nameless/laravel-api-generator 50 | ``` 51 | 52 | The package automatically registers its service provider. 53 | 54 | --- 55 | 56 | ## 🎯 Quick Start 57 | 58 | ### Single Entity Generation 59 | 60 | Generate a complete API for a single entity: 61 | 62 | ```bash 63 | php artisan make:fullapi User --fields="name:string,email:string,age:integer,is_active:boolean" 64 | ``` 65 | 66 | This creates: 67 | 68 | - `app/Models/User.php` 69 | - `app/Http/Controllers/UserController.php` 70 | - `app/Http/Requests/UserRequest.php` 71 | - `app/Http/Resources/UserResource.php` 72 | - `app/Services/UserService.php` 73 | - `app/DTO/UserDTO.php` 74 | - `app/Policies/UserPolicy.php` 75 | - `database/factories/UserFactory.php` 76 | - `database/seeders/UserSeeder.php` 77 | - `database/migrations/xxxx_create_users_table.php` 78 | - API routes in `routes/api.php` 79 | 80 | ### Bulk Generation from JSON 81 | 82 | Create a `class_data.json` file in your project root: 83 | 84 | ```json 85 | [ 86 | { 87 | "name": "User", 88 | "attributes": [ 89 | {"name": "name", "_type": "string"}, 90 | {"name": "email", "_type": "string"}, 91 | {"name": "email_verified_at", "_type": "timestamp"} 92 | ], 93 | "oneToManyRelationships": [ 94 | {"role": "posts", "comodel": "Post"} 95 | ] 96 | }, 97 | { 98 | "name": "Post", 99 | "attributes": [ 100 | {"name": "title", "_type": "string"}, 101 | {"name": "content", "_type": "text"}, 102 | {"name": "published_at", "_type": "timestamp"} 103 | ], 104 | "manyToOneRelationships": [ 105 | {"role": "user", "comodel": "User"} 106 | ] 107 | } 108 | ] 109 | ``` 110 | 111 | Then run: 112 | 113 | ```bash 114 | php artisan make:fullapi 115 | ``` 116 | 117 | ### Delete Generated API 118 | 119 | Remove all generated files for an entity: 120 | 121 | --- 122 | 123 | ## 🏗️ Architecture Overview 124 | 125 | ### Service Layer Pattern 126 | 127 | The generated code follows the Service Layer pattern for better organization: 128 | 129 | ```php 130 | class UserController extends Controller 131 | { 132 | public function __construct( 133 | private readonly UserService $service 134 | ) {} 135 | 136 | public function store(UserRequest $request) 137 | { 138 | $dto = UserDTO::fromRequest($request); 139 | $user = $this->service->create($dto); 140 | return new UserResource($user); 141 | } 142 | } 143 | ``` 144 | 145 | ### Data Transfer Objects 146 | 147 | Type-safe data handling with DTOs: 148 | 149 | ```php 150 | readonly class UserDTO 151 | { 152 | public function __construct( 153 | public ?string $name, 154 | public ?string $email, 155 | public ?int $age, 156 | public ?bool $is_active, 157 | ) {} 158 | 159 | public static function fromRequest(Request $request): self 160 | { 161 | return new self( 162 | name: $request->get('name'), 163 | email: $request->get('email'), 164 | age: $request->get('age'), 165 | is_active: $request->get('is_active'), 166 | ); 167 | } 168 | } 169 | ``` 170 | 171 | ## 🛠️ Advanced Usage 172 | 173 | ### Custom Field Types 174 | 175 | Supported field types: 176 | 177 | - `string` - VARCHAR(255) 178 | - `text` - TEXT 179 | - `integer`/`int` - INTEGER 180 | - `bigint` - BIG INTEGER 181 | - `boolean`/`bool` - BOOLEAN 182 | - `float`/`decimal` - DECIMAL 183 | - `json` - JSON 184 | - `date` - DATE 185 | - `datetime` - DATETIME 186 | - `timestamp` - TIMESTAMP 187 | - `uuid` - UUID 188 | 189 | ### Relationship Types 190 | 191 | The generator supports all Laravel relationship types: 192 | 193 | - **One-to-One**: `oneToOneRelationships` 194 | - **One-to-Many**: `oneToManyRelationships` 195 | - **Many-to-One**: `manyToOneRelationships` 196 | - **Many-to-Many**: `manyToManyRelationships` 197 | 198 | ### Model Inheritance 199 | 200 | Support for model inheritance: 201 | 202 | ```json 203 | { 204 | "name": "AdminUser", 205 | "parent": "User", 206 | "attributes": [ 207 | {"name": "permissions", "_type": "json"} 208 | ] 209 | } 210 | ``` 211 | 212 | ### Generated File Structure 213 | 214 | This command generates: 215 | 216 | - **Models** (`App\Models`) 217 | - **Controllers** (`App\Http\Controllers`) 218 | - **Services** (`App\Services`) 219 | - **DTOs** (`App\DTO`) 220 | - **Policies** (`App\Policies`) 221 | - **Requests** (`App\Http\Requests`) 222 | - **Resources** (`App\Http\Resources`) 223 | - **Factories** (`Database\Factories`) 224 | - **Migrations** (`Database\Migrations`) 225 | - **Seeders** (`Database\Seeders`) 226 | 227 | ### Delete API Structure 228 | 229 | To remove the generated API structure, you can use: 230 | 231 | ```bash 232 | php artisan delete:fullapi 233 | ``` 234 | 235 | This will remove all the generated files from the API structure. 236 | 237 | To delete a specific model's API structure, use: 238 | 239 | ```bash 240 | php artisan delete:fullapi ModelName 241 | ``` 242 | 243 | For example: 244 | 245 | ```bash 246 | php artisan delete:fullapi Post 247 | ``` 248 | 249 | This will delete all the generated files related to the Post model, including controllers, services, DTOs, policies, resources, factories, seeders, and migrations. 250 | 251 | --- 252 | 253 | ## 🔧 Configuration 254 | 255 | ### Custom Stubs 256 | 257 | You can customize the generated code by publishing and modifying the stubs: 258 | 259 | ```bash 260 | php artisan vendor:publish --tag=laravel-api-generator-stubs 261 | ``` 262 | 263 | ### Service Registration 264 | 265 | The package automatically registers all generators and services through dependency injection. 266 | 267 | --- 268 | 269 | ## 🧪 Testing 270 | 271 | ```bash 272 | composer test 273 | ``` 274 | 275 | Run static analysis: 276 | 277 | ```bash 278 | composer analyse 279 | ``` 280 | 281 | Format code: 282 | 283 | ```bash 284 | composer format 285 | ``` 286 | 287 | --- 288 | 289 | ## 📖 API Documentation 290 | 291 | The package integrates with [Scramble](https://github.com/dedoc/scramble) for automatic API documentation generation. 292 | 293 | After generating your APIs, visit `/docs/api` to see the generated documentation. 294 | 295 | --- 296 | 297 | ## Generated Structure 298 | 299 | ### Modern Controller Example 300 | 301 | ```php 302 | service = $service; 321 | } 322 | 323 | public function index() 324 | { 325 | $posts = $this->service->getAll(); 326 | return PostResource::collection($posts); 327 | } 328 | 329 | public function store(PostRequest $request) 330 | { 331 | $dto = PostDTO::fromRequest($request); 332 | $post = $this->service->create($dto); 333 | return new PostResource($post); 334 | } 335 | 336 | public function show(Post $post) 337 | { 338 | return new PostResource($post); 339 | } 340 | 341 | public function update(PostRequest $request, Post $post) 342 | { 343 | $dto = PostDTO::fromRequest($request); 344 | $updatedPost = $this->service->update($post, $dto); 345 | return new PostResource($updatedPost); 346 | } 347 | 348 | public function destroy(Post $post) 349 | { 350 | $this->service->delete($post); 351 | return response(null, 204); 352 | } 353 | } 354 | ``` 355 | 356 | ### Service 357 | 358 | ```php 359 | update((array) $dto); 386 | return $post; 387 | } 388 | 389 | public function delete(Post $post) 390 | { 391 | return $post->delete(); 392 | } 393 | } 394 | ``` 395 | 396 | ### DTO 397 | 398 | ```php 399 | get('title'), 417 | content: $request->get('content'), 418 | published: $request->get('published'), 419 | ); 420 | } 421 | } 422 | ``` 423 | 424 | ### Model 425 | 426 | ```php 427 | */ 439 | use HasFactory; 440 | } 441 | ``` 442 | 443 | ### Resource 444 | 445 | ```php 446 | $this->title, 459 | 'content' => $this->content, 460 | 'published' => $this->published, 461 | ]; 462 | } 463 | } 464 | ``` 465 | 466 | ### Request 467 | 468 | ```php 469 | 'string|max:255', 486 | 'content' => 'string', 487 | 'published' => 'boolean', 488 | ]; 489 | } 490 | } 491 | ``` 492 | 493 | ### Factory 494 | 495 | ```php 496 | fake()->word(), 508 | 'content' => fake()->sentence(), 509 | 'published' => fake()->boolean(), 510 | ]; 511 | } 512 | } 513 | ``` 514 | 515 | ### Seeder 516 | 517 | ```php 518 | create(); 529 | } 530 | } 531 | ``` 532 | 533 | ### Policy 534 | 535 | ```php 536 | id(); 601 | $table->string('title')->nullable(); 602 | $table->text('content')->nullable(); 603 | $table->boolean('published')->nullable(); 604 | $table->timestamps(); 605 | }); 606 | } 607 | 608 | public function down(): void 609 | { 610 | Schema::dropIfExists('posts'); 611 | } 612 | }; 613 | ``` 614 | 615 | --- 616 | 617 | ## 🤝 Contributing 618 | 619 | Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. 620 | 621 | 1. Fork the repository 622 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 623 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 624 | 4. Push to the branch (`git push origin feature/amazing-feature`) 625 | 5. Open a Pull Request 626 | 627 | --- 628 | 629 | ## 📚 Testing 630 | 631 | Run the test suite: 632 | 633 | ```bash 634 | composer test 635 | ``` 636 | 637 | Run static analysis: 638 | 639 | ```bash 640 | composer analyse 641 | ``` 642 | 643 | Format code: 644 | 645 | ```bash 646 | composer format 647 | ``` 648 | 649 | --- 650 | 651 | ## 🚀 Local Development 652 | 653 | 1. Clone this repository: 654 | 655 | ```bash 656 | git clone https://github.com/Nameless0l/laravel-api-generator.git 657 | ``` 658 | 659 | 2. Install dependencies: 660 | 661 | ```bash 662 | composer install 663 | ``` 664 | 665 | 3. Run tests: 666 | 667 | ```bash 668 | ./vendor/bin/phpunit 669 | ``` 670 | 671 | ### Testing in a Laravel Project 672 | 673 | 1. In your Laravel project's `composer.json`, add: 674 | 675 | ```json 676 | { 677 | "repositories": [ 678 | { 679 | "type": "path", 680 | "url": "../laravel-api-generator", 681 | "options": { 682 | "symlink": true 683 | } 684 | } 685 | ], 686 | "require": { 687 | "nameless/laravel-api-generator": "@dev" 688 | } 689 | } 690 | ``` 691 | 692 | 2. Run: 693 | 694 | ```bash 695 | composer update 696 | ``` 697 | 698 | --- 699 | 700 | ## 🔒 Security 701 | 702 | If you discover any security-related issues, please email [loicmbassi5@gmail.com](mailto:loicmbassi5@gmail.com) instead of using the issue tracker. 703 | 704 | --- 705 | 706 | ## 🏆 Credits 707 | 708 | - **Author**: [Mbassi Loïc Aron](https://github.com/Nameless0l) 709 | - **Email**: loicmbassi5@gmail.com 710 | 711 | --- 712 | 713 | ## 📚 Changelog 714 | 715 | Please see [CHANGELOG.md](CHANGELOG.md) for more information on what has changed recently. 716 | 717 | --- 718 | 719 | ## 💡 Why Choose Laravel API Generator? 720 | 721 | ✅ **Professional Architecture** - Built with enterprise-grade patterns 722 | ✅ **Type Safety** - Full PHP 8.1+ type declarations 723 | ✅ **Clean Code** - SOLID principles and clean architecture 724 | ✅ **Extensible** - Easy to extend with custom generators 725 | ✅ **Well Tested** - Comprehensive test suite 726 | ✅ **Documentation** - Complete API documentation generation 727 | ✅ **Best Practices** - Follows Laravel and PHP best practices 728 | 729 | Transform your Laravel development workflow with professional API generation! 730 | 731 | --- 732 | 733 | ## 📄 License 734 | 735 | This package is open-source and distributed under the MIT License. See the [LICENSE](LICENSE.md) file for more details. -------------------------------------------------------------------------------- /src/Console/Commands/MakeApi.php: -------------------------------------------------------------------------------- 1 | argument('name'); 22 | 23 | if (empty($this->argument('name'))) { 24 | $this->callDiagramsMethods(); 25 | return; 26 | } 27 | $fields = $this->option('fields'); 28 | $this->nameLower = strtolower($name); 29 | if (!$fields) { 30 | $this->error('Vous devez spécifier des champs avec l\'option --fields="champ1:type1,champ2:type2"'); 31 | return; 32 | } 33 | $this->callDefaulttMethods($name, $fields); 34 | } 35 | 36 | private function callDefaulttMethods($name, $fields, ?array $classData = null) 37 | { 38 | 39 | $fieldsArray = $this->parseFields($fields); 40 | $pluralName = Str::plural(Str::lower($name)); 41 | $this->nameLower = Str::lower($name); 42 | 43 | info("Création des fichiers pour l'API: {$name}"); 44 | $route = "Route::apiResource('{$pluralName}', App\Http\Controllers\\{$name}Controller::class);"; 45 | $apiFilePath = base_path('routes/api.php'); 46 | $phpHeader = "updateModel($name, $fieldsArray, $classData); 63 | $this->info("Modèle mis à jour avec les fillable et les relations."); 64 | 65 | // Ajouter les champs et les clés étrangères dans la migration 66 | $this->updateMigration($name, $fieldsArray, $classData); 67 | 68 | // Créer les migrations pour les tables pivots (ManyToMany) 69 | $this->createPivotMigrations($name, $classData); 70 | 71 | // Créer le service 72 | $this->createService($name, $this->nameLower); 73 | $this->info("Service créé."); 74 | 75 | // Créer la policy 76 | Artisan::call("make:policy {$name}Policy --model={$name}"); 77 | $this->updatePolicy($name); 78 | $this->info("Policy créée et configurée."); 79 | 80 | // Créer le contrôleur avec CRUD 81 | Artisan::call("make:controller {$name}Controller"); 82 | $this->info("Contrôleur API créé."); 83 | 84 | // Créer la resource 85 | Artisan::call("make:resource {$name}Resource"); 86 | $this->info("Resource créée."); 87 | 88 | $this->updateResource($name, $fieldsArray); 89 | // Créer la requête (FormRequest) 90 | Artisan::call("make:request {$name}Request"); 91 | $this->updateRequest($name, $fieldsArray); 92 | $this->info("Requête créée."); 93 | 94 | // Créer le seeder 95 | Artisan::call("make:seeder {$name}Seeder"); 96 | $this->updateSeeder($name, $fieldsArray); 97 | $this->info("Seeder créé."); 98 | 99 | // Créer un DTO 100 | $this->createDTO($name, $fieldsArray); 101 | $this->info("DTO créé."); 102 | 103 | // Ajouter les méthodes CRUD au contrôleur 104 | $this->addCrudToController($name, $this->nameLower, $pluralName); 105 | 106 | // Mettre à jour AuthServiceProvider 107 | $this->updateAuthServiceProvider($name); 108 | 109 | $this->info("API complète créée pour : {$name} !"); 110 | $this->updateFactory($name, $fieldsArray); 111 | } 112 | private function callDiagramsMethods() 113 | { 114 | $this->warn("Aucun nom fourni. Utilisation par défaut du fichier JSON..."); 115 | $jsonFilePath = base_path('class_data.json'); 116 | if (!file_exists($jsonFilePath)) { 117 | $this->error("Le fichier class_data.json est introuvable."); 118 | return; 119 | } 120 | 121 | $this->info("Lecture du fichier JSON..."); 122 | $jsonData = file_get_contents($jsonFilePath); 123 | // Le JSON peut être un tableau d'objets ou un seul objet avec une clé "data" 124 | $rawClasses = json_decode($jsonData, true); 125 | 126 | if (json_last_error() !== JSON_ERROR_NONE) { 127 | $this->error("Erreur de décodage JSON : " . json_last_error_msg()); 128 | return; 129 | } 130 | 131 | // S'assurer que nous travaillons avec un tableau 132 | if (isset($rawClasses['data']) && is_array($rawClasses['data'])) { 133 | $this->classes = [$rawClasses]; 134 | } else { 135 | $this->classes = $rawClasses; 136 | } 137 | 138 | $this->info("Extraction des données JSON..."); 139 | $this->jsonExtractionToArray(); 140 | 141 | $this->info("Génération des API avec Artisan..."); 142 | $this->runFullApiWithDiagram(); 143 | return; 144 | } 145 | 146 | /** 147 | * Extraire et formater les données JSON dans un tableau compatible. 148 | */ 149 | public function jsonExtractionToArray() 150 | { 151 | $this->classes = array_map(function ($classData) { 152 | // Gérer les deux formats JSON (avec ou sans l'enveloppe 'data') 153 | $class = isset($classData['data']) ? $classData['data'] : $classData; 154 | 155 | return [ 156 | 'name' => ucfirst($class['name']), 157 | 'parent' => isset($class['parent']) ? ucfirst($class['parent']) : null, 158 | 'attributes' => array_map(function ($attribute) { 159 | return [ 160 | 'name' => $attribute['name'], 161 | '_type' => match (strtolower($attribute['_type'])) { 162 | 'integer', 'long', 'int' => 'int', 163 | 'bigint' => 'int', 164 | 'str', 'string', 'text', 'java.time.offsetdatetime', 'java.time.localdate' => 'string', 165 | 'boolean' => 'bool', 166 | 'java.math.bigdecimal' => 'float', 167 | 'java.util.map' => 'json', 168 | default => strtolower($attribute['_type']), 169 | }, 170 | ]; 171 | }, $class['attributes'] ?? []), 172 | 'oneToOneRelationships' => $class['oneToOneRelationships'] ?? [], 173 | 'manyToOneRelationships' => $class['manyToOneRelationships'] ?? [], 174 | 'oneToManyRelationships' => $class['oneToManyRelationships'] ?? [], 175 | 'manyToManyRelationships' => $class['manyToManyRelationships'] ?? [], 176 | ]; 177 | }, $this->classes); 178 | } 179 | 180 | /** 181 | * Parcourir les classes et exécuter les commandes Artisan pour générer les API. 182 | */ 183 | public function runFullApiWithDiagram() 184 | { 185 | foreach ($this->classes as $class) { 186 | $className = ucfirst($class['name']); 187 | $fields = []; 188 | 189 | foreach ($class['attributes'] as $attribute) { 190 | $fields[] = "{$attribute['name']}:{$attribute['_type']}"; 191 | } 192 | 193 | $fieldsString = implode(',', $fields); 194 | 195 | try { 196 | // Passer le tableau complet de la classe, qui inclut maintenant les relations 197 | $this->callDefaulttMethods($className, $fieldsString, $class); 198 | $this->info("API pour la classe $className générée avec succès !"); 199 | } catch (\Exception $e) { 200 | $this->error("Erreur lors de la génération de l'API pour la classe $className : " . $e->getMessage()); 201 | } 202 | } 203 | } 204 | 205 | private function updateFactory($name, $fieldsArray) 206 | { 207 | $factoryPath = database_path("factories/{$name}Factory.php"); 208 | 209 | if (!file_exists($factoryPath)) { 210 | $this->error("Le fichier Factory pour {$name} n'existe pas."); 211 | return; 212 | } 213 | 214 | $factoryFile = file_get_contents($factoryPath); 215 | 216 | $fields = []; 217 | foreach ($fieldsArray as $field => $type) { 218 | $value = match ($type) { 219 | 'string' => "fake()->word()", 220 | 'integer', 'int' => "fake()->randomNumber()", 221 | 'boolean', 'bool' => "fake()->boolean()", 222 | 'text' => "fake()->sentence()", 223 | 'uuid', 'UUID' => "fake()->uuid()", 224 | 'float' => "fake()->randomFloat(2, 1, 1000)", 225 | 'json' => "json_encode(['key' => 'value'])", 226 | 'date', 'datetime', 'timestamp', 'time' => "fake()->dateTime()", 227 | default => "fake()->word()" 228 | }; 229 | $fields[] = "'{$field}' => {$value}"; 230 | } 231 | 232 | $fieldsContent = implode(",\n ", $fields); 233 | 234 | $factoryFile = preg_replace( 235 | "/return \[.*?\];/s", 236 | "return [\n {$fieldsContent}\n ];", 237 | $factoryFile 238 | ); 239 | 240 | file_put_contents($factoryPath, $factoryFile); 241 | 242 | $this->info("Factory mis à jour : {$factoryPath}"); 243 | } 244 | 245 | private function parseFields($fields) 246 | { 247 | $fieldsArray = []; 248 | foreach (explode(',', $fields) as $field) { 249 | $parts = explode(':', $field); 250 | if(count($parts) == 2){ 251 | $fieldsArray[$parts[0]] = strtolower($parts[1]); 252 | } 253 | } 254 | return $fieldsArray; 255 | } 256 | 257 | private function updateMigration($name, $fieldsArray, ?array $classData = null) 258 | { 259 | $pluralName = Str::plural(Str::snake($name)); 260 | $migrations = glob(database_path("migrations/*_create_{$pluralName}_table.php")); 261 | 262 | if (empty($migrations)) { 263 | $this->error("Impossible de trouver une migration pour {$name}."); 264 | return; 265 | } 266 | 267 | $migrationPath = $migrations[0]; 268 | $migrationFile = file_get_contents($migrationPath); 269 | 270 | $fieldLines = ''; 271 | foreach ($fieldsArray as $field => $type) { 272 | $type = match ($type) { 273 | 'string' => 'string', 274 | 'integer', 'int' => 'integer', 275 | 'boolean', 'bool' => 'boolean', 276 | 'text' => 'text', 277 | 'float' => 'decimal', 278 | 'json' => 'json', 279 | 'date', 'datetime', 'timestamp', 'time' => 'timestamp', 280 | 'uuid', 'UUID' => 'uuid', 281 | default => 'string' 282 | }; 283 | if($type === 'decimal'){ 284 | $fieldLines .= "\$table->{$type}('{$field}', 8, 2)->nullable();\n "; 285 | } else { 286 | $fieldLines .= "\$table->{$type}('{$field}')->nullable();\n "; 287 | } 288 | } 289 | 290 | $foreignKeyLines = ''; 291 | if ($classData) { 292 | // Les relations Many-to-One et One-to-One (côté "enfant") impliquent une clé étrangère 293 | $relations = array_merge($classData['manyToOneRelationships'], $classData['oneToOneRelationships']); 294 | foreach ($relations as $relation) { 295 | $foreignKeyColumn = Str::snake($relation['role']) . '_id'; 296 | // Assumer que le nom de la table est le pluriel du nom du modèle lié 297 | $relatedTable = Str::plural(Str::snake($relation['comodel'])); 298 | $foreignKeyLines .= "\$table->foreignId('{$foreignKeyColumn}')->nullable()->constrained('{$relatedTable}')->onDelete('set null');\n "; 299 | } 300 | } 301 | 302 | // Injecter les champs dans la migration 303 | $pattern = '/\$table->id\(\);(.*?)\$table->timestamps\(\);/s'; 304 | $replacement = "\$table->id();\n {$fieldLines}{$foreignKeyLines}\$table->timestamps();"; 305 | 306 | if(preg_match($pattern, $migrationFile)) { 307 | $migrationFile = preg_replace($pattern, $replacement, $migrationFile); 308 | } else { 309 | $pattern = '/Schema::create\([\'"]' . preg_quote($pluralName, '/') . '[\'"],\s*function\s*\(Blueprint\s*\$table\)\s*\{.*?id\(\);/s'; 310 | $replacement = "Schema::create('{$pluralName}', function (Blueprint \$table) {\n \$table->id();\n {$fieldLines}{$foreignKeyLines}"; 311 | $migrationFile = preg_replace($pattern, $replacement, $migrationFile); 312 | } 313 | 314 | file_put_contents($migrationPath, $migrationFile); 315 | $this->info("Migration mise à jour : {$migrationPath}"); 316 | } 317 | 318 | private function createPivotMigrations($name, ?array $classData = null) 319 | { 320 | if (!$classData || empty($classData['manyToManyRelationships'])) { 321 | return; 322 | } 323 | 324 | $modelNameSingular = Str::snake($name); 325 | 326 | foreach ($classData['manyToManyRelationships'] as $relation) { 327 | $relatedModel = ucfirst($relation['comodel']); 328 | $relatedModelSingular = Str::snake($relatedModel); 329 | 330 | $tableParts = [$modelNameSingular, $relatedModelSingular]; 331 | sort($tableParts); 332 | $pivotTableName = implode('_', $tableParts); 333 | 334 | $existingMigrations = File::glob(database_path("migrations/*_create_{$pivotTableName}_table.php")); 335 | if (!empty($existingMigrations)) { 336 | $this->warn("La migration de la table pivot '{$pivotTableName}' existe déjà. Ignorée."); 337 | continue; 338 | } 339 | 340 | $migrationName = "create_{$pivotTableName}_table"; 341 | Artisan::call('make:migration', ['name' => $migrationName]); 342 | $this->info("Migration de la table pivot créée : {$migrationName}"); 343 | 344 | $migrationFile = last(File::glob(database_path("migrations/*_{$migrationName}.php"))); 345 | 346 | if ($migrationFile) { 347 | $upMethodContent = " 348 | Schema::create('{$pivotTableName}', function (Blueprint \$table) { 349 | \$table->primary(['{$tableParts[0]}_id', '{$tableParts[1]}_id']); 350 | \$table->foreignId('{$tableParts[0]}_id')->constrained('" . Str::plural($tableParts[0]) . "')->onDelete('cascade'); 351 | \$table->foreignId('{$tableParts[1]}_id')->constrained('" . Str::plural($tableParts[1]) . "')->onDelete('cascade'); 352 | \$table->timestamps(); 353 | });"; 354 | 355 | $migrationContent = file_get_contents($migrationFile); 356 | $migrationContent = preg_replace( 357 | '/(public function up\(\): void\s*{)/s', 358 | "$1" . $upMethodContent, 359 | $migrationContent 360 | ); 361 | 362 | file_put_contents($migrationFile, $migrationContent); 363 | $this->info("Migration de la table pivot mise à jour : {$migrationFile}"); 364 | } 365 | } 366 | } 367 | 368 | 369 | private function updateRequest($name, $fieldsArray) 370 | { 371 | $requestPath = app_path("Http/Requests/{$name}Request.php"); 372 | $requestFile = file_get_contents($requestPath); 373 | 374 | // Ajouter la méthode authorize qui retourne true 375 | $requestFile = str_replace( 376 | "public function authorize(): bool\n {\n return false;\n }", 377 | "public function authorize(): bool\n {\n return true;\n }", 378 | $requestFile 379 | ); 380 | 381 | $rules = ''; 382 | foreach ($fieldsArray as $field => $type) { 383 | $rule = match ($type) { 384 | 'string' => 'string|max:255', 385 | 'integer', 'int' => 'integer', 386 | 'boolean', 'bool' => 'boolean', 387 | 'text' => 'string', 388 | 'uuid', 'UUID' => 'uuid', 389 | 'float' => 'numeric', 390 | 'json' => 'json', 391 | 'date', 'datetime', 'timestamp' => 'date', 392 | default => 'sometimes|string' 393 | }; 394 | // CORRECTION : Pas de guillemets dans le match, on les ajoute ici 395 | $rules .= "'{$field}' => 'sometimes|{$rule}',\n "; 396 | } 397 | 398 | $requestFile = preg_replace( 399 | "/public function rules\(\).*?\{.*?\n.*?\}/s", 400 | "public function rules(): array\n {\n return [\n {$rules}\n ];\n }", 401 | $requestFile 402 | ); 403 | 404 | file_put_contents($requestPath, $requestFile); 405 | } 406 | 407 | private function addCrudToController($name, $nameLower, $pluralName) 408 | { 409 | $controllerPath = app_path("Http/Controllers/{$name}Controller.php"); 410 | 411 | if (!file_exists($controllerPath)) { 412 | $this->error("Le contrôleur {$name}Controller n'existe pas."); 413 | return; 414 | } 415 | 416 | // Lire le contenu original 417 | $content = file_get_contents($controllerPath); 418 | 419 | // Ajouter l'import de Controller si pas déjà présent 420 | if (strpos($content, 'use App\Http\Controllers\Controller;') === false) { 421 | $content = str_replace( 422 | 'namespace App\Http\Controllers;', 423 | "namespace App\Http\Controllers;\n\nuse App\Http\Controllers\Controller;", 424 | $content 425 | ); 426 | } 427 | 428 | // Remplacer la déclaration de classe 429 | $content = preg_replace( 430 | '/class\s+' . $name . 'Controller\s*{/', 431 | 'class ' . $name . 'Controller extends Controller {', 432 | $content 433 | ); 434 | 435 | $methods = <<service = \$service; 442 | } 443 | 444 | public function index() 445 | { 446 | \${$pluralName} = \$this->service->getAll(); 447 | return {$name}Resource::collection(\${$pluralName}); 448 | } 449 | 450 | public function store({$name}Request \$request) 451 | { 452 | \$dto = {$name}DTO::fromRequest(\$request); 453 | \${$nameLower} = \$this->service->create(\$dto); 454 | return new {$name}Resource(\${$nameLower}); 455 | } 456 | 457 | public function show({$name} \${$nameLower}) 458 | { 459 | return new {$name}Resource(\${$nameLower}); 460 | } 461 | 462 | public function update({$name}Request \$request, {$name} \${$nameLower}) 463 | { 464 | \$dto = {$name}DTO::fromRequest(\$request); 465 | \$updated{$name} = \$this->service->update(\${$nameLower}, \$dto); 466 | return new {$name}Resource(\$updated{$name}); 467 | } 468 | 469 | public function destroy({$name} \${$nameLower}) 470 | { 471 | \$this->service->delete(\${$nameLower}); 472 | return response(null, 204); 473 | } 474 | 475 | EOD; 476 | 477 | 478 | // Ajouter les imports nécessaires 479 | $content = str_replace( 480 | 'use Illuminate\Http\Request;', 481 | "use App\\Http\\Requests\\{$name}Request;\nuse App\\Models\\{$name};\nuse App\\Http\\Resources\\{$name}Resource;\nuse App\\Services\\{$name}Service;\nuse App\\DTO\\{$name}DTO;\nuse Illuminate\\Http\\Response;", 482 | $content 483 | ); 484 | 485 | // Vider la classe avant d'ajouter les nouvelles méthodes 486 | $pattern = '/class ' . $name . 'Controller extends Controller\s*{[^}]*}/'; 487 | $replacement = 'class ' . $name . 'Controller extends Controller {' . $methods . '}'; 488 | $content = preg_replace($pattern, $replacement, $content, 1); 489 | 490 | 491 | file_put_contents($controllerPath, $content); 492 | 493 | $this->info("CRUD ajouté au contrôleur : {$controllerPath}"); 494 | } 495 | 496 | private function updateSeeder($name, $fieldsArray) 497 | { 498 | $seederPath = database_path("seeders/{$name}Seeder.php"); 499 | $seederFile = file_get_contents($seederPath); 500 | 501 | $factoryFields = []; 502 | foreach ($fieldsArray as $field => $type) { 503 | $value = match ($type) { 504 | 'string' => "fake()->word()", 505 | 'integer' => "fake()->randomNumber()", 506 | 'boolean' => "fake()->boolean()", 507 | 'uuid','UUID'=>"fake()->uuid()", 508 | 'bigint' => "fake()->randomNumber()", 509 | 'date', 'datetime', 'timestamp', 'time' => "fake()->dateTime()", 510 | 'text' => "fake()->paragraph()", 511 | default => "fake()->word()" 512 | }; 513 | $factoryFields[] = "'{$field}' => {$value}"; 514 | } 515 | 516 | $factoryContent = implode(",\n ", $factoryFields); 517 | $seederFile = preg_replace( 518 | "/public function run\(\): void\s*{[^}]*}/s", 519 | "public function run(): void\n {\n \\App\\Models\\{$name}::factory(10)->create();\n }", 520 | $seederFile 521 | ); 522 | 523 | file_put_contents($seederPath, $seederFile); 524 | } 525 | 526 | private function createDTO($name, $fieldsArray) 527 | { 528 | $dtoPath = app_path("DTO/{$name}DTO.php"); 529 | if (!file_exists(app_path('DTO'))) { 530 | mkdir(app_path('DTO'), 0755, true); 531 | } 532 | 533 | $attributes = ''; 534 | 535 | foreach ($fieldsArray as $field => $type) { 536 | if ($type == 'text') { 537 | $type = 'string'; 538 | } 539 | if ($type == 'boolean') { 540 | $type = 'bool'; 541 | } 542 | if ($type == 'integer' || $type == 'bigint') { 543 | $type = 'int'; 544 | } 545 | if ($type == 'float'){ 546 | $type = 'float'; 547 | } 548 | if($type == 'json'){ 549 | $type = 'array'; 550 | } 551 | if ($type == 'date' || $type == 'datetime' || $type == 'timestamp' || $type == 'time') { 552 | $type = '\DateTimeInterface'; 553 | } 554 | if($type == 'uuid' || $type == 'UUID'){ 555 | $type = 'string'; 556 | } 557 | 558 | $attributes .= "public ?{$type} \${$field}, 559 | "; 560 | } 561 | $atributsFromRequest = ''; 562 | foreach ($fieldsArray as $field => $type) { 563 | if ($type == 'date' || $type == 'datetime' || $type == 'timestamp' || $type == 'time') { 564 | $atributsFromRequest .= "$field: \$request->filled('{$field}') ? \Carbon\Carbon::parse(\$request->get('{$field}')) : null,\n "; 565 | } else { 566 | $atributsFromRequest .= "$field: \$request->get('{$field}'),\n "; 567 | } 568 | } 569 | $attributes = rtrim($attributes, ", 570 | "); 571 | $atributsFromRequest = rtrim($atributsFromRequest, ", 572 | "); 573 | 574 | $content = << \$value !== null); 626 | return {$name}::create(\$data); 627 | } 628 | 629 | public function find(\$id) 630 | { 631 | return {$name}::findOrFail(\$id); 632 | } 633 | 634 | public function update({$name} \${$nameLower}, {$name}DTO \$dto) 635 | { 636 | \$data = array_filter((array) \$dto, fn(\$value) => \$value !== null); 637 | \${$nameLower}->update(\$data); 638 | return \${$nameLower}; 639 | } 640 | 641 | public function delete({$name} \${$nameLower}) 642 | { 643 | return \${$nameLower}->delete(); 644 | } 645 | } 646 | EOD; 647 | 648 | file_put_contents($servicePath, $content); 649 | } 650 | 651 | private function updatePolicy($name) 652 | { 653 | $policyPath = app_path("Policies/{$name}Policy.php"); 654 | 655 | if (!file_exists($policyPath)) { 656 | $this->error("Le fichier Policy pour {$name} n'existe pas."); 657 | return; 658 | } 659 | 660 | $policyContent = file_get_contents($policyPath); 661 | 662 | // Rendre toutes les méthodes true 663 | $policyContent = preg_replace('/(return\s+)(false|Response::deny\(\));/m', '$1true;', $policyContent); 664 | 665 | 666 | file_put_contents($policyPath, $policyContent); 667 | } 668 | 669 | private function updateAuthServiceProvider($name) 670 | { 671 | $providerPath = app_path('Providers/AuthServiceProvider.php'); 672 | 673 | if (!file_exists($providerPath)) { 674 | $this->createAuthServiceProvider(); 675 | } 676 | 677 | $content = file_get_contents($providerPath); 678 | 679 | // Éviter les doublons d'imports et de mappings 680 | $modelImport = "use App\\Models\\{$name};"; 681 | $policyImport = "use App\\Policies\\{$name}Policy;"; 682 | $mapping = " {$name}::class => {$name}Policy::class,"; 683 | 684 | if (strpos($content, $modelImport) === false) { 685 | $content = preg_replace('/(namespace App\\\\Providers;)/', "$1\n{$modelImport}", $content, 1); 686 | } 687 | if (strpos($content, $policyImport) === false) { 688 | $content = preg_replace('/(namespace App\\\\Providers;)/', "$1\n{$policyImport}", $content, 1); 689 | } 690 | 691 | if (strpos($content, $mapping) === false) { 692 | $content = preg_replace( 693 | '/protected \$policies = \[/', 694 | "protected \$policies = [\n{$mapping}", 695 | $content, 696 | 1 697 | ); 698 | } 699 | 700 | file_put_contents($providerPath, $content); 701 | } 702 | 703 | private function createAuthServiceProvider() 704 | { 705 | $providerPath = app_path('Providers/AuthServiceProvider.php'); 706 | $content = << 720 | */ 721 | protected \$policies = []; 722 | 723 | /** 724 | * Register any authentication / authorization services. 725 | */ 726 | public function boot(): void 727 | { 728 | \$this->registerPolicies(); 729 | } 730 | } 731 | EOD; 732 | 733 | if (!file_exists(app_path('Providers'))) { 734 | mkdir(app_path('Providers'), 0755, true); 735 | } 736 | 737 | file_put_contents($providerPath, $content); 738 | $this->info('AuthServiceProvider créé.'); 739 | } 740 | 741 | private function updateModel($name, $fieldsArray, ?array $classData = null) 742 | { 743 | $modelPath = app_path("Models/{$name}.php"); 744 | 745 | if (!file_exists($modelPath)) { 746 | $this->error("Le modèle {$name} n'existe pas."); 747 | return; 748 | } 749 | 750 | // Préparer les données pour le template 751 | $fillableFields = array_keys($fieldsArray); 752 | 753 | // Ajouter les clés étrangères 754 | if ($classData) { 755 | $relations = array_merge( 756 | $classData['manyToOneRelationships'] ?? [], 757 | $classData['oneToOneRelationships'] ?? [] 758 | ); 759 | foreach ($relations as $relation) { 760 | $fillableFields[] = Str::snake($relation['role']) . '_id'; 761 | } 762 | } 763 | 764 | $fillableString = "'" . implode("', '", $fillableFields) . "'"; 765 | $fillableProperty = "protected \$fillable = [{$fillableString}];"; 766 | 767 | // Générer les relations 768 | $relationshipMethods = ''; 769 | $imports = ''; 770 | $parentClass = 'Model'; 771 | 772 | if ($classData) { 773 | // Ne pas traiter l'héritage pour l'instant car dans ce JSON c'est des relations 774 | // L'héritage sera traité différemment plus tard 775 | 776 | $relations = [ 777 | 'oneToOne' => $classData['oneToOneRelationships'] ?? [], 778 | 'oneToMany' => $classData['oneToManyRelationships'] ?? [], 779 | 'manyToOne' => $classData['manyToOneRelationships'] ?? [], 780 | 'manyToMany' => $classData['manyToManyRelationships'] ?? [] 781 | ]; 782 | 783 | $relatedModels = []; 784 | 785 | foreach ($relations['oneToOne'] as $rel) { 786 | $methodName = Str::camel($rel['role']); 787 | $relatedModel = ucfirst($rel['comodel']); 788 | $relatedModels[$relatedModel] = true; 789 | $relationshipMethods .= "\n public function {$methodName}()\n {\n return \$this->hasOne({$relatedModel}::class);\n }\n"; 790 | } 791 | 792 | foreach ($relations['oneToMany'] as $rel) { 793 | $methodName = Str::camel($rel['role']); 794 | $relatedModel = ucfirst($rel['comodel']); 795 | $relatedModels[$relatedModel] = true; 796 | $relationshipMethods .= "\n public function {$methodName}()\n {\n return \$this->hasMany({$relatedModel}::class);\n }\n"; 797 | } 798 | 799 | foreach ($relations['manyToOne'] as $rel) { 800 | $methodName = Str::camel($rel['role']); 801 | $relatedModel = ucfirst($rel['comodel']); 802 | $relatedModels[$relatedModel] = true; 803 | $relationshipMethods .= "\n public function {$methodName}()\n {\n return \$this->belongsTo({$relatedModel}::class);\n }\n"; 804 | } 805 | 806 | foreach ($relations['manyToMany'] as $rel) { 807 | $methodName = Str::camel($rel['role']); 808 | $relatedModel = ucfirst($rel['comodel']); 809 | $relatedModels[$relatedModel] = true; 810 | $relationshipMethods .= "\n public function {$methodName}()\n {\n return \$this->belongsToMany({$relatedModel}::class);\n }\n"; 811 | } 812 | 813 | // Ajouter les imports pour les modèles liés 814 | $uniqueRelatedModels = array_unique(array_keys($relatedModels)); 815 | foreach ($uniqueRelatedModels as $relatedModel) { 816 | if ($relatedModel !== $name && $relatedModel !== $parentClass) { // Éviter l'auto-import et le parent 817 | $imports .= "\nuse App\\Models\\{$relatedModel};"; 818 | } 819 | } 820 | } 821 | 822 | // Charger le stub et remplacer les placeholders 823 | $stubPath = base_path(self::BASE_STUB_PATH . 'model.stub'); 824 | if (!file_exists($stubPath)) { 825 | $this->error("Stub model.stub introuvable: {$stubPath}"); 826 | return; 827 | } 828 | 829 | $modelContent = file_get_contents($stubPath); 830 | $modelContent = str_replace('{{modelName}}', $name, $modelContent); 831 | $modelContent = str_replace('{{parentClass}}', $parentClass, $modelContent); 832 | $modelContent = str_replace('{{fillable}}', $fillableProperty, $modelContent); 833 | $modelContent = str_replace('{{relationships}}', $relationshipMethods, $modelContent); 834 | $modelContent = str_replace('{{imports}}', $imports, $modelContent); 835 | 836 | file_put_contents($modelPath, $modelContent); 837 | } 838 | 839 | private function updateResource($name, $fieldsArray) 840 | { 841 | $resourcePath = app_path("Http/Resources/{$name}Resource.php"); 842 | if (!file_exists(app_path('Http/Resources'))) { 843 | mkdir(app_path('Http/Resources'), 0755, true); 844 | } 845 | 846 | $fieldsCode = ''; 847 | foreach ($fieldsArray as $field => $type) { 848 | $fieldsCode .= " '{$field}' => \$this->{$field},\n"; 849 | } 850 | 851 | $stub = file_get_contents(base_path(MakeApi::BASE_STUB_PATH . 'resource.stub')); 852 | $template = str_replace( 853 | ['{{modelName}}', '{{fields}}'], 854 | [$name, rtrim($fieldsCode)], 855 | $stub 856 | ); 857 | file_put_contents($resourcePath, $template); 858 | } 859 | } --------------------------------------------------------------------------------