├── .DS_Store ├── stubs ├── enum.stub ├── model.stub ├── seeder.stub ├── factory.stub ├── migration.create.stub ├── alter.stub └── livewire │ ├── livewire.detail.stub │ ├── livewire.store.stub │ ├── livewire.update.stub │ └── livewire.lists.stub ├── src ├── Contract │ ├── ModelGenerationInterface.php │ ├── ModelInspectorInterface.php │ ├── ModelGeneratorInterface.php │ ├── FileSystemInterface.php │ ├── ServiceInterface.php │ ├── RelationshipServiceInterface.php │ └── PromptServiceInterface.php ├── Application │ ├── Port │ │ ├── ModelUpdatePort.php │ │ ├── MigrationGeneratorPort.php │ │ ├── FileSystemPort.php │ │ └── UserInterfacePort.php │ └── UseCase │ │ ├── UpdateExistingModelUseCase.php │ │ └── GetModelFieldsUseCase.php ├── Domain │ ├── Port │ │ ├── ModelRepositoryInterface.php │ │ └── SchemaRepositoryInterface.php │ ├── Entity │ │ ├── ModelField.php │ │ └── ModelUpdate.php │ └── Model │ │ └── ModelDefinition.php ├── Infrastructure │ ├── Laravel │ │ ├── LaravelModelInspector.php │ │ ├── LaravelFileSystem.php │ │ ├── LaravelPromptService.php │ │ ├── LaravelRelationshipService.php │ │ └── PromptService.php │ ├── Repository │ │ ├── LaravelSchemaRepository.php │ │ └── LaravelModelRepository.php │ └── Adapter │ │ ├── FileSystemAdapter.php │ │ └── LaravelFileSystemAdapter.php ├── Concerns │ ├── SeederGenerator.php │ ├── ModelFields.php │ ├── FactoryGenerator.php │ ├── MigrationGenerator.php │ ├── ModelGenerator.php │ └── ModelFieldsGenerator.php ├── Services │ └── ModelFieldsService.php ├── Constants │ └── LaravelConstants.php ├── RapidsServiceProvider.php ├── Helpers │ └── RelationshipHelper.php ├── Relations │ ├── ModelRelation.php │ └── RelationshipGeneration.php └── Console │ └── RapidsModels.php ├── composer.json ├── config └── rapids.php ├── docs ├── presentation.md ├── relations.md └── model.md └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tresor-Kasenda/rapids/HEAD/.DS_Store -------------------------------------------------------------------------------- /stubs/enum.stub: -------------------------------------------------------------------------------- 1 | count(10) 16 | ->create(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stubs/factory.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | {{ fields }} 14 | $table->timestamps(); 15 | }); 16 | } 17 | 18 | public function down(): void 19 | { 20 | Schema::dropIfExists('{{ table }}'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/Contract/ServiceInterface.php: -------------------------------------------------------------------------------- 1 | name; 19 | } 20 | 21 | public function getType(): string 22 | { 23 | return $this->type; 24 | } 25 | 26 | public function isRelation(): bool 27 | { 28 | return $this->isRelation; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/alter.stub: -------------------------------------------------------------------------------- 1 | dropColumn([ 20 | // Add column names here 21 | ]); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /stubs/livewire/livewire.detail.stub: -------------------------------------------------------------------------------- 1 | {{ model | lower }} = ${{ model | lower }}; 18 | } 19 | 20 | public function render(): View 21 | { 22 | return view('livewire.{{ path }}.detail-{{ lastSegment }}'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/Laravel/LaravelModelInspector.php: -------------------------------------------------------------------------------- 1 | getTable()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Infrastructure/Laravel/LaravelFileSystem.php: -------------------------------------------------------------------------------- 1 | modelName}Seeder", $this->modelName], 23 | $seederStub 24 | ); 25 | 26 | File::put(database_path("seeders/{$this->modelName}Seeder.php"), $seederContent); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Infrastructure/Repository/LaravelSchemaRepository.php: -------------------------------------------------------------------------------- 1 | getTable(); 15 | } 16 | 17 | public function getColumnListing(string $tableName): array 18 | { 19 | return Schema::getColumnListing($tableName); 20 | } 21 | 22 | public function getColumnType(string $tableName, string $column): string 23 | { 24 | return Schema::getColumnType($tableName, $column); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Infrastructure/Adapter/FileSystemAdapter.php: -------------------------------------------------------------------------------- 1 | filesystem->get($path); 24 | } 25 | 26 | public function put(string $path, string $content): void 27 | { 28 | $this->filesystem->put($path, $content); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Infrastructure/Adapter/LaravelFileSystemAdapter.php: -------------------------------------------------------------------------------- 1 | filesystem->get($path); 23 | } 24 | 25 | public function put(string $path, string $content): void 26 | { 27 | $this->filesystem->put($path, $content); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Repository/LaravelModelRepository.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 19 | } 20 | 21 | public function getModelName(): string 22 | { 23 | return $this->modelName; 24 | } 25 | 26 | public function withAddedField(ModelField $field): self 27 | { 28 | $newFields = $this->fields; 29 | $newFields[$field->getName()] = $field; 30 | return new self($this->modelName, $newFields); 31 | } 32 | 33 | public function getFields(): array 34 | { 35 | return $this->fields; 36 | } 37 | 38 | public function getTableName(): string 39 | { 40 | return Str::snake(Str::pluralStudly($this->modelName)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/ModelFieldsService.php: -------------------------------------------------------------------------------- 1 | modelFieldsUseCase->execute($this->modelName); 23 | 24 | $this->selectedFields = $result['fields']; 25 | $this->relationFields = $result['relations']; 26 | 27 | return $this->selectedFields; 28 | } 29 | 30 | public function getSelectedFields(): array 31 | { 32 | return $this->selectedFields; 33 | } 34 | 35 | public function getRelationFields(): array 36 | { 37 | return $this->relationFields; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /stubs/livewire/livewire.store.stub: -------------------------------------------------------------------------------- 1 | form->fill(); 25 | } 26 | 27 | public function submit(): void 28 | { 29 | ${{ model | lower }} = {{ model }}::create($this->form->getState()); 30 | 31 | $this->dispatch('notify', message: "{{ model }} created successfully", type: "success"); 32 | } 33 | 34 | public function render(): View 35 | { 36 | return view('livewire.{{ path }}.store-{{ lastSegment }}'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stubs/livewire/livewire.update.stub: -------------------------------------------------------------------------------- 1 | {{ model | lower }} = ${{ model | lower }}; 30 | $this->form->fill(${{ model | lower }}->toArray()); 31 | } 32 | 33 | public function submit(): void 34 | { 35 | $this->{{ model | lower }}->update($this->form->getState()); 36 | 37 | $this->dispatch('notify', message: "{{ model }} updated successfully", type: "success"); 38 | } 39 | 40 | public function render(): View 41 | { 42 | return view('livewire.{{ path }}.update-{{ lastSegment }}'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rapids/rapids", 3 | "description": "Package Laravel de génération de modèles et relations", 4 | "keywords": [ 5 | "laravel", 6 | "models", 7 | "relations", 8 | "generator" 9 | ], 10 | "type": "library", 11 | "license": "MIT", 12 | "autoload": { 13 | "psr-4": { 14 | "Rapids\\Rapids\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Tests\\": "tests/" 20 | } 21 | }, 22 | "require": { 23 | "php": "^8.2|^8.3|^8.4", 24 | "illuminate/console": "^10.0|^11.0|^12.0", 25 | "illuminate/support": "^10.0|^11.0|^12.0", 26 | "laravel/prompts": "^0.1.13|^0.2.0|^0.3.0" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "^8.0|^9.0|^10.0", 30 | "phpunit/phpunit": "^10.0|^11.0" 31 | }, 32 | "authors": [ 33 | { 34 | "name": "Tresor Kasenda", 35 | "email": "tresorkasendat@gmail.com" 36 | } 37 | ], 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "Rapids\\Rapids\\RapidsServiceProvider" 42 | ] 43 | } 44 | }, 45 | "minimum-stability": "stable", 46 | "prefer-stable": true, 47 | "config": { 48 | "sort-packages": true, 49 | "preferred-install": "dist", 50 | "optimize-autoloader": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Contract/RelationshipServiceInterface.php: -------------------------------------------------------------------------------- 1 | getTableName(); 24 | $fields = $modelUpdate->getFields(); 25 | 26 | $migrationContent = $this->migrationGenerator->generateAlterMigration( 27 | $tableName, 28 | $fields 29 | ); 30 | 31 | $migrationName = 'add_fields_to_'.$tableName.'_table'; 32 | $migrationFile = $this->getMigrationPath($migrationName); 33 | $this->fileSystem->put($migrationFile, $migrationContent); 34 | 35 | $this->userInterface->info('Migration created successfully.'); 36 | } 37 | 38 | private function getMigrationPath(string $migrationName): string 39 | { 40 | return database_path("migrations/".date('Y_m_d_His_').$migrationName.'.php'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/rapids.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'models' => app_path('Models'), 16 | 'livewire' => app_path('Livewire/Pages'), 17 | 'views' => resource_path('views/livewire/pages'), 18 | ], 19 | 'namespace' => [ 20 | 'models' => 'App\\Models', 21 | 'livewire' => 'App\\Livewire\\Pages', 22 | ], 23 | 'stubs' => [ 24 | 'migration' => [ 25 | 'model' => __DIR__.'/../stubs/model.stub', 26 | 'migration' => __DIR__.'/../stubs/migration.create.stub', 27 | 'factory' => __DIR__.'/../stubs/factory.stub', 28 | 'seeder' => __DIR__.'/../stubs/seeder.stub', 29 | 'alter' => __DIR__.'/../stubs/alter.stub', 30 | 'enum' => __DIR__.'/../stubs/enum.stub', // Add enum stub path 31 | ], 32 | 'class' => [ 33 | 'Lists' => __DIR__.'/../stubs/livewire/livewire.lists.stub', 34 | 'View' => __DIR__.'/../stubs/livewire/livewire.detail.stub', 35 | 'Store' => __DIR__.'/../stubs/livewire/livewire.store.stub', 36 | 'Update' => __DIR__.'/../stubs/livewire/livewire.update.stub', 37 | ], 38 | 'view_list' => __DIR__.'/../stubs/view-list.stub', 39 | 'view_show' => __DIR__.'/../stubs/view-detail.stub', 40 | 'view_store' => __DIR__.'/../stubs/view-store.stub', 41 | 'view_update' => __DIR__.'/../stubs/view-update.stub', 42 | ], 43 | ]; 44 | -------------------------------------------------------------------------------- /src/Constants/LaravelConstants.php: -------------------------------------------------------------------------------- 1 | 'CASCADE (delete related records)', 49 | 'restrict' => 'RESTRICT (prevent deletion)', 50 | 'nullify' => 'SET NULL (set null on deletion)', 51 | ]; 52 | 53 | /** 54 | * Types de colonnes qui supportent la valeur par défaut 55 | */ 56 | public const array DEFAULT_VALUE_SUPPORTED_TYPES = [ 57 | 'string', 58 | 'text', 59 | 'integer', 60 | 'bigInteger', 61 | 'float', 62 | 'decimal', 63 | 'boolean', 64 | 'enum', 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /src/Infrastructure/Laravel/LaravelPromptService.php: -------------------------------------------------------------------------------- 1 | $options); 33 | } 34 | 35 | public function confirm(string $label, bool $default = false): bool 36 | { 37 | return confirm(label: $label, default: $default); 38 | } 39 | 40 | public function table(array $headers, array $data): void 41 | { 42 | table($headers, $data); 43 | } 44 | 45 | public function info(string $message): void 46 | { 47 | info($message); 48 | } 49 | 50 | public function error(string $message): void 51 | { 52 | error($message); 53 | } 54 | 55 | public function success(string $message): void 56 | { 57 | success($message); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RapidsServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 26 | __DIR__.'/../config/rapids.php' => config_path('rapids.php'), 27 | __DIR__.'/../stubs' => base_path('stubs/vendor/rapids'), 28 | ], 'rapids'); 29 | 30 | if ($this->app->runningInConsole()) { 31 | $this->commands([ 32 | RapidsModels::class, 33 | ]); 34 | } 35 | } 36 | 37 | public function register(): void 38 | { 39 | $this->mergeConfigFrom( 40 | __DIR__.'/../config/rapids.php', 41 | 'rapids' 42 | ); 43 | 44 | // Bind interfaces to implementations 45 | $this->app->bind(FileSystemPort::class, LaravelFileSystemAdapter::class); 46 | $this->app->bind(FileSystemInterface::class, LaravelFileSystem::class); 47 | $this->app->bind(ModelInspectorInterface::class, LaravelModelInspector::class); 48 | $this->app->bind(PromptServiceInterface::class, LaravelPromptService::class); 49 | $this->app->bind(RelationshipServiceInterface::class, LaravelRelationshipService::class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /stubs/livewire/livewire.lists.stub: -------------------------------------------------------------------------------- 1 | query({{ model }}::query()) 29 | ->columns([ 30 | {{ columns }} 31 | ]) 32 | ->filters([ 33 | 34 | ]) 35 | ->actions([ 36 | ViewAction::make() 37 | ->url(fn ({{ model }} $record): string => route('{{ lastSegment }}.show', ['{{ model | lower }}' => $record])), 38 | EditAction::make() 39 | ->url(fn ({{ model }} $record): string => route('{{ lastSegment }}.edit', ['{{ model | lower }}' => $record])), 40 | DeleteAction::make() 41 | ->action(fn ({{ model }} $record) => $record->delete()) 42 | ->requiresConfirmation() 43 | ->color('danger') 44 | ]) 45 | ->bulkActions([ 46 | BulkActionGroup::make([ 47 | DeleteBulkAction::make(), 48 | ]), 49 | ]) 50 | ->emptyStateIcon('heroicon-m-bookmark') 51 | ->emptyStateHeading("Aucun {{ model }} enregistrer"); 52 | } 53 | 54 | public function render(): View 55 | { 56 | return view('livewire.{{ path }}.lists-{{ lastSegment }}'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Application/UseCase/GetModelFieldsUseCase.php: -------------------------------------------------------------------------------- 1 | modelRepository->exists($modelName)) { 24 | throw new RuntimeException("Model {$modelName} does not exist."); 25 | } 26 | 27 | $model = $this->modelRepository->getInstance($modelName); 28 | $tableName = $this->schemaRepository->getTableName($model); 29 | $columns = $this->schemaRepository->getColumnListing($tableName); 30 | 31 | $fields = []; 32 | $relationFields = []; 33 | 34 | foreach ($columns as $column) { 35 | if (in_array($column, $this->systemColumns)) { 36 | continue; 37 | } 38 | 39 | $type = $this->schemaRepository->getColumnType($tableName, $column); 40 | $mappedType = $this->mapDatabaseType($type, $column); 41 | 42 | $fields[$column] = $mappedType; 43 | 44 | if (str_ends_with($column, '_id')) { 45 | $relationFields[$column] = $column; 46 | } 47 | } 48 | 49 | return [ 50 | 'fields' => $fields, 51 | 'relations' => $relationFields 52 | ]; 53 | } 54 | 55 | private function mapDatabaseType(string $type, string $column): string 56 | { 57 | return match ($type) { 58 | 'string', 'text', 'varchar', 'longtext' => 'string', 59 | 'integer', 'bigint', 'smallint' => 'integer', 60 | 'decimal', 'float', 'double' => 'float', 61 | 'boolean' => 'boolean', 62 | 'date' => 'date', 63 | 'datetime', 'timestamp' => 'datetime', 64 | 'json', 'array' => 'json', 65 | default => 'string', 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Infrastructure/Laravel/LaravelRelationshipService.php: -------------------------------------------------------------------------------- 1 | getRelationMethodName($relationType, $relatedModel); 26 | $methods[] = $this->generateRelationMethod($modelName, $relationType, $methodName, $relatedModel); 27 | } 28 | 29 | return implode("\n\n ", $methods); 30 | } 31 | 32 | public function getRelationMethodName(string $relationType, string $modelName): string 33 | { 34 | return match ($relationType) { 35 | 'hasMany', 'belongsToMany', 'morphMany', 'morphToMany', 'morphedByMany', 'hasManyThrough' => 36 | Str::camel(Str::plural($modelName)), 37 | 'hasOne', 'belongsTo', 'morphOne', 'morphTo', 'hasOneThrough' => 38 | Str::camel(Str::singular($modelName)), 39 | default => Str::camel($modelName) 40 | }; 41 | } 42 | 43 | public function generateRelationMethod(string $modelName, string $relationType, string $methodName, string $relatedModel): string 44 | { 45 | // Si le type de relation n'est pas dans la liste des relations connues, retourner un commentaire 46 | if (!in_array($relationType, LaravelConstants::RELATION_TYPES)) { 47 | return "// Unknown relation type: {$relationType}"; 48 | } 49 | 50 | $returnTypeHint = "\\Illuminate\\Database\\Eloquent\\Relations\\" . ucfirst($relationType); 51 | $code = "public function {$methodName}(): {$returnTypeHint}\n" . 52 | " {\n" . 53 | " return \$this->{$relationType}(\\App\\Models\\{$relatedModel}::class);\n" . 54 | " }"; 55 | 56 | return $code; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Infrastructure/Laravel/PromptService.php: -------------------------------------------------------------------------------- 1 | [ 34 | 'hasOne' => 'Has One', 35 | 'belongsTo' => 'Belongs To', 36 | 'belongsToMany' => 'Belongs To Many', 37 | 'hasMany' => 'Has Many', 38 | 'morphOne' => 'Morph One', 39 | 'morphMany' => 'Morph Many', 40 | 'morphTo' => 'Morph To' 41 | ], 42 | placeholder: 'Select relationship type' 43 | ); 44 | } 45 | 46 | public function searchInverseRelationshipType(string $label): string 47 | { 48 | return search( 49 | label: $label, 50 | options: fn () => [ 51 | 'hasOne' => 'Has One', 52 | 'belongsTo' => 'Belongs To', 53 | 'belongsToMany' => 'Belongs To Many', 54 | 'hasMany' => 'Has Many', 55 | 'morphOne' => 'Morph One', 56 | 'morphMany' => 'Morph Many', 57 | 'morphTo' => 'Morph To', 58 | 'none' => 'No inverse relation' 59 | ], 60 | placeholder: 'Select inverse relationship type' 61 | ); 62 | } 63 | 64 | public function info(string $message): void 65 | { 66 | info($message); 67 | } 68 | 69 | public function select(string $label, array $options): string 70 | { 71 | return search( 72 | label: $label, 73 | options: fn () => $options, 74 | placeholder: 'Select an option' 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Contract/PromptServiceInterface.php: -------------------------------------------------------------------------------- 1 | getTable()); 25 | } 26 | 27 | while ($continue) { 28 | $fieldName = text( 29 | label: 'Enter field name (or press enter to finish)', 30 | placeholder: 'e.g. name, email, phone', 31 | ); 32 | 33 | if (empty($fieldName)) { 34 | break; 35 | } 36 | 37 | // Check if field already exists in model 38 | if (in_array($fieldName, $existingFields)) { 39 | info("Field '{$fieldName}' already exists in the model."); 40 | continue; 41 | } 42 | 43 | $fieldType = search( 44 | label: 'Select field type', 45 | options: fn () => [ 46 | 'string' => 'String', 47 | 'text' => 'Text', 48 | 'integer' => 'Integer', 49 | 'bigInteger' => 'Big Integer', 50 | 'float' => 'Float', 51 | 'decimal' => 'Decimal', 52 | 'boolean' => 'Boolean', 53 | 'date' => 'Date', 54 | 'datetime' => 'DateTime', 55 | 'timestamp' => 'Timestamp', 56 | 'json' => 'JSON', 57 | 'enum' => 'Enum', 58 | 'uuid' => 'UUID', 59 | ], 60 | placeholder: 'Select field type' 61 | ); 62 | 63 | $nullable = confirm( 64 | label: "Is this field nullable?", 65 | default: false 66 | ); 67 | 68 | $fields[$fieldName] = [ 69 | 'type' => $fieldType, 70 | 'nullable' => $nullable 71 | ]; 72 | 73 | if ('enum' === $fieldType) { 74 | $values = text( 75 | label: 'Enter enum values (comma-separated)', 76 | placeholder: 'e.g. draft,published,archived' 77 | ); 78 | $fields[$fieldName]['values'] = array_map('trim', explode(',', $values)); 79 | } 80 | } 81 | 82 | return $fields; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Domain/Model/ModelDefinition.php: -------------------------------------------------------------------------------- 1 | name; 28 | } 29 | 30 | public function getFields(): array 31 | { 32 | return $this->fields; 33 | } 34 | 35 | /** 36 | * @param array $fields 37 | * @return self 38 | */ 39 | public function withFields(array $fields): self 40 | { 41 | return new self( 42 | $this->name, 43 | $fields, 44 | $this->relations, 45 | $this->useFillable, 46 | $this->useSoftDelete 47 | ); 48 | } 49 | 50 | public function getRelations(): array 51 | { 52 | return $this->relations; 53 | } 54 | 55 | /** 56 | * @param array $relations 57 | * @return self 58 | */ 59 | public function withRelations(array $relations): self 60 | { 61 | return new self( 62 | $this->name, 63 | $this->fields, 64 | $relations, 65 | $this->useFillable, 66 | $this->useSoftDelete 67 | ); 68 | } 69 | 70 | public function useFillable(): bool 71 | { 72 | return $this->useFillable; 73 | } 74 | 75 | /** 76 | * @param bool $useFillable 77 | * @return self 78 | */ 79 | public function withUseFillable(bool $useFillable): self 80 | { 81 | return new self( 82 | $this->name, 83 | $this->fields, 84 | $this->relations, 85 | $useFillable, 86 | $this->useSoftDelete 87 | ); 88 | } 89 | 90 | public function useSoftDeletes(): bool 91 | { 92 | return $this->useSoftDelete; 93 | } 94 | 95 | /** 96 | * @param bool $useSoftDelete 97 | * @return self 98 | */ 99 | public function withUseSoftDeletes(bool $useSoftDelete): self 100 | { 101 | return new self( 102 | $this->name, 103 | $this->fields, 104 | $this->relations, 105 | $this->useFillable, 106 | $useSoftDelete 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/presentation.md: -------------------------------------------------------------------------------- 1 | # RapidsModels Package Presentation 2 | 3 | ## Overview 4 | 5 | RapidsModels is a powerful Laravel package designed to revolutionize the model creation workflow. It automates and 6 | streamlines the process of generating complete model ecosystems with a single, interactive command. 7 | 8 | ## Why RapidsModels Was Created 9 | 10 | RapidsModels was born out of a common frustration faced by Laravel developers: the repetitive and time-consuming process 11 | of setting up models and their supporting files. The traditional workflow requires developers to: 12 | 13 | 1. Create the model file 14 | 2. Generate a migration 15 | 3. Write a factory 16 | 4. Create a seeder 17 | 5. Manually implement relationships between models 18 | 19 | This fragmented approach not only consumes valuable development time but also increases the likelihood of errors, 20 | inconsistencies, and missed relationships. RapidsModels was created to solve these problems by providing a unified, 21 | interactive solution that handles the entire model ecosystem creation process. 22 | 23 | ## Value Added to the Laravel Community 24 | 25 | RapidsModels brings several significant benefits to Laravel developers: 26 | 27 | ### 1. Development Speed 28 | 29 | - Reduces model setup time by 70-80% 30 | - Eliminates repetitive boilerplate code 31 | - Streamlines the creation of complex relationships 32 | 33 | ### 2. Code Consistency 34 | 35 | - Enforces naming conventions 36 | - Creates standardized model structures 37 | - Generates properly formatted relationships 38 | 39 | ### 3. Error Reduction 40 | 41 | - Validates field types automatically 42 | - Ensures proper relationship definitions 43 | - Maintains referential integrity 44 | 45 | ### 4. Learning Tool 46 | 47 | - Helps junior developers understand Laravel relationships 48 | - Provides practical examples of model structure 49 | - Demonstrates Laravel best practices in action 50 | 51 | ### 5. Documentation Aid 52 | 53 | - Self-documents relationships between models 54 | - Provides clear visibility into database structure 55 | - Makes onboarding new team members easier 56 | 57 | ## Competitive Advantages 58 | 59 | Unlike other model generators, RapidsModels: 60 | 61 | - Handles the complete model ecosystem, not just individual files 62 | - Uses an interactive approach for greater flexibility 63 | - Supports all Laravel relationship types 64 | - Creates both sides of relationships automatically 65 | - Generates factories with realistic test data 66 | 67 | ## Technical Innovation 68 | 69 | RapidsModels represents a technical advancement in Laravel tooling by: 70 | 71 | 1. Combining multiple artisan commands into a unified workflow 72 | 2. Providing a conversational interface for model creation 73 | 3. Supporting complex relationship types including polymorphic relations 74 | 4. Automatically generating appropriate migrations based on relations 75 | 5. Creating testing infrastructure alongside production code 76 | 77 | ## About the Developer 78 | 79 | RapidsModels was developed by Tresor Kasenda, a passionate Laravel developer dedicated to improving the developer 80 | experience and productivity within the Laravel ecosystem. 81 | 82 | With extensive experience in building Laravel applications, Tresor identified common pain points in the model creation 83 | workflow and designed RapidsModels to address these challenges directly. 84 | 85 | Tresor is committed to maintaining and enhancing RapidsModels based on community feedback and evolving Laravel best 86 | practices. The package represents his dedication to contributing valuable tools to the Laravel community while 87 | addressing real-world development needs. 88 | 89 | ## Community Impact 90 | 91 | Since its release, RapidsModels has helped developers: 92 | 93 | - Save thousands of development hours 94 | - Create more consistent and maintainable codebases 95 | - Reduce errors in database relationships 96 | - Accelerate project setup and feature development 97 | 98 | ## Future Directions 99 | 100 | The RapidsModels roadmap includes: 101 | 102 | - Expanded support for additional field types 103 | - Visual relationship mapping 104 | - Integration with database visualization tools 105 | - Enhanced factory data generation 106 | - Support for custom model paths and namespaces 107 | 108 | ## Getting Started 109 | 110 | Join thousands of Laravel developers who have transformed their model creation workflow: 111 | 112 | ```bash 113 | composer require rapids/rapids 114 | php artisan rapids:model YourModel 115 | ``` 116 | 117 | Experience the difference that RapidsModels brings to your Laravel development workflow today. 118 | -------------------------------------------------------------------------------- /src/Helpers/RelationshipHelper.php: -------------------------------------------------------------------------------- 1 | Str::camel(Str::plural($model)), 26 | default => Str::camel(Str::singular($model)), 27 | }; 28 | } 29 | 30 | /** 31 | * Génère le code d'une méthode de relation 32 | * 33 | * @param string $type Type de relation 34 | * @param string $methodName Nom de la méthode 35 | * @param string $model Nom du modèle 36 | * @return string Code PHP généré 37 | */ 38 | public static function generateMethod(string $type, string $methodName, string $model): string 39 | { 40 | // Vérifier si le type de relation est valide 41 | if (!in_array($type, LaravelConstants::RELATION_TYPES)) { 42 | return "// Type de relation inconnu: {$type}"; 43 | } 44 | 45 | return match ($type) { 46 | 'hasOne' => "return \$this->hasOne({$model}::class);", 47 | 'hasMany' => "return \$this->hasMany({$model}::class);", 48 | 'belongsTo' => "return \$this->belongsTo({$model}::class);", 49 | 'belongsToMany' => "return \$this->belongsToMany({$model}::class);", 50 | 'hasOneThrough' => "return \$this->hasOneThrough({$model}::class, IntermediateModel::class);", 51 | 'hasManyThrough' => "return \$this->hasManyThrough({$model}::class, IntermediateModel::class);", 52 | 'morphOne' => "return \$this->morphOne({$model}::class, 'morphable');", 53 | 'morphMany' => "return \$this->morphMany({$model}::class, 'morphable');", 54 | 'morphTo' => "return \$this->morphTo();", 55 | 'morphToMany' => "return \$this->morphToMany({$model}::class, 'taggable');", 56 | 'morphedByMany' => "return \$this->morphedByMany({$model}::class, 'taggable');", 57 | default => "// Logique pour {$type} à implémenter", 58 | }; 59 | } 60 | 61 | /** 62 | * Génère le code d'une méthode de relation avec des indications pour les types complexes 63 | * 64 | * @param string $type Type de relation 65 | * @param string $methodName Nom de la méthode 66 | * @param string $model Nom du modèle 67 | * @return string Message explicatif et code PHP généré 68 | */ 69 | public static function generateMethodWithHint(string $type, string $methodName, string $model): string 70 | { 71 | $methodCode = self::generateMethod($type, $methodName, $model); 72 | 73 | $hint = match ($type) { 74 | 'hasOneThrough', 'hasManyThrough' => 75 | "// Remplacez IntermediateModel par le modèle intermédiaire approprié\n ", 76 | 'morphOne', 'morphMany' => 77 | "// Remplacez 'morphable' par le nom polymorphique approprié\n ", 78 | 'morphToMany', 'morphedByMany' => 79 | "// Remplacez 'taggable' par le nom polymorphique approprié\n ", 80 | default => "", 81 | }; 82 | 83 | return $hint . $methodCode; 84 | } 85 | 86 | /** 87 | * Détermine si un type de relation nécessite des champs polymorphiques 88 | * 89 | * @param string $type Type de relation 90 | * @return bool True si la relation est polymorphique 91 | */ 92 | public static function isPolymorphicRelation(string $type): bool 93 | { 94 | return in_array($type, ['morphOne', 'morphMany', 'morphTo', 'morphToMany', 'morphedByMany']); 95 | } 96 | 97 | /** 98 | * Détermine les champs requis pour un type de relation 99 | * 100 | * @param string $type Type de relation 101 | * @param string $modelName Nom du modèle 102 | * @return array Liste des champs requis 103 | */ 104 | public static function getRequiredFields(string $type, string $modelName): array 105 | { 106 | $singularModel = Str::singular(Str::snake($modelName)); 107 | 108 | return match ($type) { 109 | 'belongsTo' => ["{$singularModel}_id"], 110 | 'morphTo' => [ 111 | "{$singularModel}able_id", 112 | "{$singularModel}able_type" 113 | ], 114 | default => [], 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Concerns/FactoryGenerator.php: -------------------------------------------------------------------------------- 1 | "'{field}' => \$this->faker->words(3, true),", 25 | 'text' => "'{field}' => \$this->faker->paragraph,", 26 | 'integer' => "'{field}' => \$this->faker->numberBetween(1, 1000),", 27 | 'bigInteger' => "'{field}' => \$this->faker->numberBetween(1000, 9999999),", 28 | 'float' => "'{field}' => \$this->faker->randomFloat(2, 1, 1000),", 29 | 'decimal' => "'{field}' => \$this->faker->randomFloat(2, 1, 1000),", 30 | 'boolean' => "'{field}' => \$this->faker->boolean,", 31 | 'date' => "'{field}' => \$this->faker->date(),", 32 | 'datetime' => "'{field}' => \$this->faker->dateTime(),", 33 | 'timestamp' => "'{field}' => \$this->faker->dateTime(),", 34 | 'json' => "'{field}' => ['key' => \$this->faker->word],", 35 | 'uuid' => "'{field}' => \$this->faker->uuid,", 36 | 'email' => "'{field}' => \$this->faker->safeEmail,", 37 | 'phone' => "'{field}' => \$this->faker->phoneNumber,", 38 | 'url' => "'{field}' => \$this->faker->url,", 39 | 'code' => "'{field}' => \$this->faker->unique()->bothify('CODE-####'),", 40 | ]; 41 | 42 | /** 43 | * @param string $modelName The name of the model 44 | * @param array $selectedFields The selected fields for the model 45 | * @param array $relationFields The relation fields for the model 46 | * @param bool $interactive Whether to use interactive prompts (default: true) 47 | */ 48 | public function __construct( 49 | public string $modelName, 50 | public array $selectedFields, 51 | public array $relationFields, 52 | private readonly bool $interactive = true 53 | ) { 54 | } 55 | 56 | /** 57 | * Generate a factory file for the model 58 | * 59 | * @return void 60 | */ 61 | public function generateFactory(): void 62 | { 63 | try { 64 | $factoryStub = File::get(config('rapids.stubs.migration.factory')); 65 | $fields = $this->getModelFields(); 66 | $factoryFields = $this->buildFactoryFields($fields); 67 | $factoryContent = $this->generateFactoryContent($factoryStub, $factoryFields); 68 | 69 | $factoryPath = database_path("factories/{$this->modelName}Factory.php"); 70 | File::put($factoryPath, $factoryContent); 71 | } catch (Exception $e) { 72 | throw new RuntimeException("Failed to generate factory: {$e->getMessage()}", 0, $e); 73 | } 74 | } 75 | 76 | /** 77 | * Get the model fields 78 | * 79 | * @return array 80 | */ 81 | private function getModelFields(): array 82 | { 83 | $modelRepository = new LaravelModelRepository(); 84 | $schemaRepository = new LaravelSchemaRepository(); 85 | $useCase = new GetModelFieldsUseCase($modelRepository, $schemaRepository); 86 | 87 | $service = new ModelFieldsService($this->modelName, $useCase); 88 | return $service->getModelFields(); 89 | } 90 | 91 | /** 92 | * Build factory fields based on field types 93 | * 94 | * @param array $fields 95 | * @return array 96 | */ 97 | private function buildFactoryFields(array $fields): array 98 | { 99 | $factoryFields = []; 100 | 101 | foreach ($fields as $field => $type) { 102 | if (str_ends_with($field, '_id')) { 103 | $factoryFields[] = $this->handleRelationField($field); 104 | } else { 105 | $factoryFields[] = $this->handleRegularField($field, $type); 106 | } 107 | } 108 | 109 | return $factoryFields; 110 | } 111 | 112 | /** 113 | * Handle relation field 114 | * 115 | * @param string $field 116 | * @return string 117 | */ 118 | private function handleRelationField(string $field): string 119 | { 120 | $suggestedModel = Str::studly(Str::beforeLast($field, '_id')); 121 | $relatedModel = $suggestedModel; 122 | 123 | if ($this->interactive) { 124 | $relatedModel = text( 125 | label: "Enter related model name for {$field}", 126 | placeholder: $suggestedModel, 127 | default: $suggestedModel, 128 | required: true 129 | ); 130 | } 131 | 132 | return "'{$field}' => \\App\\Models\\{$relatedModel}::factory(),"; 133 | } 134 | 135 | /** 136 | * Handle regular field based on type 137 | * 138 | * @param string $field 139 | * @param string $type 140 | * @param array $options 141 | * @return string 142 | */ 143 | private function handleRegularField(string $field, string $type, array $options = []): string 144 | { 145 | if (isset(self::TYPE_MAPPINGS[$type])) { 146 | return str_replace('{field}', $field, self::TYPE_MAPPINGS[$type]); 147 | } elseif ('enum' === $type) { 148 | $values = $options['values'] ?? []; 149 | return "'{$field}' => \$this->faker->randomElement(['".implode("', '", $values)."']),"; 150 | } 151 | 152 | return "'{$field}' => \$this->faker->word,"; 153 | } 154 | 155 | /** 156 | * Generate factory content 157 | * 158 | * @param string $stub 159 | * @param array $factoryFields 160 | * @return string 161 | */ 162 | private function generateFactoryContent(string $stub, array $factoryFields): string 163 | { 164 | return str_replace( 165 | ['{{ namespace }}', '{{ model }}', '{{ fields }}'], 166 | ['Database\\Factories', $this->modelName, implode("\n ", $factoryFields)], 167 | $stub 168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Concerns/MigrationGenerator.php: -------------------------------------------------------------------------------- 1 | 'CASCADE (delete related records)', 22 | 'restrict' => 'RESTRICT (prevent deletion)', 23 | 'nullify' => 'SET NULL (set null on deletion)', 24 | ]; 25 | 26 | /** 27 | * @param string|null $modelName The name of the model 28 | * @param bool $interactive Whether to use interactive prompts 29 | */ 30 | public function __construct( 31 | public ?string $modelName, 32 | private bool $interactive = true 33 | ) { 34 | } 35 | 36 | /** 37 | * Generate a migration file for the model 38 | * 39 | * @param array $fields The fields for the migration 40 | * @return void 41 | * @throws RuntimeException If migration generation fails 42 | */ 43 | public function generateMigration(array $fields, bool $softDeletes = false): void 44 | { 45 | try { 46 | $tableName = Str::snake(Str::pluralStudly($this->modelName)); 47 | $migrationName = "create_{$tableName}_table"; 48 | $migrationFile = $this->getMigrationFilePath($migrationName); 49 | 50 | $stub = File::get(config('rapids.stubs.migration.migration')); 51 | $tableFields = $this->generateMigrationFields($fields); 52 | 53 | if ($softDeletes) { 54 | $tableFields .= "\n\$table->softDeletes();"; 55 | } 56 | 57 | $migrationContent = $this->replacePlaceholders($stub, $tableName, $tableFields); 58 | 59 | File::put($migrationFile, $migrationContent); 60 | } catch (Exception $e) { 61 | throw new RuntimeException("Failed to generate migration: {$e->getMessage()}", 0, $e); 62 | } 63 | } 64 | 65 | /** 66 | * Get the full path for a migration file 67 | * 68 | * @param string $migrationName 69 | * @return string 70 | */ 71 | private function getMigrationFilePath(string $migrationName): string 72 | { 73 | $timestamp = date('Y_m_d_His_'); 74 | return database_path("migrations/{$timestamp}{$migrationName}.php"); 75 | } 76 | 77 | /** 78 | * Generate migration fields based on field configuration 79 | * 80 | * @param array $fields 81 | * @return string 82 | */ 83 | public function generateMigrationFields(array $fields): string 84 | { 85 | $tableFields = []; 86 | 87 | foreach ($fields as $field => $options) { 88 | if (str_ends_with($field, '_id')) { 89 | $tableFields[] = $this->generateForeignKeyField($field, $options); 90 | } else { 91 | $tableFields[] = $this->generateRegularField($field, $options); 92 | } 93 | } 94 | 95 | return implode("\n", $tableFields); 96 | } 97 | 98 | /** 99 | * Generate a foreign key field definition 100 | * 101 | * @param string $field 102 | * @param array $options 103 | * @return string 104 | */ 105 | private function generateForeignKeyField(string $field, array $options): string 106 | { 107 | $suggestedTable = Str::plural(Str::beforeLast($field, '_id')); 108 | $relatedTable = $suggestedTable; 109 | $constraintType = 'cascade'; 110 | 111 | if ($this->interactive) { 112 | $relatedTable = text( 113 | label: "Enter related table name for {$field}", 114 | placeholder: $suggestedTable, 115 | default: $suggestedTable 116 | ); 117 | 118 | $constraintType = search( 119 | label: "Select constraint type for {$field}", 120 | options: fn () => self::CONSTRAINT_TYPES 121 | ); 122 | } 123 | 124 | $constraintMethod = match ($constraintType) { 125 | 'cascade' => '->cascadeOnDelete()', 126 | 'restrict' => '->restrictOnDelete()', 127 | 'nullify' => '->nullOnDelete()', 128 | default => '->cascadeOnDelete()', 129 | }; 130 | 131 | $nullable = $options['nullable'] ?? false; 132 | $nullableMethod = $nullable ? '->nullable()' : ''; 133 | 134 | return "\$table->foreignId('{$field}')" 135 | ."->constrained('{$relatedTable}')" 136 | .$constraintMethod 137 | .$nullableMethod 138 | .";"; 139 | } 140 | 141 | /** 142 | * Generate a regular field definition 143 | * 144 | * @param string $field 145 | * @param array $options 146 | * @return string 147 | */ 148 | private function generateRegularField(string $field, array $options): string 149 | { 150 | $type = $options['type'] ?? 'string'; 151 | $nullable = $options['nullable'] ?? false; 152 | 153 | if ('enum' === $type) { 154 | return $this->generateEnumField($field, $options); 155 | } 156 | 157 | $fieldDefinition = "\$table->{$type}('{$field}')"; 158 | 159 | if ($nullable) { 160 | $fieldDefinition .= '->nullable()'; 161 | } 162 | 163 | return $fieldDefinition.';'; 164 | } 165 | 166 | /** 167 | * Generate an enum field definition 168 | * 169 | * @param string $field 170 | * @param array $options 171 | * @return string 172 | */ 173 | private function generateEnumField(string $field, array $options): string 174 | { 175 | $values = array_map(fn ($value) => "'{$value}'", $options['values'] ?? []); 176 | if (empty($values)) { 177 | // Avoid creating an enum column with no values, maybe default to string or throw error? 178 | // For now, let's default to string if no values provided. 179 | return "\$table->string('{$field}')" . (($options['nullable'] ?? false) ? '->nullable()' : '') . '; // Enum values missing'; 180 | } 181 | 182 | $fieldDefinition = "\$table->enum('{$field}', [" . implode(', ', $values) . "])"; 183 | 184 | // Set default value only if provided values are not empty 185 | if (!empty($options['values'])) { 186 | // Use the first value as default, consistent with previous logic 187 | $defaultValue = $options['values'][0]; 188 | $fieldDefinition .= "->default('{$defaultValue}')"; 189 | } 190 | 191 | 192 | if ($options['nullable'] ?? false) { 193 | $fieldDefinition .= '->nullable()'; 194 | } 195 | 196 | return $fieldDefinition . ';'; 197 | } 198 | 199 | /** 200 | * Replace placeholders in the migration stub 201 | * 202 | * @param string $stub 203 | * @param string $tableName 204 | * @param string $tableFields 205 | * @return string 206 | */ 207 | private function replacePlaceholders(string $stub, string $tableName, string $tableFields): string 208 | { 209 | return str_replace( 210 | ['{{ table }}', '{{ fields }}'], 211 | [$tableName, $tableFields], 212 | $stub 213 | ); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Relations/ModelRelation.php: -------------------------------------------------------------------------------- 1 | 'Has One', 21 | 'belongsTo' => 'Belongs To', 22 | 'belongsToMany' => 'Belongs To Many', 23 | 'hasMany' => 'Has Many', 24 | 'hasOneThrough' => 'Has One Through', 25 | 'hasManyThrough' => 'Has Many Through', 26 | 'morphOne' => 'Morph One', 27 | 'morphMany' => 'Morph Many', 28 | 'morphTo' => 'Morph To', 29 | 'morphToMany' => 'Morph To Many', 30 | 'morphedByMany' => 'Morphed By Many', 31 | ]; 32 | 33 | public function __construct( 34 | public array $relationFields 35 | ) { 36 | } 37 | 38 | public function getModelRelations(): array 39 | { 40 | $relations = []; 41 | $hasIdFields = false; 42 | 43 | // Check for foreign key fields 44 | foreach ($this->relationFields as $field => $displayField) { 45 | if (str_ends_with($field, '_id')) { 46 | $hasIdFields = true; 47 | 48 | $relatedModelName = text( 49 | label: "Enter related model name for {$field}", 50 | placeholder: 'e.g. User for user_id', 51 | required: true 52 | ); 53 | 54 | // Get relationship type for current model 55 | $currentModelRelation = search( 56 | label: "Select relationship type for current model to {$relatedModelName}", 57 | options: fn () => self::RELATION_TYPES + ['none' => 'No relation'], 58 | placeholder: 'Select relationship type' 59 | ); 60 | 61 | if ('none' !== $currentModelRelation) { 62 | // Get inverse relationship 63 | $inverseRelation = search( 64 | label: "Select inverse relationship type from {$relatedModelName}", 65 | options: fn () => self::RELATION_TYPES + ['none' => 'No inverse relation'], 66 | placeholder: 'Select inverse relationship type' 67 | ); 68 | 69 | // Add primary relation 70 | $relations[] = [ 71 | 'type' => $currentModelRelation, 72 | 'model' => $relatedModelName, 73 | 'field' => $field 74 | ]; 75 | 76 | // Add inverse relation if selected 77 | if ('none' !== $inverseRelation) { 78 | $relations[] = [ 79 | 'type' => $inverseRelation, 80 | 'model' => $relatedModelName, 81 | 'inverse' => true, 82 | 'field' => $field 83 | ]; 84 | } 85 | } 86 | } 87 | } 88 | 89 | // Manage relations (add, edit, delete) 90 | $this->manageRelations($relations); 91 | 92 | if (empty($relations)) { 93 | info('No relationships defined.'); 94 | } 95 | 96 | return $relations; 97 | } 98 | 99 | private function manageRelations(array &$relations): void 100 | { 101 | while (true) { 102 | // Display current relations 103 | if (!empty($relations)) { 104 | info("Current relationships:"); 105 | $tableData = []; 106 | foreach ($relations as $index => $relation) { 107 | $tableData[] = [ 108 | 'index' => $index, 109 | 'type' => $relation['type'], 110 | 'model' => $relation['model'], 111 | 'inverse' => isset($relation['inverse']) ? 'Yes' : 'No', 112 | 'field' => $relation['field'] ?? 'N/A' 113 | ]; 114 | } 115 | table(['#', 'Type', 'Related Model', 'Inverse', 'Field'], $tableData); 116 | } 117 | 118 | // Choose action 119 | $action = select( 120 | 'What would you like to do with relationships?', 121 | [ 122 | 'add' => 'Add a new relationship', 123 | 'edit' => 'Edit an existing relationship', 124 | 'delete' => 'Delete a relationship', 125 | 'done' => 'Done - proceed with these relationships' 126 | ], 127 | default: empty($relations) ? 'add' : null 128 | ); 129 | 130 | if ('done' === $action) { 131 | break; 132 | } elseif ('delete' === $action && !empty($relations)) { 133 | $indexToDelete = select( 134 | label: 'Select relationship to delete', 135 | options: array_map(fn ($i) => (string)$i, array_keys($relations)) 136 | ); 137 | unset($relations[$indexToDelete]); 138 | $relations = array_values($relations); // Re-index array 139 | info("Relationship has been deleted."); 140 | continue; 141 | } elseif ('edit' === $action && !empty($relations)) { 142 | $indexToEdit = select( 143 | label: 'Select relationship to edit', 144 | options: array_map(fn ($i) => (string)$i, array_keys($relations)) 145 | ); 146 | 147 | $relationType = search( 148 | label: 'Select new relationship type', 149 | options: fn () => self::RELATION_TYPES, 150 | placeholder: "Select new relationship type", 151 | scroll: 10 152 | ); 153 | 154 | $relatedModel = text( 155 | label: 'Enter related model name', 156 | placeholder: 'e.g. User, Post, Comment', 157 | default: $relations[$indexToEdit]['model'], 158 | required: true 159 | ); 160 | 161 | $relations[$indexToEdit]['type'] = $relationType; 162 | $relations[$indexToEdit]['model'] = $relatedModel; 163 | 164 | info("Relationship has been updated."); 165 | continue; 166 | } elseif ('add' === $action) { 167 | $relationType = search( 168 | label: 'Select relationship type', 169 | options: fn () => self::RELATION_TYPES, 170 | placeholder: 'Select relationship type' 171 | ); 172 | 173 | $relatedModel = text( 174 | label: 'Enter related model name', 175 | placeholder: 'e.g. User, Post, Comment', 176 | required: true 177 | ); 178 | 179 | $relations[] = [ 180 | 'type' => $relationType, 181 | 'model' => $relatedModel 182 | ]; 183 | 184 | // Ask if they want to add an inverse relation 185 | if (confirm(label: "Would you like to add an inverse relationship from {$relatedModel}?", default: true)) { 186 | $inverseRelation = search( 187 | label: "Select inverse relationship type from {$relatedModel}", 188 | options: fn () => self::RELATION_TYPES, 189 | placeholder: 'Select inverse relationship type' 190 | ); 191 | 192 | $relations[] = [ 193 | 'type' => $inverseRelation, 194 | 'model' => $relatedModel, 195 | 'inverse' => true 196 | ]; 197 | } 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /docs/relations.md: -------------------------------------------------------------------------------- 1 | # Relations Laravel Avancées avec Rapids 2 | 3 | Ce guide détaille l'utilisation des relations Laravel avancées avec le package Rapids, y compris toutes les relations polymorphiques et les relations "through". 4 | 5 | ## Relations disponibles 6 | 7 | Rapids supporte maintenant **toutes** les relations Laravel : 8 | 9 | 1. Relations simples 10 | - `hasOne` 11 | - `belongsTo` 12 | - `hasMany` 13 | - `belongsToMany` 14 | 15 | 2. Relations "through" 16 | - `hasOneThrough` 17 | - `hasManyThrough` 18 | 19 | 3. Relations polymorphiques 20 | - `morphOne` 21 | - `morphMany` 22 | - `morphTo` 23 | - `morphToMany` 24 | - `morphedByMany` 25 | 26 | ## Relations "Through" 27 | 28 | ### 1. hasOneThrough 29 | 30 | Cette relation établit une connexion "one-to-one" entre deux modèles en passant par un modèle intermédiaire. 31 | 32 | **Exemple :** Un `Supplier` a une relation indirecte vers `Account` via le modèle intermédiaire `User`. 33 | 34 | ``` 35 | Supplier → User → Account 36 | ``` 37 | 38 | **Utilisation avec Rapids :** 39 | 40 | ```bash 41 | php artisan rapids:model Supplier 42 | 43 | # Quand on vous le demande: 44 | > Ajouter une relation ? Yes 45 | > Sélectionner le type de relation : hasOneThrough 46 | > Entrer le nom du modèle lié : Account 47 | > Entrer le nom du modèle intermédiaire : User 48 | > Entrer la clé étrangère sur le modèle intermédiaire : supplier_id 49 | > Entrer la clé étrangère sur le modèle cible : user_id 50 | ``` 51 | 52 | **Code généré :** 53 | 54 | ```php 55 | // Dans Supplier.php 56 | public function account(): \Illuminate\Database\Eloquent\Relations\HasOneThrough 57 | { 58 | return $this->hasOneThrough( 59 | Account::class, 60 | User::class, 61 | 'supplier_id', // Clé étrangère sur la table User 62 | 'user_id', // Clé étrangère sur la table Account 63 | 'id', // Clé locale sur la table Supplier 64 | 'id' // Clé locale sur la table User 65 | ); 66 | } 67 | ``` 68 | 69 | ### 2. hasManyThrough 70 | 71 | Cette relation établit une connexion "one-to-many" entre deux modèles en passant par un modèle intermédiaire. 72 | 73 | **Exemple :** Un `Country` a plusieurs `Post` via le modèle intermédiaire `User`. 74 | 75 | ``` 76 | Country → Users → Posts 77 | ``` 78 | 79 | **Utilisation avec Rapids :** 80 | 81 | ```bash 82 | php artisan rapids:model Country 83 | 84 | # Quand on vous le demande: 85 | > Ajouter une relation ? Yes 86 | > Sélectionner le type de relation : hasManyThrough 87 | > Entrer le nom du modèle lié : Post 88 | > Entrer le nom du modèle intermédiaire : User 89 | > Entrer la clé étrangère sur le modèle intermédiaire : country_id 90 | > Entrer la clé étrangère sur le modèle cible : user_id 91 | ``` 92 | 93 | **Code généré :** 94 | 95 | ```php 96 | // Dans Country.php 97 | public function posts(): \Illuminate\Database\Eloquent\Relations\HasManyThrough 98 | { 99 | return $this->hasManyThrough( 100 | Post::class, 101 | User::class, 102 | 'country_id', // Clé étrangère sur la table User 103 | 'user_id', // Clé étrangère sur la table Post 104 | 'id', // Clé locale sur la table Country 105 | 'id' // Clé locale sur la table User 106 | ); 107 | } 108 | ``` 109 | 110 | ## Relations Polymorphiques 111 | 112 | ### 1. morphTo et morphOne/morphMany 113 | 114 | Ces relations permettent à un modèle d'appartenir à plus d'un autre modèle sur une seule association. 115 | 116 | **Exemple :** Une `Image` peut appartenir à un `Post` ou à un `User`. 117 | 118 | **Utilisation avec Rapids :** 119 | 120 | ```bash 121 | php artisan rapids:model Image 122 | 123 | # Quand on vous le demande: 124 | > Ajouter un champ : imageable_id 125 | > Type du champ : integer 126 | > Ajouter un champ : imageable_type 127 | > Type du champ : string 128 | > Sélectionner le type de relation : morphTo 129 | > Entrer le nom polymorphique : imageable 130 | ``` 131 | 132 | **Code généré :** 133 | 134 | ```php 135 | // Dans Image.php 136 | public function imageable(): \Illuminate\Database\Eloquent\Relations\MorphTo 137 | { 138 | return $this->morphTo(); 139 | } 140 | 141 | // Dans Post.php (lors de la création du modèle Post) 142 | public function image(): \Illuminate\Database\Eloquent\Relations\MorphOne 143 | { 144 | return $this->morphOne(Image::class, 'imageable'); 145 | } 146 | 147 | // Dans User.php (lors de la création du modèle User) 148 | public function image(): \Illuminate\Database\Eloquent\Relations\MorphOne 149 | { 150 | return $this->morphOne(Image::class, 'imageable'); 151 | } 152 | ``` 153 | 154 | ### 2. morphToMany et morphedByMany 155 | 156 | Ces relations établissent des connexions "many-to-many" polymorphiques. 157 | 158 | **Exemple :** Un `Post` ou une `Video` peuvent avoir plusieurs `Tag`, et un `Tag` peut être associé à de nombreux `Post` ou `Video`. 159 | 160 | **Utilisation avec Rapids :** 161 | 162 | ```bash 163 | php artisan rapids:model Post 164 | 165 | # Quand on vous le demande: 166 | > Ajouter une relation ? Yes 167 | > Sélectionner le type de relation : morphToMany 168 | > Entrer le nom du modèle lié : Tag 169 | > Entrer le nom polymorphique : taggable 170 | > Ajouter timestamps à la table pivot ? Yes 171 | > Personnaliser le nom de la table pivot ? No 172 | ``` 173 | 174 | **Code généré :** 175 | 176 | ```php 177 | // Dans Post.php 178 | public function tags(): \Illuminate\Database\Eloquent\Relations\MorphToMany 179 | { 180 | return $this->morphToMany(Tag::class, 'taggable') 181 | ->withTimestamps(); 182 | } 183 | 184 | // Dans Tag.php (lors de la création ou mise à jour du modèle Tag) 185 | public function posts(): \Illuminate\Database\Eloquent\Relations\MorphedByMany 186 | { 187 | return $this->morphedByMany(Post::class, 'taggable') 188 | ->withTimestamps(); 189 | } 190 | ``` 191 | 192 | ## Options avancées pour les tables pivot 193 | 194 | ### Personnalisation des tables pivot 195 | 196 | Pour les relations `belongsToMany`, `morphToMany` et `morphedByMany`, vous pouvez personnaliser la table pivot : 197 | 198 | ```bash 199 | > Personnaliser le nom de la table pivot ? Yes 200 | > Entrer le nom personnalisé de la table pivot : custom_post_tags 201 | ``` 202 | 203 | ### Ajout de champs supplémentaires 204 | 205 | Pour les tables pivot, vous pouvez ajouter des champs supplémentaires : 206 | 207 | ```bash 208 | > Ajouter des champs supplémentaires à la table pivot ? Yes 209 | > Entrer le nom du champ : status 210 | > Entrer le type du champ : enum 211 | > Entrer les valeurs (séparées par des virgules) : pending,approved,rejected 212 | ``` 213 | 214 | ### Ajout de timestamps 215 | 216 | Pour les tables pivot, vous pouvez activer les timestamps : 217 | 218 | ```bash 219 | > Ajouter timestamps à la table pivot ? Yes 220 | ``` 221 | 222 | Cela ajoute `$table->timestamps();` à la migration de la table pivot et appelle `->withTimestamps()` sur la relation. 223 | 224 | ## Stratégies d'implémentation recommandées 225 | 226 | ### 1. Relations hiérarchiques 227 | 228 | Pour les modèles avec des hiérarchies ou des arbres : 229 | 230 | ```php 231 | // Auto-relation 232 | public function parent(): \Illuminate\Database\Eloquent\Relations\BelongsTo 233 | { 234 | return $this->belongsTo(Category::class, 'parent_id'); 235 | } 236 | 237 | public function children(): \Illuminate\Database\Eloquent\Relations\HasMany 238 | { 239 | return $this->hasMany(Category::class, 'parent_id'); 240 | } 241 | ``` 242 | 243 | ### 2. Combinaison de relations standard et polymorphiques 244 | 245 | Pour des modèles comme les commentaires qui peuvent être à la fois liés à un utilisateur et à différents types de contenu : 246 | 247 | ```php 248 | // Dans Comment.php 249 | public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo 250 | { 251 | return $this->belongsTo(User::class); 252 | } 253 | 254 | public function commentable(): \Illuminate\Database\Eloquent\Relations\MorphTo 255 | { 256 | return $this->morphTo(); 257 | } 258 | ``` 259 | 260 | ### 3. Relations many-to-many avec données pivots 261 | 262 | Pour des relations many-to-many avec des données supplémentaires : 263 | 264 | ```php 265 | // Dans User.php 266 | public function roles(): \Illuminate\Database\Eloquent\Relations\BelongsToMany 267 | { 268 | return $this->belongsToMany(Role::class) 269 | ->withTimestamps() 270 | ->withPivot('is_active', 'expires_at'); 271 | } 272 | ``` 273 | 274 | ## Conclusion 275 | 276 | Le package Rapids offre maintenant un support complet pour toutes les relations Laravel, y compris les relations les plus avancées comme les relations "through" et polymorphiques. Cette flexibilité vous permet de modéliser facilement des structures de données complexes tout en maintenant un code propre et maintenable. -------------------------------------------------------------------------------- /src/Concerns/ModelGenerator.php: -------------------------------------------------------------------------------- 1 | getName(); 29 | $fields = $modelDefinition->getFields(); 30 | 31 | try { 32 | $modelContent = $this->buildModelContent($modelDefinition); 33 | 34 | $modelPath = $this->getModelPath($modelName); 35 | $this->fileSystem->put($modelPath, $modelContent); 36 | 37 | $this->processRelationships($modelDefinition); 38 | } catch (Exception $e) { 39 | throw new RuntimeException("Failed to generate model: {$e->getMessage()}", 0, $e); 40 | } 41 | } 42 | 43 | private function buildModelContent(ModelDefinition $modelDefinition): string 44 | { 45 | $modelName = $modelDefinition->getName(); 46 | $fields = $modelDefinition->getFields(); 47 | $relations = []; 48 | $protectionType = $this->promptService->select( 49 | 'How would you like to protect your model attributes?', 50 | [ 51 | 'fillable' => 'Use $fillable (explicitly allow fields)', 52 | 'guarded' => 'Use $guarded (explicitly deny fields)' 53 | ] 54 | ); 55 | 56 | $fieldNames = array_keys($fields); 57 | 58 | // Generate protection array string 59 | $protectionStr = match ($protectionType) { 60 | 'fillable' => "\n protected \$fillable = ['" . implode("', '", $fieldNames) . "'];", 61 | 'guarded' => "\n protected \$guarded = [];" // Empty guarded means all fields are mass assignable 62 | }; 63 | 64 | $useStatements = 'use Illuminate\\Database\\Eloquent\\Model;'; 65 | $traits = ''; 66 | $casts = []; // Initialize casts array 67 | 68 | if ($modelDefinition->useSoftDeletes()) { 69 | $useStatements .= "\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;"; 70 | $traits = "\n use SoftDeletes;"; // Indent trait usage 71 | } 72 | 73 | foreach ($fields as $field => $options) { 74 | if (str_ends_with($field, '_id')) { 75 | $relatedModel = ucfirst(Str::beforeLast($field, '_id')); 76 | $relations[] = [ 77 | 'type' => 'belongsTo', 78 | 'model' => $relatedModel 79 | ]; 80 | } elseif ($options['type'] === 'enum') { 81 | // Add Enum cast 82 | $enumName = Str::studly($modelName) . Str::studly($field) . 'Enum'; 83 | $enumNamespace = 'App\\Enums'; 84 | // Add use statement only if not already added (e.g., multiple enums) 85 | $useStatementToAdd = "\nuse {$enumNamespace}\\{$enumName};"; 86 | if (!str_contains($useStatements, $useStatementToAdd)) { 87 | $useStatements .= $useStatementToAdd; 88 | } 89 | $casts[] = "'{$field}' => {$enumName}::class"; 90 | } 91 | // Add other casts if needed, e.g., for date/datetime, json, boolean 92 | elseif (in_array($options['type'], ['date', 'datetime', 'timestamp'])) { 93 | $castType = ($options['type'] === 'date') ? 'date' : 'datetime'; 94 | $casts[] = "'{$field}' => '{$castType}'"; 95 | } elseif ($options['type'] === 'json') { 96 | $casts[] = "'{$field}' => 'array'"; // Ou 'object' selon le besoin 97 | } elseif ($options['type'] === 'boolean') { 98 | $casts[] = "'{$field}' => 'boolean'"; 99 | } elseif ($options['type'] === 'uuid') { 100 | $useStatements .= "\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;"; 101 | $traits .= $traits ? "\n use HasUuids;" : "\n use HasUuids;"; 102 | } 103 | } 104 | 105 | $relations = array_merge($relations, $modelDefinition->getRelations()); 106 | $modelStubPath = $this->getStubPath(); 107 | if (!$this->fileSystem->exists($modelStubPath)) { 108 | throw new RuntimeException("Model stub not found at: {$modelStubPath}"); 109 | } 110 | $modelStub = $this->fileSystem->get($modelStubPath); 111 | $relationMethods = $this->relationshipService->generateRelationMethods($modelName, $relations); 112 | 113 | // Generate casts string 114 | $castsStr = ''; 115 | if (!empty($casts)) { 116 | $castsStr = "\n\n protected \$casts = [\n " . implode(",\n ", $casts) . "\n ];"; 117 | } 118 | 119 | return str_replace( 120 | ['{{ namespace }}', '{{ use }}', '{{ class }}', '{{ traits }}', '{{ protection }}', '{{ casts }}', '{{ relations }}'], 121 | ['App\\Models', $useStatements, $modelName, $traits, $protectionStr, $castsStr, $relationMethods], 122 | $modelStub 123 | ); 124 | } 125 | 126 | private function getStubPath(): string 127 | { 128 | return config('rapids.stubs.migration.model'); 129 | } 130 | 131 | private function getModelPath(string $modelName): string 132 | { 133 | return app_path("Models/{$modelName}.php"); 134 | } 135 | 136 | private function processRelationships(ModelDefinition $modelDefinition): void 137 | { 138 | $modelName = $modelDefinition->getName(); 139 | $fields = $modelDefinition->getFields(); 140 | $relations = $modelDefinition->getRelations(); 141 | 142 | foreach ($fields as $field => $options) { 143 | if (str_ends_with($field, '_id')) { 144 | $this->processForeignKeyRelation($modelName, $field); 145 | } 146 | } 147 | 148 | $this->promptForAdditionalRelations($modelName); 149 | } 150 | 151 | private function processForeignKeyRelation(string $modelName, string $field): void 152 | { 153 | $relatedModelName = $this->promptService->text( 154 | "Enter related model name for {$field}", 155 | 'e.g. User for user_id', 156 | true 157 | ); 158 | 159 | $currentModelRelation = $this->promptService->searchRelationshipType( 160 | "Select relationship type for {$modelName} to {$relatedModelName}" 161 | ); 162 | 163 | $inverseRelation = $this->promptService->searchInverseRelationshipType( 164 | "Select inverse relationship type for {$relatedModelName} to {$modelName}" 165 | ); 166 | 167 | if ('none' !== $inverseRelation) { 168 | $this->addRelationToRelatedModel($relatedModelName, $modelName, $inverseRelation); 169 | } 170 | } 171 | 172 | private function addRelationToRelatedModel(string $relatedModelName, string $currentModelName, string $relationType): void 173 | { 174 | $modelPath = app_path("Models/{$relatedModelName}.php"); 175 | 176 | if (!$this->fileSystem->exists($modelPath)) { 177 | $this->promptService->info("Related model file not found: {$modelPath}"); 178 | return; 179 | } 180 | 181 | $content = $this->fileSystem->get($modelPath); 182 | 183 | $methodName = $this->relationshipService->getRelationMethodName($relationType, $currentModelName); 184 | 185 | $relationMethod = $this->relationshipService->generateRelationMethod( 186 | $relatedModelName, 187 | $relationType, 188 | $methodName, 189 | $currentModelName 190 | ); 191 | 192 | if (str_contains($content, "function {$methodName}(")) { 193 | $this->promptService->info("Relation method {$methodName}() already exists in {$relatedModelName} model"); 194 | return; 195 | } 196 | 197 | $content = preg_replace('/}(\s*)$/', "\n {$relationMethod}\n}", $content); 198 | 199 | $this->fileSystem->put($modelPath, $content); 200 | $this->promptService->info("Added {$relationType} relation from {$relatedModelName} to {$currentModelName}"); 201 | } 202 | 203 | private function promptForAdditionalRelations(string $modelName): void 204 | { 205 | while ($this->promptService->confirm("Would you like to add another relationship?", false)) { 206 | $relationType = $this->promptService->searchRelationshipType('Select relationship type'); 207 | 208 | $relatedModel = $this->promptService->text( 209 | 'Enter related model name', 210 | 'e.g. User, Post, Comment', 211 | true 212 | ); 213 | 214 | $inverseRelation = $this->promptService->searchInverseRelationshipType( 215 | "Select inverse relationship type for {$relatedModel} to {$modelName}" 216 | ); 217 | 218 | if ('none' !== $inverseRelation) { 219 | $this->addRelationToRelatedModel($relatedModel, $modelName, $inverseRelation); 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Concerns/ModelFieldsGenerator.php: -------------------------------------------------------------------------------- 1 | modelInspector = $modelInspector ?? new LaravelModelInspector(); 27 | $this->promptService = $promptService ?? new LaravelPromptService(); 28 | } 29 | 30 | public function generate(): array 31 | { 32 | $this->fields = []; 33 | $this->usedFieldNames = []; 34 | $existingFields = $this->modelInspector->getExistingFields($this->modelName); 35 | 36 | while (true) { 37 | $this->displayCurrentFields(); 38 | 39 | $action = $this->promptService->select( 40 | 'What would you like to do?', 41 | [ 42 | 'add' => 'Add a new field', 43 | 'edit' => 'Edit an existing field', 44 | 'delete' => 'Delete a field', 45 | 'done' => 'Done - proceed with these fields' 46 | ], 47 | empty($this->fields) ? 'add' : null 48 | ); 49 | 50 | if ('done' === $action) { 51 | break; 52 | } 53 | 54 | if ('delete' === $action) { 55 | $this->deleteField(); 56 | continue; 57 | } 58 | 59 | if ('edit' === $action) { 60 | $this->editField(); 61 | continue; 62 | } 63 | 64 | $this->addField($existingFields); 65 | } 66 | 67 | return $this->fields; 68 | } 69 | 70 | private function displayCurrentFields(): void 71 | { 72 | if (empty($this->fields)) { 73 | return; 74 | } 75 | 76 | $tableData = []; 77 | foreach ($this->fields as $name => $options) { 78 | $tableData[] = [ 79 | 'name' => $name, 80 | 'type' => $options['type'], 81 | 'nullable' => $options['nullable'] ? 'Yes' : 'No', 82 | 'values' => isset($options['values']) ? implode(',', $options['values']) : '-' 83 | ]; 84 | } 85 | 86 | $this->promptService->table(['Field', 'Type', 'Nullable', 'Enum Values'], $tableData); 87 | } 88 | 89 | private function deleteField(): void 90 | { 91 | if (empty($this->fields)) { 92 | $this->promptService->error('No fields to delete.'); 93 | return; 94 | } 95 | 96 | $fieldToDelete = $this->promptService->search( 97 | 'Select field to delete', 98 | array_keys($this->fields) 99 | ); 100 | 101 | unset($this->fields[$fieldToDelete]); 102 | $this->usedFieldNames = array_diff($this->usedFieldNames, [$fieldToDelete]); 103 | $this->promptService->info("Field '{$fieldToDelete}' has been deleted."); 104 | } 105 | 106 | private function editField(): void 107 | { 108 | if (empty($this->fields)) { 109 | $this->promptService->error('No fields to edit.'); 110 | return; 111 | } 112 | 113 | $fieldToEdit = $this->promptService->search( 114 | 'Select field to edit', 115 | array_keys($this->fields) 116 | ); 117 | 118 | // Store original type and values to check if they changed 119 | $originalOptions = $this->fields[$fieldToEdit] ?? []; 120 | $originalType = $originalOptions['type'] ?? null; 121 | $originalValues = $originalOptions['values'] ?? []; 122 | 123 | // Collect new options, which might trigger enum generation if type changes to enum 124 | $newOptions = $this->collectFieldOptions($fieldToEdit, $originalOptions); 125 | $this->fields[$fieldToEdit] = $newOptions; 126 | 127 | $enumName = Str::studly($this->modelName) . Str::studly($fieldToEdit) . 'Enum'; 128 | $enumPath = app_path("Enums/{$enumName}.php"); 129 | 130 | // Case 1: Type changed FROM enum TO something else 131 | if ($originalType === 'enum' && $newOptions['type'] !== 'enum') { 132 | if (File::exists($enumPath)) { 133 | // Optionally ask the user if they want to delete the old enum file 134 | if ($this->promptService->confirm("Field '{$fieldToEdit}' is no longer an enum. Delete the existing Enum file '{$enumName}.php'?", false)) { 135 | File::delete($enumPath); 136 | $this->promptService->info("Deleted Enum file: {$enumPath}"); 137 | } 138 | } 139 | } 140 | // Case 2: Type remained enum, but values changed 141 | elseif ($originalType === 'enum' && $newOptions['type'] === 'enum' && $originalValues !== $newOptions['values']) { 142 | $this->promptService->info("Enum values changed for '{$fieldToEdit}'. Regenerating Enum file..."); 143 | // Force regeneration by deleting the old one first if it exists 144 | if (File::exists($enumPath)) { 145 | File::delete($enumPath); 146 | } 147 | // generateEnumFile is called within collectFieldOptions, but we call it again explicitly 148 | // after deleting to ensure it's recreated with new values. 149 | // This assumes collectFieldOptions doesn't skip generation if file exists. 150 | // Let's refine generateEnumFile to handle overwriting if needed during edits. 151 | $this->generateEnumFile($fieldToEdit, $newOptions['values']); // Regenerate 152 | } 153 | // Case 3: Type changed TO enum (handled inside collectFieldOptions -> generateEnumFile) 154 | 155 | $this->promptService->info("Field '{$fieldToEdit}' has been updated."); 156 | } 157 | 158 | private function collectFieldOptions(string $fieldName, array $defaults = []): array 159 | { 160 | $fieldType = $this->promptService->search( 161 | "Select field type".( ! empty($defaults) ? " for '{$fieldName}'" : ""), 162 | [ 163 | 'string' => 'String', 164 | 'text' => 'Text', 165 | 'integer' => 'Integer', 166 | 'bigInteger' => 'Big Integer', 167 | 'float' => 'Float', 168 | 'decimal' => 'Decimal', 169 | 'boolean' => 'Boolean', 170 | 'date' => 'Date', 171 | 'datetime' => 'DateTime', 172 | 'timestamp' => 'Timestamp', 173 | 'json' => 'JSON', 174 | 'enum' => 'Enum', 175 | 'uuid' => 'UUID', 176 | 'foreignId' => 'Foreign ID (UNSIGNED BIGINT)', 177 | ], 178 | $defaults['type'] ?? null 179 | ); 180 | 181 | $nullable = $this->promptService->confirm( 182 | "Is this field nullable?", 183 | $defaults['nullable'] ?? false 184 | ); 185 | 186 | $options = [ 187 | 'type' => $fieldType, 188 | 'nullable' => $nullable 189 | ]; 190 | 191 | if ('enum' === $fieldType) { 192 | $defaultValues = isset($defaults['values']) ? implode(',', $defaults['values']) : ''; 193 | $values = $this->promptService->text( 194 | 'Enter enum values (comma-separated)', 195 | 'e.g. draft,published,archived', 196 | $defaultValues 197 | ); 198 | $options['values'] = array_map('trim', explode(',', $values)); 199 | 200 | // Generate Enum file 201 | $this->generateEnumFile($fieldName, $options['values']); 202 | } 203 | 204 | return $options; 205 | } 206 | 207 | private function addField(array $existingFields): void 208 | { 209 | $fieldName = $this->promptService->text( 210 | 'Enter field name', 211 | 'e.g. name, email, phone' 212 | ); 213 | 214 | if (empty($fieldName)) { 215 | return; 216 | } 217 | 218 | if (in_array($fieldName, $this->usedFieldNames)) { 219 | $this->promptService->error("You have already added a field named '{$fieldName}'. Please use a different name."); 220 | return; 221 | } 222 | 223 | if (in_array($fieldName, $existingFields)) { 224 | $this->promptService->error("Field '{$fieldName}' already exists in the model."); 225 | return; 226 | } 227 | 228 | $this->fields[$fieldName] = $this->collectFieldOptions($fieldName); 229 | $this->usedFieldNames[] = $fieldName; 230 | } 231 | 232 | private function generateEnumFile(string $fieldName, array $values): void 233 | { 234 | $enumName = Str::studly($this->modelName) . Str::studly($fieldName) . 'Enum'; 235 | $enumPath = app_path("Enums/{$enumName}.php"); 236 | $enumNamespace = 'App\\Enums'; 237 | 238 | if (File::exists($enumPath)) { 239 | // Don't overwrite if editing and values haven't changed, handled in editField 240 | // If adding a new field and it exists, maybe prompt? For now, just inform. 241 | $this->promptService->info("Enum file already exists: {$enumPath}"); 242 | return; 243 | } 244 | 245 | // Ensure the Enums directory exists 246 | if (!File::isDirectory(app_path('Enums'))) { 247 | File::makeDirectory(app_path('Enums'), 0755, true); 248 | } 249 | 250 | $stubPath = config('rapids.stubs.migration.enum'); 251 | if (!$stubPath || !File::exists($stubPath)) { 252 | $this->promptService->error("Enum stub file not found at path defined in config: {$stubPath}"); 253 | return; 254 | } 255 | $stub = File::get($stubPath); 256 | 257 | 258 | $cases = ''; 259 | foreach ($values as $value) { 260 | // Sanitize value to be a valid case name (e.g., 'draft' -> Draft, 'is_active' -> IsActive) 261 | $caseName = Str::studly($value); 262 | // Ensure case name starts with a letter or underscore 263 | if (!preg_match('/^[a-zA-Z_]/', $caseName)) { 264 | $caseName = '_' . $caseName; // Prepend underscore if it starts with a number 265 | } 266 | // Ensure case name is valid PHP identifier (basic check) 267 | $caseName = preg_replace('/[^a-zA-Z0-9_\x7f-\xff]/', '', $caseName); 268 | 269 | $cases .= " case {$caseName} = '{$value}';\n"; 270 | } 271 | 272 | $content = str_replace( 273 | ['{{ namespace }}', '{{ class }}', '{{ cases }}'], 274 | [$enumNamespace, $enumName, rtrim($cases)], // rtrim to remove trailing newline 275 | $stub 276 | ); 277 | 278 | File::put($enumPath, $content); 279 | $this->promptService->info("Created Enum: {$enumPath}"); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RapidsModels 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/rapids/rapids.svg?style=flat-square)](https://packagist.org/packages/rapids/rapids) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/rapids/rapids.svg?style=flat-square)](https://packagist.org/packages/rapids/rapids) 5 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/Tresor-Kasenda?style=social)](https://x.com/TresorKasenda) 6 | [![GitHub Issues](https://img.shields.io/github/issues/Tresor-Kasenda/rapids-models.svg?style=flat-square)](https://packagist.org/packages/rapids/rapids) 7 | 8 | > **Supercharge your Laravel development workflow by generating complete model ecosystems with a single command** 9 | 10 | ## 📚 Table of Contents 11 | 12 | - [Introduction](#introduction) 13 | - [Installation](#installation) 14 | - [Core Features](#core-features) 15 | - [Basic Usage](#basic-usage) 16 | - [Field Types](#field-types) 17 | - [Relationship Management](#relationship-management) 18 | - [Belongs To Relationship](#1-belongs-to-relationship) 19 | - [Has One Relationship](#2-has-one-relationship) 20 | - [Has Many Relationship](#3-has-many-relationship) 21 | - [Belongs To Many Relationship](#4-belongs-to-many-relationship) 22 | - [Has One Through Relationship](#5-has-one-through-relationship) 23 | - [Has Many Through Relationship](#6-has-many-through-relationship) 24 | - [Polymorphic Relationships](#7-polymorphic-relationships) 25 | - [Working with Existing Models](#working-with-existing-models) 26 | - [PHP Compatibility](#php-compatibility) 27 | - [Contributing](#contributing) 28 | - [Support](#support) 29 | - [License](#license) 30 | 31 | ## Introduction 32 | 33 | RapidsModels is a Laravel package designed to streamline your development workflow by automating the creation of the 34 | entire model ecosystem. Instead of manually creating models, migrations, factories, and seeders separately, RapidsModels 35 | handles everything with a single command and an intuitive interactive process. 36 | 37 | **Why Use RapidsModels?** 38 | 39 | - **Time Efficiency**: Create complete model ecosystems in seconds 40 | - **Consistency**: Maintain standardized code across your project 41 | - **Interactive Process**: Guided setup with clear prompts 42 | - **Complete Solution**: Generates models, migrations, factories, seeders, and relationships 43 | - **Full Laravel Relations Support**: Supports ALL Laravel relationship types (hasOne, belongsTo, hasMany, belongsToMany, hasOneThrough, hasManyThrough, morphOne, morphMany, morphTo, morphToMany, morphedByMany) 44 | - **Modern PHP Support**: Compatible with PHP 8.2, 8.3, and 8.4 45 | - **Flexible**: Works with new projects or existing codebases 46 | 47 | ## Installation 48 | 49 | Installing RapidsModels is straightforward with Composer: 50 | 51 | ```bash 52 | composer require rapids/rapids 53 | ``` 54 | 55 | Laravel will automatically discover the package - no additional configuration required. 56 | 57 | ## Core Features 58 | 59 | - **One-Command Generation**: Create models, migrations, factories, and seeders with a single command 60 | - **Interactive Setup**: Guided creation process for fields and relationships 61 | - **Comprehensive Field Support**: Supports all Laravel field types with appropriate options 62 | - **Automated Relationships**: Configures both sides of model relationships 63 | - **Complete Relations Support**: All Laravel relationships including through and polymorphic relations 64 | - **Pivot Table Support**: Handles many-to-many relationships with customizable pivot tables 65 | - **Existing Model Integration**: Works with existing models to add fields or relationships 66 | - **Migration Generation**: Creates migrations for new models or updates to existing ones 67 | - **Modern PHP Support**: Takes advantage of PHP 8.2+ features like readonly classes 68 | 69 | ## Basic Usage 70 | 71 | Generate a complete model ecosystem with a single command: 72 | 73 | ```bash 74 | php artisan rapids:model Product 75 | ``` 76 | 77 | The interactive process will guide you through: 78 | 79 | 1. Adding fields with their types and options 80 | 2. Setting up foreign keys and relationships 81 | 3. Configuring timestamps, soft deletes, and other options 82 | 4. Creating factories and seeders 83 | 84 | ### Using Fields JSON Flag 85 | 86 | You can also create a model with a single command by providing field definitions as a JSON string: 87 | 88 | ```bash 89 | php artisan rapids:model User --fields='{"name":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"_config":{"softDeletes":true}}' 90 | ``` 91 | 92 | The JSON structure supports: 93 | - Field definitions with type, nullable, default, length properties 94 | - Relationship definitions with type, model, and inverse properties 95 | - Configuration options like softDeletes 96 | 97 | Example with relationships: 98 | 99 | ```bash 100 | php artisan rapids:model Post --fields='{"title":{"type":"string"},"content":{"type":"text"},"category":{"relation":{"type":"belongsTo","model":"Category","inverse":"hasMany"}},"_config":{"softDeletes":true}}' 101 | ``` 102 | 103 | ### Traditional Approach vs RapidsModels 104 | 105 | **Traditional Approach:** 106 | 107 | ``` 108 | - Create model: php artisan make:model Product 109 | - Create migration: php artisan make:migration create_products_table 110 | - Create factory: php artisan make:factory ProductFactory 111 | - Create seeder: php artisan make:seeder ProductSeeder 112 | - Define fields: Manually edit migration file 113 | - Configure relations: Manually edit model files 114 | ``` 115 | 116 | **RapidsModels Approach:** 117 | 118 | ``` 119 | - Everything at once: php artisan rapids:model Product 120 | (follow the interactive prompts) 121 | ``` 122 | 123 | ## Field Types 124 | 125 | RapidsModels supports all standard Laravel field types: 126 | 127 | | Type | Description | Example Use Cases | 128 | |----------|-----------------------|---------------------------------| 129 | | string | Text data | name, title, slug | 130 | | text | Longer text | content, description, biography | 131 | | integer | Whole numbers | count, position, age | 132 | | decimal | Numbers with decimals | price, weight, rating | 133 | | boolean | True/false values | is_active, has_discount | 134 | | date | Date without time | birth_date, release_date | 135 | | datetime | Date with time | starts_at, expires_at | 136 | | enum | Predefined options | status, role, type | 137 | | json | JSON data | settings, preferences, metadata | 138 | | uuid | UUID identifiers | uuid field with HasUuids trait | 139 | 140 | ## Relationship Management 141 | 142 | RapidsModels simplifies creating and managing relationships between models. 143 | 144 | ### 1. Belongs To Relationship 145 | 146 | **Example: Product belongs to Category** 147 | 148 | ```bash 149 | > Enter field name: category_id 150 | > Enter field type: integer 151 | > Is this a foreign key? Yes 152 | > Enter related model name: Category 153 | > Select relationship type: belongsTo 154 | > Select inverse relationship type: hasMany 155 | ``` 156 | 157 | **Generated Code:** 158 | 159 | ```php 160 | // In Product.php 161 | public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo 162 | { 163 | return $this->belongsTo(Category::class); 164 | } 165 | 166 | // In Category.php 167 | public function products(): \Illuminate\Database\Eloquent\Relations\HasMany 168 | { 169 | return $this->hasMany(Product::class); 170 | } 171 | ``` 172 | 173 | ### 2. Has One Relationship 174 | 175 | **Example: User has one Profile** 176 | 177 | ```bash 178 | > Enter field name: user_id 179 | > Enter field type: integer 180 | > Is this a foreign key? Yes 181 | > Enter related model name: User 182 | > Select relationship type: belongsTo 183 | > Select inverse relationship type: hasOne 184 | ``` 185 | 186 | **Generated Code:** 187 | 188 | ```php 189 | // In Profile.php 190 | public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo 191 | { 192 | return $this->belongsTo(User::class); 193 | } 194 | 195 | // In User.php 196 | public function profile(): \Illuminate\Database\Eloquent\Relations\HasOne 197 | { 198 | return $this->hasOne(Profile::class); 199 | } 200 | ``` 201 | 202 | ### 3. Has Many Relationship 203 | 204 | **Example: Author has many Books** 205 | 206 | ```bash 207 | > Enter field name: author_id 208 | > Enter field type: integer 209 | > Is this a foreign key? Yes 210 | > Enter related model name: Author 211 | > Select relationship type: belongsTo 212 | > Select inverse relationship type: hasMany 213 | ``` 214 | 215 | **Generated Code:** 216 | 217 | ```php 218 | // In Book.php 219 | public function author(): \Illuminate\Database\Eloquent\Relations\BelongsTo 220 | { 221 | return $this->belongsTo(Author::class); 222 | } 223 | 224 | // In Author.php 225 | public function books(): \Illuminate\Database\Eloquent\Relations\HasMany 226 | { 227 | return $this->hasMany(Book::class); 228 | } 229 | ``` 230 | 231 | ### 4. Belongs To Many Relationship 232 | 233 | **Example: Post has many Tags (and vice versa)** 234 | 235 | ```bash 236 | > Add relationship? Yes 237 | > Enter related model name: Tag 238 | > Select relationship type: belongsToMany 239 | > Customize pivot table name? No 240 | > Add timestamps to pivot? Yes 241 | ``` 242 | 243 | **Generated Code:** 244 | 245 | ```php 246 | // In Post.php 247 | public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany 248 | { 249 | return $this->belongsToMany(Tag::class) 250 | ->withTimestamps(); 251 | } 252 | 253 | // In Tag.php 254 | public function posts(): \Illuminate\Database\Eloquent\Relations\BelongsToMany 255 | { 256 | return $this->belongsToMany(Post::class) 257 | ->withTimestamps(); 258 | } 259 | ``` 260 | 261 | ### 5. Has One Through Relationship 262 | 263 | **Example: Supplier has one Account through User** 264 | 265 | ```bash 266 | > Add relationship? Yes 267 | > Select relationship type: hasOneThrough 268 | > Enter related model name: Account 269 | > Enter intermediate model name: User 270 | > Enter foreign key on intermediate model: supplier_id 271 | > Enter foreign key on target model: user_id 272 | ``` 273 | 274 | **Generated Code:** 275 | 276 | ```php 277 | // In Supplier.php 278 | public function account(): \Illuminate\Database\Eloquent\Relations\HasOneThrough 279 | { 280 | return $this->hasOneThrough( 281 | Account::class, 282 | User::class, 283 | 'supplier_id', // Foreign key on User table... 284 | 'user_id', // Foreign key on Account table... 285 | 'id', // Local key on Supplier table... 286 | 'id' // Local key on User table... 287 | ); 288 | } 289 | ``` 290 | 291 | ### 6. Has Many Through Relationship 292 | 293 | **Example: Country has many Patients through Hospitals** 294 | 295 | ```bash 296 | > Add relationship? Yes 297 | > Select relationship type: hasManyThrough 298 | > Enter related model name: Patient 299 | > Enter intermediate model name: Hospital 300 | > Enter foreign key on intermediate model: country_id 301 | > Enter foreign key on target model: hospital_id 302 | ``` 303 | 304 | **Generated Code:** 305 | 306 | ```php 307 | // In Country.php 308 | public function patients(): \Illuminate\Database\Eloquent\Relations\HasManyThrough 309 | { 310 | return $this->hasManyThrough( 311 | Patient::class, 312 | Hospital::class, 313 | 'country_id', // Foreign key on Hospital table... 314 | 'hospital_id', // Foreign key on Patient table... 315 | 'id', // Local key on Country table... 316 | 'id' // Local key on Hospital table... 317 | ); 318 | } 319 | ``` 320 | 321 | ### 7. Polymorphic Relationships 322 | 323 | **Example: Image morphTo multiple models (Post, User)** 324 | 325 | ```bash 326 | > Enter field name: imageable_id 327 | > Enter field name: imageable_type 328 | > Select relationship type: morphTo 329 | > Enter polymorphic name: imageable 330 | ``` 331 | 332 | **Generated Code:** 333 | 334 | ```php 335 | // In Image.php 336 | public function imageable(): \Illuminate\Database\Eloquent\Relations\MorphTo 337 | { 338 | return $this->morphTo(); 339 | } 340 | 341 | // In Post.php (when creating the Post model) 342 | public function image(): \Illuminate\Database\Eloquent\Relations\MorphOne 343 | { 344 | return $this->morphOne(Image::class, 'imageable'); 345 | } 346 | 347 | // In User.php (when creating the User model) 348 | public function image(): \Illuminate\Database\Eloquent\Relations\MorphOne 349 | { 350 | return $this->morphOne(Image::class, 'imageable'); 351 | } 352 | ``` 353 | 354 | ## Working with Existing Models 355 | 356 | RapidsModels integrates seamlessly with existing Laravel projects: 357 | 358 | ### Adding Fields to Existing Models 359 | 360 | When running `rapids:model` on an existing model name: 361 | 362 | ```bash 363 | php artisan rapids:model Product 364 | ``` 365 | 366 | The system will detect the existing model and offer options: 367 | 368 | 1. **Add a new migration for the existing model**: Create a migration to add fields to an existing table 369 | 2. **Update the existing model file**: Add relationships or methods to the model class 370 | 3. **Generate additional components**: Create missing factory or seeder files 371 | 372 | ### Example: Adding a Relationship to an Existing Model 373 | 374 | ```bash 375 | php artisan rapids:model Product 376 | > Model Product already exists. 377 | > What would you like to do? Update existing model file 378 | > Add relationship? Yes 379 | > Enter related model name: Supplier 380 | > Select relationship type: belongsTo 381 | > Create migration for foreign key? Yes 382 | ``` 383 | 384 | This will: 385 | 386 | 1. Create a migration to add the supplier_id field 387 | 2. Add the relationship method to your Product model 388 | 3. Add the inverse relationship method to your Supplier model 389 | 390 | ## PHP Compatibility 391 | 392 | RapidsModels is compatible with: 393 | 394 | - PHP 8.2 395 | - PHP 8.3 396 | - PHP 8.4 397 | 398 | The package takes advantage of modern PHP features including: 399 | 400 | - Readonly classes and properties 401 | - Constructor property promotion 402 | - Match expressions 403 | - Return type declarations 404 | - Named arguments 405 | 406 | ## Contributing 407 | 408 | Contributions are welcome! Here's how you can help: 409 | 410 | 1. Fork the repository 411 | 2. Create a feature branch: `git checkout -b feature/amazing-feature` 412 | 3. Commit your changes: `git commit -m 'Add some amazing feature'` 413 | 4. Push to the branch: `git push origin feature/amazing-feature` 414 | 5. Open a Pull Request 415 | 416 | ### Development Setup 417 | 418 | ```bash 419 | # Clone the repository 420 | git clone https://github.com/Tresor-Kasenda/rapids-models.git 421 | 422 | # Install dependencies 423 | composer install 424 | 425 | # Run tests 426 | ./vendor/bin/phpunit 427 | ``` 428 | 429 | ## Support 430 | 431 | If you find RapidsModels useful in your projects, consider supporting development: 432 | 433 | - **Star the repository** on GitHub 434 | - **Share your experience** on social media using #RapidsModels 435 | - **Donate** via [GitHub Sponsors](https://github.com/sponsors/Tresor-Kasenda) 436 | - **Hire me** for your Laravel projects 437 | 438 | ## License 439 | 440 | RapidsModels is open-source software licensed under the [MIT license](LICENSE.md). 441 | 442 | --- 443 | 444 | Made with ❤️ by [Tresor Kasenda](https://github.com/Tresor-Kasenda) 445 | -------------------------------------------------------------------------------- /src/Console/RapidsModels.php: -------------------------------------------------------------------------------- 1 | pathinfo($file, PATHINFO_FILENAME), 49 | glob($modelPath.'/*.php') 50 | ); 51 | 52 | $availableModels = array_filter($modelFiles, fn ($model) => class_exists("App\\Models\\{$model}")); 53 | 54 | $modelName = $this->argument('name') ?? text( 55 | label: 'Enter model name (without "App\\Models\\")', 56 | placeholder: 'e.g. User, Post, Product', 57 | required: true, 58 | validate: fn (string $value) => match (true) { 59 | mb_strlen($value) < 2 => 'The model name must be at least 2 characters.', 60 | ! preg_match('/^[A-Za-z]+$/', $value) => 'The model name must contain only letters.', 61 | default => null 62 | } 63 | ); 64 | 65 | if (in_array($modelName, $availableModels)) { 66 | info("Model {$modelName} already exists."); 67 | 68 | $choice = search( 69 | label: 'What would you like to do?', 70 | options: fn () => [ 71 | 'new' => 'Enter a different model name', 72 | 'migration' => 'Add new migration for existing model', 73 | 'cancel' => 'Cancel operation' 74 | ] 75 | ); 76 | 77 | match ($choice) { 78 | 'new' => $this->call('rapids:model'), 79 | 'migration' => $this->handleExistingModel($modelName), 80 | 'cancel' => info('Operation cancelled.') 81 | }; 82 | 83 | return; 84 | } 85 | 86 | $this->modelName = ucfirst($modelName); 87 | 88 | // Check if fields flag is provided 89 | $fieldsJson = $this->option('fields'); 90 | if (!empty($fieldsJson)) { 91 | $this->handleModelCreationFromJson($fieldsJson); 92 | } else { 93 | $this->handleModelCreation(); 94 | } 95 | 96 | info('Running migrations...'); 97 | $this->generateFactory(); 98 | new SeederGenerator($this->modelName)->generateSeeder(); 99 | info('Model created successfully.'); 100 | } 101 | 102 | protected function handleExistingModel(bool|array|string $modelName): void 103 | { 104 | $this->modelName = $modelName; 105 | info("Adding new migration for {$modelName}"); 106 | 107 | $fields = new ModelFieldsGenerator($this->modelName)->generate(); 108 | 109 | foreach ($fields as $field => &$options) { 110 | $options['nullable'] = true; 111 | } 112 | 113 | unset($options); 114 | foreach ($fields as $field => $options) { 115 | if (str_ends_with($field, '_id')) { 116 | $relatedModelName = text( 117 | label: "Enter related model name for {$field}", 118 | placeholder: 'e.g. User for user_id', 119 | required: true 120 | ); 121 | 122 | $currentModelRelation = search( 123 | label: "Select relationship type for {$this->modelName} to {$relatedModelName}", 124 | options: fn () => [ 125 | 'belongsTo' => 'Belongs To', 126 | 'hasOne' => 'Has One', 127 | 'hasMany' => 'Has Many', 128 | 'belongsToMany' => 'Belongs To Many', 129 | 'hasOneThrough' => 'Has One Through', 130 | 'hasManyThrough' => 'Has Many Through', 131 | 'morphOne' => 'Morph One', 132 | 'morphMany' => 'Morph Many', 133 | 'morphTo' => 'Morph To', 134 | 'morphToMany' => 'Morph To Many', 135 | 'morphedByMany' => 'Morphed By Many', 136 | ], 137 | placeholder: 'Select relationship type' 138 | ); 139 | 140 | $inverseRelation = search( 141 | label: "Select inverse relationship type for {$relatedModelName} to {$this->modelName}", 142 | options: fn () => [ 143 | 'hasMany' => 'Has Many', 144 | 'hasOne' => 'Has One', 145 | 'belongsTo' => 'Belongs To', 146 | 'belongsToMany' => 'Belongs To Many', 147 | 'hasOneThrough' => 'Has One Through', 148 | 'hasManyThrough' => 'Has Many Through', 149 | 'morphOne' => 'Morph One', 150 | 'morphMany' => 'Morph Many', 151 | 'morphTo' => 'Morph To', 152 | 'morphToMany' => 'Morph To Many', 153 | 'morphedByMany' => 'Morphed By Many', 154 | 'none' => 'No inverse relation' 155 | ], 156 | placeholder: 'Select inverse relationship type' 157 | ); 158 | 159 | $this->addRelationToModel( 160 | $this->modelName, 161 | $relatedModelName, 162 | $currentModelRelation 163 | ); 164 | 165 | if ('none' !== $inverseRelation) { 166 | $this->addRelationToModel( 167 | $relatedModelName, 168 | $this->modelName, 169 | $inverseRelation 170 | ); 171 | } 172 | } 173 | } 174 | 175 | $migrationName = 'add_fields_to_'.Str::snake(Str::pluralStudly($modelName)).'_table'; 176 | $migrationFile = database_path("migrations/".date('Y_m_d_His_').$migrationName.'.php'); 177 | 178 | $stub = File::get(config('rapids.stubs.migration.alter')); 179 | $tableFields = new MigrationGenerator($this->modelName) 180 | ->generateMigrationFields($fields); 181 | 182 | $migrationContent = str_replace( 183 | ['{{ table }}', '{{ fields }}'], 184 | [Str::snake(Str::pluralStudly($modelName)), $tableFields], 185 | $stub 186 | ); 187 | 188 | File::put($migrationFile, $migrationContent); 189 | info('Migration created successfully.'); 190 | $this->call('migrate'); 191 | } 192 | 193 | protected function addRelationToModel(string $modelName, string $relatedModelName, string $relationType): void 194 | { 195 | $modelPath = app_path("Models/{$modelName}.php"); 196 | 197 | if ( ! File::exists($modelPath)) { 198 | info("Model file not found: {$modelPath}"); 199 | return; 200 | } 201 | 202 | $content = File::get($modelPath); 203 | 204 | $methodName = Str::camel(Str::singular($relatedModelName)); 205 | 206 | $relationShips = new RelationshipGeneration($this->modelName); 207 | 208 | $relationMethod = $relationShips->relationGeneration( 209 | $relationType, 210 | $methodName, 211 | (array)$relatedModelName 212 | ); 213 | 214 | if (empty($relationMethod)) { 215 | info("Invalid relationship type: {$relationType}"); 216 | return; 217 | } 218 | 219 | if (str_contains($content, "function {$methodName}(")) { 220 | info("Relation method {$methodName}() already exists in {$modelName} model"); 221 | return; 222 | } 223 | 224 | $content = preg_replace('/}(\s*)$/', "\n {$relationMethod}\n}", $content); 225 | 226 | File::put($modelPath, $content); 227 | info("Added {$relationType} relation from {$modelName} to {$relatedModelName}"); 228 | } 229 | 230 | protected function handleModelCreation(): void 231 | { 232 | $fields = new ModelFieldsGenerator($this->modelName); 233 | 234 | $fileSystem = new LaravelFileSystem(); 235 | $relationshipService = new LaravelRelationshipService(); 236 | $promptService = new PromptService(); 237 | 238 | $modelGeneration = new ModelGenerator( 239 | $fileSystem, 240 | $relationshipService, 241 | $promptService 242 | ); 243 | 244 | $generatedFields = $fields->generate(); 245 | $this->selectedFields = $generatedFields; // Store for later use in factory generation 246 | 247 | $useSoftDeletes = confirm( 248 | label: 'Would you like to add soft delete functionality?', 249 | default: false 250 | ); 251 | 252 | // Ajouter un log pour vérifier la valeur 253 | info("SoftDelete choisi par l'utilisateur: " . ($useSoftDeletes ? 'Oui' : 'Non')); 254 | 255 | $modelDefinition = new ModelDefinition( 256 | $this->modelName, 257 | $generatedFields, 258 | $this->relationFields, 259 | true, // useFillable, valeur par défaut 260 | $useSoftDeletes // Nous passons le choix de SoftDelete 261 | ); 262 | 263 | // Vérifier que la valeur est correctement enregistrée dans l'objet 264 | info("SoftDelete stocké dans ModelDefinition: " . ($modelDefinition->useSoftDeletes() ? 'Oui' : 'Non')); 265 | 266 | $modelGeneration->generateModel($modelDefinition); 267 | 268 | new MigrationGenerator($this->modelName)->generateMigration($generatedFields, $useSoftDeletes); 269 | } 270 | 271 | /** 272 | * Handle model creation from JSON input 273 | * 274 | * @param string $fieldsJson JSON string containing field definitions 275 | * @return void 276 | */ 277 | protected function handleModelCreationFromJson(string $fieldsJson): void 278 | { 279 | try { 280 | $fieldsData = json_decode($fieldsJson, true, 512, JSON_THROW_ON_ERROR); 281 | if (!is_array($fieldsData)) { 282 | throw new \Exception("Invalid JSON format for fields"); 283 | } 284 | 285 | // Process fields 286 | $processedFields = []; 287 | $this->relationFields = []; 288 | 289 | foreach ($fieldsData as $fieldName => $fieldConfig) { 290 | // If field is a relation 291 | if (isset($fieldConfig['relation'])) { 292 | $relationType = $fieldConfig['relation']['type'] ?? 'belongsTo'; 293 | $relatedModel = $fieldConfig['relation']['model'] ?? null; 294 | $inverseRelation = $fieldConfig['relation']['inverse'] ?? 'none'; 295 | 296 | if ($relatedModel) { 297 | // Add relation to the model 298 | $this->relationFields[$fieldName] = [ 299 | 'type' => $relationType, 300 | 'model' => $relatedModel, 301 | ]; 302 | 303 | // If field is a foreign key, add it to fields 304 | if ($relationType === 'belongsTo') { 305 | $processedFields[Str::snake($fieldName) . '_id'] = [ 306 | 'type' => 'foreignId', 307 | 'nullable' => $fieldConfig['nullable'] ?? false, 308 | ]; 309 | } 310 | 311 | // Add inverse relation if specified 312 | if ($inverseRelation !== 'none') { 313 | $this->addRelationToModel( 314 | $relatedModel, 315 | $this->modelName, 316 | $inverseRelation 317 | ); 318 | } 319 | } 320 | } else { 321 | // Regular field 322 | $processedFields[$fieldName] = [ 323 | 'type' => $fieldConfig['type'] ?? 'string', 324 | 'nullable' => $fieldConfig['nullable'] ?? false, 325 | ]; 326 | 327 | // Add additional properties if provided 328 | if (isset($fieldConfig['default'])) { 329 | $processedFields[$fieldName]['default'] = $fieldConfig['default']; 330 | } 331 | 332 | if (isset($fieldConfig['length'])) { 333 | $processedFields[$fieldName]['length'] = $fieldConfig['length']; 334 | } 335 | 336 | if (isset($fieldConfig['values']) && $fieldConfig['type'] === 'enum') { 337 | $processedFields[$fieldName]['values'] = $fieldConfig['values']; 338 | } 339 | } 340 | } 341 | 342 | $this->selectedFields = $processedFields; 343 | 344 | // Determine if soft deletes should be used 345 | $useSoftDeletes = $fieldsData['_config']['softDeletes'] ?? false; 346 | 347 | // Create the model and migration 348 | $fileSystem = new LaravelFileSystem(); 349 | $relationshipService = new LaravelRelationshipService(); 350 | $promptService = new PromptService(); 351 | 352 | $modelGeneration = new ModelGenerator( 353 | $fileSystem, 354 | $relationshipService, 355 | $promptService 356 | ); 357 | 358 | $modelDefinition = new ModelDefinition( 359 | $this->modelName, 360 | $processedFields, 361 | $this->relationFields, 362 | true, // useFillable, default value 363 | $useSoftDeletes 364 | ); 365 | 366 | $modelGeneration->generateModel($modelDefinition); 367 | 368 | // Generate the migration 369 | new MigrationGenerator($this->modelName)->generateMigration($processedFields, $useSoftDeletes); 370 | 371 | info("Model {$this->modelName} created successfully from JSON input"); 372 | 373 | } catch (\Exception $e) { 374 | $this->error("Error processing JSON input: " . $e->getMessage()); 375 | $this->handleModelCreation(); // Fall back to interactive mode 376 | } 377 | } 378 | 379 | /** 380 | * @throws FileNotFoundException 381 | */ 382 | protected function generateFactory(): void 383 | { 384 | $factories = new FactoryGenerator( 385 | $this->modelName, 386 | $this->selectedFields, 387 | $this->relationFields 388 | ); 389 | $factories->generateFactory(); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /docs/model.md: -------------------------------------------------------------------------------- 1 | # RapidsModels Documentation 2 | 3 | ## Overview 4 | 5 | RapidsModels is a powerful Laravel package that streamlines model creation by generating a complete model ecosystem with 6 | a single command. Instead of creating models, migrations, factories, and seeders separately, RapidsModels handles 7 | everything through an interactive process. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | composer require rapids/rapids 13 | ``` 14 | 15 | ## Basic Usage 16 | 17 | ```bash 18 | php artisan rapids:model Product 19 | ``` 20 | 21 | ## Traditional vs. RapidsModels Approach 22 | 23 | ``` 24 | Traditional Approach: 25 | ┌──────────────────────────┬─────────────────────────────────────────────────┐ 26 | │ Step │ Command │ 27 | ├──────────────────────────┼─────────────────────────────────────────────────┤ 28 | │ 1. Create model │ php artisan make:model Product │ 29 | │ 2. Create migration │ php artisan make:migration create_products_table│ 30 | │ 3. Create factory │ php artisan make:factory ProductFactory │ 31 | │ 4. Create seeder │ php artisan make:seeder ProductSeeder │ 32 | │ 5. Define fields manually│ Edit migration file │ 33 | │ 6. Set up relationships │ Edit model files │ 34 | └──────────────────────────┴─────────────────────────────────────────────────┘ 35 | 36 | RapidsModels Approach: 37 | ┌──────────────────────────┬──────────────────────────────────────────────┐ 38 | │ Step │ Command │ 39 | ├──────────────────────────┼──────────────────────────────────────────────┤ 40 | │ Complete model ecosystem │ php artisan rapids:model Product │ 41 | │ with interactive setup │ (follow prompts for fields and relationships)│ 42 | └──────────────────────────┴──────────────────────────────────────────────┘ 43 | ``` 44 | 45 | ## PHP Compatibility 46 | 47 | RapidsModels now fully supports: 48 | - PHP 8.2 49 | - PHP 8.3 50 | - PHP 8.4 51 | 52 | And takes advantage of modern PHP features like: 53 | - Readonly classes and properties 54 | - Constructor property promotion 55 | - Match expressions 56 | - Return type declarations 57 | - Named arguments 58 | 59 | ## Field Types Reference 60 | 61 | ``` 62 | Field Type Options: 63 | ┌────────────┬──────────────────────────┬───────────────────────────┐ 64 | │ Type │ Description │ Example Usage │ 65 | ├────────────┼──────────────────────────┼───────────────────────────┤ 66 | │ string │ Text data │ name, title, description │ 67 | │ text │ Longer text data │ content, bio, details │ 68 | │ integer │ Whole numbers │ count, position, age │ 69 | │ decimal │ Numbers with decimals │ price, weight, rating │ 70 | │ boolean │ True/false values │ is_active, has_discount │ 71 | │ date │ Date without time │ birth_date, release_date │ 72 | │ datetime │ Date with time │ starts_at, expires_at │ 73 | │ enum │ Predefined options │ status, role, type │ 74 | │ json │ JSON data │ settings, preferences │ 75 | │ uuid │ UUID identifiers │ adds HasUuids trait │ 76 | └────────────┴──────────────────────────┴───────────────────────────┘ 77 | ``` 78 | 79 | ## Supported Laravel Relations 80 | 81 | RapidsModels now supports ALL Laravel relationship types: 82 | 83 | ``` 84 | Relationship Types: 85 | ┌──────────────┬────────────────────────────────────────────┐ 86 | │ Type │ Description │ 87 | ├──────────────┼────────────────────────────────────────────┤ 88 | │ hasOne │ One-to-one relation │ 89 | │ belongsTo │ Inverse of hasOne or hasMany │ 90 | │ hasMany │ One-to-many relation │ 91 | │ belongsToMany│ Many-to-many relation │ 92 | │ hasOneThrough│ One-to-one relation through another model │ 93 | │ hasManyThrough│ One-to-many relation through another model│ 94 | │ morphOne │ One-to-one polymorphic relation │ 95 | │ morphMany │ One-to-many polymorphic relation │ 96 | │ morphTo │ Inverse of morphOne or morphMany │ 97 | │ morphToMany │ Many-to-many polymorphic relation │ 98 | │ morphedByMany│ Inverse of morphToMany │ 99 | └──────────────┴────────────────────────────────────────────┘ 100 | ``` 101 | 102 | ## Working with Field Types 103 | 104 | ### String Fields 105 | 106 | ```bash 107 | > Enter field name: title 108 | > Enter field type: string 109 | > Field is nullable? No 110 | ``` 111 | 112 | Creates: `$table->string('title');` 113 | 114 | ### Text Fields 115 | 116 | ```bash 117 | > Enter field name: description 118 | > Enter field type: text 119 | > Field is nullable? No 120 | ``` 121 | 122 | Creates: `$table->text('description');` 123 | 124 | ### Integer Fields 125 | 126 | ```bash 127 | > Enter field name: quantity 128 | > Enter field type: integer 129 | > Field is nullable? No 130 | ``` 131 | 132 | Creates: `$table->integer('quantity');` 133 | 134 | ### Decimal Fields 135 | 136 | ```bash 137 | > Enter field name: price 138 | > Enter field type: decimal 139 | > Field is nullable? No 140 | ``` 141 | 142 | Creates: `$table->decimal('price');` 143 | 144 | ### Boolean Fields 145 | 146 | ```bash 147 | > Enter field name: is_featured 148 | > Enter field type: boolean 149 | > Field is nullable? No 150 | ``` 151 | 152 | Creates: `$table->boolean('is_featured');` 153 | 154 | ### Date Fields 155 | 156 | ```bash 157 | > Enter field name: publication_date 158 | > Enter field type: date 159 | > Field is nullable? Yes 160 | ``` 161 | 162 | Creates: `$table->date('publication_date')->nullable();` 163 | 164 | ### DateTime Fields 165 | 166 | ```bash 167 | > Enter field name: expires_at 168 | > Enter field type: datetime 169 | > Field is nullable? Yes 170 | ``` 171 | 172 | Creates: `$table->datetime('expires_at')->nullable();` 173 | 174 | ### Enum Fields 175 | 176 | ```bash 177 | > Enter field name: status 178 | > Enter field type: enum 179 | > Enter values (comma separated): draft,published,archived 180 | > Field is nullable? No 181 | ``` 182 | 183 | Creates: `$table->enum('status', ['draft', 'published', 'archived'])->default('draft');` 184 | 185 | ### JSON Fields 186 | 187 | ```bash 188 | > Enter field name: settings 189 | > Enter field type: json 190 | > Field is nullable? Yes 191 | ``` 192 | 193 | Creates: `$table->json('settings')->nullable();` 194 | 195 | ### UUID Fields 196 | 197 | ```bash 198 | > Enter field name: uuid 199 | > Enter field type: uuid 200 | > Field is nullable? No 201 | ``` 202 | 203 | Creates: `$table->uuid('uuid');` and adds `use Illuminate\Database\Eloquent\Concerns\HasUuids;` to the model. 204 | 205 | ## Relationship Examples - New Advanced Features 206 | 207 | ### 1. Has One Through Relationship 208 | 209 | ``` 210 | ┌────────────┬──────────────┬────────────┬────────────┐ 211 | │ From Table │ Relation Type│ To Table │ Via Model │ 212 | ├────────────┼──────────────┼────────────┼────────────┤ 213 | │ Supplier │ hasOneThrough│ Account │ User │ 214 | └────────────┴──────────────┴────────────┴────────────┘ 215 | ``` 216 | 217 | **How to create:** 218 | 219 | ```bash 220 | php artisan rapids:model Supplier 221 | 222 | # When prompted: 223 | > Add relationship? Yes 224 | > Select relationship type: hasOneThrough 225 | > Enter related model name: Account 226 | > Enter intermediate model name: User 227 | > Enter foreign key on intermediate model: supplier_id 228 | > Enter foreign key on target model: user_id 229 | ``` 230 | 231 | Generated code: 232 | 233 | ```php 234 | // In Supplier.php 235 | public function account(): \Illuminate\Database\Eloquent\Relations\HasOneThrough 236 | { 237 | return $this->hasOneThrough( 238 | Account::class, 239 | User::class, 240 | 'supplier_id', // Foreign key on User table... 241 | 'user_id', // Foreign key on Account table... 242 | 'id', // Local key on Supplier table... 243 | 'id' // Local key on User table... 244 | ); 245 | } 246 | ``` 247 | 248 | ### 2. Has Many Through Relationship (Enhanced) 249 | 250 | ``` 251 | ┌────────────┬──────────────┬────────────┬────────────┐ 252 | │ From Table │ Relation Type│ To Table │ Via Model │ 253 | ├────────────┼──────────────┼────────────┼────────────┤ 254 | │ Country │ hasManyThrough│ Post │ User │ 255 | └────────────┴──────────────┴────────────┴────────────┘ 256 | ``` 257 | 258 | **How to create:** 259 | 260 | ```bash 261 | php artisan rapids:model Country 262 | 263 | # When prompted: 264 | > Add relationship? Yes 265 | > Select relationship type: hasManyThrough 266 | > Enter related model name: Post 267 | > Enter intermediate model name: User 268 | > Enter foreign key on intermediate model: country_id 269 | > Enter foreign key on target model: user_id 270 | ``` 271 | 272 | Generated code: 273 | 274 | ```php 275 | // In Country.php 276 | public function posts(): \Illuminate\Database\Eloquent\Relations\HasManyThrough 277 | { 278 | return $this->hasManyThrough( 279 | Post::class, 280 | User::class, 281 | 'country_id', // Foreign key on User table... 282 | 'user_id', // Foreign key on Post table... 283 | 'id', // Local key on Country table... 284 | 'id' // Local key on User table... 285 | ); 286 | } 287 | ``` 288 | 289 | ### 3. Polymorphic Relationships (Enhanced Support) 290 | 291 | ``` 292 | ┌────────────┬──────────────┬────────────┬────────────┐ 293 | │ From Table │ Relation Type│ To Table │ Via Method │ 294 | ├────────────┼──────────────┼────────────┼────────────┤ 295 | │ Comment │ morphTo │ Commentable│ commentable│ 296 | │ Post │ morphMany │ Comment │ comments │ 297 | │ Video │ morphMany │ Comment │ comments │ 298 | └────────────┴──────────────┴────────────┴────────────┘ 299 | ``` 300 | 301 | **How to create:** 302 | 303 | ```bash 304 | php artisan rapids:model Comment 305 | 306 | # When prompted: 307 | > Enter field name: commentable_id 308 | > Enter field name: commentable_type 309 | > Select relationship type: morphTo 310 | > Enter polymorphic name: commentable 311 | ``` 312 | 313 | Generated code: 314 | 315 | ```php 316 | // In Comment.php 317 | public function commentable(): \Illuminate\Database\Eloquent\Relations\MorphTo 318 | { 319 | return $this->morphTo(); 320 | } 321 | 322 | // In Post.php (when creating Post model) 323 | public function comments(): \Illuminate\Database\Eloquent\Relations\MorphMany 324 | { 325 | return $this->morphMany(Comment::class, 'commentable'); 326 | } 327 | 328 | // In Video.php (when creating Video model) 329 | public function comments(): \Illuminate\Database\Eloquent\Relations\MorphMany 330 | { 331 | return $this->morphMany(Comment::class, 'commentable'); 332 | } 333 | ``` 334 | 335 | ### 4. Many-to-Many Polymorphic Relationships 336 | 337 | ``` 338 | ┌────────────┬──────────────┬────────────┬────────────┐ 339 | │ From Table │ Relation Type│ To Table │ Via Method │ 340 | ├────────────┼──────────────┼────────────┼────────────┤ 341 | │ Post │ morphToMany │ Tag │ tags │ 342 | │ Video │ morphToMany │ Tag │ tags │ 343 | │ Tag │ morphedByMany│ Post │ posts │ 344 | │ Tag │ morphedByMany│ Video │ videos │ 345 | └────────────┴──────────────┴────────────┴────────────┘ 346 | ``` 347 | 348 | **How to create:** 349 | 350 | ```bash 351 | php artisan rapids:model Post 352 | 353 | # When prompted: 354 | > Add relationship? Yes 355 | > Select relationship type: morphToMany 356 | > Enter related model name: Tag 357 | > Enter morph name: taggable 358 | > Add timestamps to pivot table? Yes 359 | ``` 360 | 361 | Generated code: 362 | 363 | ```php 364 | // In Post.php 365 | public function tags(): \Illuminate\Database\Eloquent\Relations\MorphToMany 366 | { 367 | return $this->morphToMany(Tag::class, 'taggable') 368 | ->withTimestamps(); 369 | } 370 | 371 | // In Tag.php (when creating or updating Tag model) 372 | > Add relationship? Yes 373 | > Select relationship type: morphedByMany 374 | > Enter related model name: Post 375 | > Enter morph name: taggable 376 | > Add timestamps to pivot table? Yes 377 | 378 | public function posts(): \Illuminate\Database\Eloquent\Relations\MorphedByMany 379 | { 380 | return $this->morphedByMany(Post::class, 'taggable') 381 | ->withTimestamps(); 382 | } 383 | ``` 384 | 385 | ## Working with Existing Models 386 | 387 | ### Adding Fields to Existing Models 388 | 389 | ```bash 390 | php artisan rapids:model Product 391 | 392 | # If Product exists: 393 | > Model Product already exists. 394 | > What would you like to do? 395 | > Add new migration for existing model 396 | ``` 397 | 398 | Adding a new field: 399 | 400 | ```bash 401 | # Selected "Add new migration for existing model" 402 | > Enter field name: discount 403 | > Enter field type: float 404 | > Field is nullable? Yes 405 | ``` 406 | 407 | This will generate a new migration to add the field to the existing table. 408 | 409 | ### Adding Relationships to Existing Models 410 | 411 | ```bash 412 | # Selected "Add new migration for existing model" 413 | > Enter field name: supplier_id 414 | > Enter related model name for supplier_id: Supplier 415 | > Select relationship type: belongsTo 416 | > Select inverse relationship type: hasMany 417 | ``` 418 | 419 | This will: 420 | 421 | 1. Create a migration to add the supplier_id field 422 | 2. Add the relationship method to your Product model 423 | 3. Add the inverse relationship method to your Supplier model 424 | 425 | ## Complete Workflow Examples 426 | 427 | ### Blog System Example 428 | 429 | ``` 430 | Full Blog System Workflow: 431 | ┌────────────────────┬─────────────────────────────────────────────────────┐ 432 | │ Step │ Process │ 433 | ├────────────────────┼─────────────────────────────────────────────────────┤ 434 | │ 1. Create User │ php artisan rapids:model User │ 435 | │ │ - Add name (string) │ 436 | │ │ - Add email (string) │ 437 | │ │ - Add password (string) │ 438 | ├────────────────────┼─────────────────────────────────────────────────────┤ 439 | │ 2. Create Category │ php artisan rapids:model Category │ 440 | │ │ - Add name (string) │ 441 | │ │ - Add slug (string) │ 442 | │ │ - Add description (text) │ 443 | ├────────────────────┼─────────────────────────────────────────────────────┤ 444 | │ 3. Create Post │ php artisan rapids:model Post │ 445 | │ │ - Add title (string) │ 446 | │ │ - Add content (text) │ 447 | │ │ - Add status (enum: draft,published,archived) │ 448 | │ │ - Add user_id (foreign key) │ 449 | │ │ - belongsTo User / hasMany Posts │ 450 | │ │ - Add category_id (foreign key) │ 451 | │ │ - belongsTo Category / hasMany Posts │ 452 | ├────────────────────┼─────────────────────────────────────────────────────┤ 453 | │ 4. Create Tag │ php artisan rapids:model Tag │ 454 | │ │ - Add name (string) │ 455 | │ │ - Add relationship to Post (belongsToMany) │ 456 | ├────────────────────┼─────────────────────────────────────────────────────┤ 457 | │ 5. Create Comment │ php artisan rapids:model Comment │ 458 | │ │ - Add content (text) │ 459 | │ │ - Add user_id (foreign key) │ 460 | │ │ - belongsTo User / hasMany Comments │ 461 | │ │ - Add commentable_id & commentable_type │ 462 | │ │ - morphTo commentable │ 463 | └────────────────────┴─────────────────────────────────────────────────────┘ 464 | ``` 465 | 466 | ### Resulting Relationships 467 | 468 | ``` 469 | Table Relationships: 470 | ┌────────────┬──────────────┬────────────┬────────────┐ 471 | │ From Table │ Relation Type│ To Table │ Via Method │ 472 | ├────────────┼──────────────┼────────────┼────────────┤ 473 | │ Post │ belongsTo │ User │ user │ 474 | │ User │ hasMany │ Post │ posts │ 475 | │ Post │ belongsTo │ Category │ category │ 476 | │ Category │ hasMany │ Post │ posts │ 477 | │ Post │ belongsToMany│ Tag │ tags │ 478 | │ Tag │ belongsToMany│ Post │ posts │ 479 | │ Comment │ belongsTo │ User │ user │ 480 | │ User │ hasMany │ Comment │ comments │ 481 | │ Comment │ morphTo │ Commentable│ commentable│ 482 | │ Post │ morphMany │ Comment │ comments │ 483 | └────────────┴──────────────┴────────────┴────────────┘ 484 | ``` 485 | 486 | ## Advanced Features 487 | 488 | ### Advanced Foreign Key Constraints 489 | 490 | When adding foreign key fields, you can specify the constraint type: 491 | 492 | ```bash 493 | > Enter field name: user_id 494 | > Enter related table name for user_id: users 495 | > Select constraint type for user_id: 496 | > cascade 497 | > restrict 498 | > nullify 499 | ``` 500 | 501 | This generates different foreign key constraints: 502 | 503 | - `cascade`: `->foreignId('user_id')->constrained('users')->cascadeOnDelete();` 504 | - `restrict`: `->foreignId('user_id')->constrained('users')->restrictOnDelete();` 505 | - `nullify`: `->foreignId('user_id')->constrained('users')->nullOnDelete();` 506 | 507 | ### Field Modifiers and Default Values 508 | 509 | You can add modifiers and default values to your fields: 510 | 511 | ```bash 512 | > Field is nullable? Yes 513 | > Field is unique? Yes 514 | > Add default value? Yes 515 | > Enter default value: user@example.com 516 | ``` 517 | 518 | Generates: `$table->string('email')->nullable()->unique()->default('user@example.com');` 519 | 520 | ## PHP 8.2+ Features 521 | 522 | RapidsModels now takes advantage of modern PHP features including: 523 | 524 | ```php 525 | // Readonly class for data integrity 526 | readonly class ModelDefinition 527 | { 528 | public function __construct( 529 | private string $name, 530 | private array $fields = [], 531 | private array $relations = [], 532 | private bool $useFillable = true, 533 | private bool $useSoftDelete = false, 534 | ) { 535 | } 536 | 537 | // Immutable methods returning new instances 538 | public function withFields(array $fields): self 539 | { 540 | return new self( 541 | $this->name, 542 | $fields, 543 | $this->relations, 544 | $this->useFillable, 545 | $this->useSoftDelete 546 | ); 547 | } 548 | 549 | // Rest of the class... 550 | } 551 | ``` 552 | 553 | ## Benefits of RapidsModels 554 | 555 | ``` 556 | Key Benefits: 557 | ┌─────────────────────┬───────────────────────────────────────────────────┐ 558 | │ Benefit │ Description │ 559 | ├─────────────────────┼───────────────────────────────────────────────────┤ 560 | │ Time Savings │ Reduces model setup time by 70-80% │ 561 | │ Consistency │ Ensures standard model structure and relationships │ 562 | │ Reduced Errors │ Prevents common mistakes in relationships │ 563 | │ Better Documentation│ Self-documented relationships and field types │ 564 | │ Easier Maintenance │ Standardized approach to model creation │ 565 | │ Automatic Testing │ Generated factories for testing all models │ 566 | │ Full Relation Support│ All Laravel relation types including polymorphic │ 567 | │ Modern PHP Support │ Takes advantage of PHP 8.2+ features │ 568 | └─────────────────────┴───────────────────────────────────────────────────┘ 569 | ``` 570 | 571 | ## Troubleshooting 572 | 573 | ### Common Issues and Solutions 574 | 575 | ``` 576 | Common Issues: 577 | ┌─────────────────────────────┬───────────────────────────────────────────┐ 578 | │ Issue │ Solution │ 579 | ├─────────────────────────────┼───────────────────────────────────────────┤ 580 | │ Migration already exists │ Use a unique name or timestamp │ 581 | │ Model already exists │ Choose "Add new migration" option │ 582 | │ Missing related models │ Create required models first │ 583 | │ Invalid field types │ Check supported types in documentation │ 584 | │ Relationship errors │ Ensure inverse relationships are correct │ 585 | │ PHP version compatibility │ Ensure PHP 8.2 or higher is installed │ 586 | └─────────────────────────────┴───────────────────────────────────────────┘ 587 | ``` 588 | 589 | ## Best Practices 590 | 591 | 1. **Create models in dependency order**: Create parent models before child models 592 | 2. **Use consistent naming**: Follow Laravel conventions for table and column names 593 | 3. **Add relationships as needed**: Don't overcomplicate your initial model 594 | 4. **Use meaningful field names**: Be descriptive about the data the field holds 595 | 5. **Document special relationships**: Add comments for complex relationships 596 | 6. **Take advantage of PHP 8.2+ features**: Use readonly classes for data integrity 597 | 7. **Use typed properties**: Always specify return types for clarity 598 | 599 | ## Conclusion 600 | 601 | RapidsModels transforms Laravel model creation from a multi-step process into a streamlined, interactive experience. By 602 | automating the creation of models, migrations, factories, and seeders with proper relationships, it significantly 603 | accelerates development while ensuring consistency across your application. 604 | 605 | With full support for all Laravel relationships and modern PHP 8.2+ features, RapidsModels provides a comprehensive 606 | solution for model creation and management in your Laravel projects. 607 | -------------------------------------------------------------------------------- /src/Relations/RelationshipGeneration.php: -------------------------------------------------------------------------------- 1 | generateMorphToMethod($methodName); 38 | continue; 39 | } 40 | 41 | $methods = $this->relationGeneration($relation, $methodName, $methods); 42 | } 43 | 44 | return implode("\n\n ", array_filter($methods)); 45 | } 46 | 47 | private function generateMorphToMethod(string $methodName): string 48 | { 49 | return "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\MorphTo\n". 50 | " {\n". 51 | " return \$this->morphTo();\n". 52 | " }"; 53 | } 54 | 55 | public function relationGeneration(mixed $relationType, string $methodName, mixed $modelOrMethods): array|string 56 | { 57 | // Handle different parameter patterns 58 | if (is_array($relationType) && isset($relationType['type'])) { 59 | // Called from generateRelationMethods with relation array 60 | $type = $relationType['type']; 61 | $model = $relationType['model']; 62 | $methods = $modelOrMethods; 63 | 64 | $methods[] = $this->generateRelationMethod($type, $methodName, $model); 65 | return $methods; 66 | } else { 67 | // Called from addRelationToRelatedModel with relation type string 68 | $type = $relationType; 69 | $model = is_array($modelOrMethods) && ! empty($modelOrMethods) ? $modelOrMethods[0] : ''; 70 | 71 | return $this->generateRelationMethod($type, $methodName, $model); 72 | } 73 | } 74 | 75 | private function generateRelationMethod(string $type, string $methodName, string $model): string 76 | { 77 | return match ($type) { 78 | 'hasOne' => $this->generateHasOneMethod($methodName, $model), 79 | 'belongsTo' => $this->generateBelongsToMethod($methodName, $model), 80 | 'belongsToMany' => $this->generateBelongsToManyMethod($methodName, $model), 81 | 'hasMany' => $this->generateHasManyMethod($methodName, $model), 82 | 'hasOneThrough' => $this->generateHasOneThroughMethod($methodName, $model), 83 | 'hasManyThrough' => $this->generateHasManyThroughMethod($methodName, $model), 84 | 'morphOne' => $this->generateMorphOneMethod($methodName, $model), 85 | 'morphMany' => $this->generateMorphManyMethod($methodName, $model), 86 | 'morphToMany' => $this->generateMorphToManyMethod($methodName, $model), 87 | 'morphedByMany' => $this->generateMorphedByManyMethod($methodName, $model), 88 | default => '' 89 | }; 90 | } 91 | 92 | private function generateHasOneMethod(string $methodName, string $model): string 93 | { 94 | // The foreign key is defined on the related model's table (handled by belongsTo) 95 | // Default foreign key: Str::snake($this->modelName) . '_id' 96 | // Default local key: 'id' 97 | return "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\HasOne\n". 98 | " {\n". 99 | " return \$this->hasOne({$model}::class); // Laravel defaults are usually sufficient\n". 100 | // " // Example with explicit keys: return \$this->hasOne({$model}::class, 'foreign_key', 'local_key');\n". 101 | " }"; 102 | } 103 | 104 | private function generateBelongsToMethod(string $methodName, string $model): string 105 | { 106 | // Derive the foreign key from the *related* model name (convention) 107 | // e.g., if $model is 'User', foreign key is 'user_id' 108 | // The method name ($methodName) often matches the singular related model, but we derive FK from the $model class name for consistency. 109 | $foreignKey = Str::snake(Str::singular(class_basename($model))).'_id'; 110 | 111 | // Default owner key: 'id' 112 | return "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\BelongsTo\n". 113 | " {\n". 114 | " return \$this->belongsTo({$model}::class, '{$foreignKey}'); // Laravel defaults owner key to 'id'\n". 115 | // " // Example with explicit keys: return \$this->belongsTo({$model}::class, 'foreign_key', 'owner_key');\n". 116 | " }"; 117 | } 118 | 119 | private function generateBelongsToManyMethod(string $methodName, string $model): string 120 | { 121 | // Generate the pivot table name in alphabetical order (Laravel convention) 122 | // Use class base names to avoid issues with namespaces if models are in different directories 123 | $model1Base = class_basename($this->modelName); 124 | $model2Base = class_basename($model); 125 | $table1 = Str::snake(Str::singular($model1Base)); 126 | $table2 = Str::snake(Str::singular($model2Base)); 127 | $pivotTableName = collect([$table1, $table2])->sort()->implode('_'); 128 | 129 | // Define foreign keys based on model base names 130 | $foreignKey = Str::snake(Str::singular($model1Base)).'_id'; 131 | $relatedKey = Str::snake(Str::singular($model2Base)).'_id'; 132 | 133 | $withTimestamps = confirm( 134 | label: "Add timestamps to the pivot table?", 135 | default: false 136 | ); 137 | 138 | // Create pivot table migration if it doesn't exist 139 | $this->createPivotTableMigration($pivotTableName, $foreignKey, $relatedKey, $this->modelName, $model, $withTimestamps); 140 | 141 | $code = "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\BelongsToMany\n". 142 | " {\n". 143 | " return \$this->belongsToMany(\n". 144 | " {$model}::class,\n". 145 | " '{$pivotTableName}',\n". 146 | " '{$foreignKey}',\n". 147 | " '{$relatedKey}'\n". 148 | " )"; 149 | 150 | if ($withTimestamps) { 151 | $code .= "\n ->withTimestamps()"; 152 | } 153 | 154 | $code .= ";\n }"; 155 | 156 | return $code; 157 | } 158 | 159 | private function createPivotTableMigration( 160 | string $pivotTable, 161 | string $foreignKey, 162 | string $relatedKey, 163 | string $model1, 164 | string $model2, 165 | bool $withTimestamps = false 166 | ): void { 167 | $migrationName = "create_{$pivotTable}_table"; 168 | $migrationPath = database_path("migrations/".date('Y_m_d_His_').$migrationName.'.php'); 169 | 170 | $existingMigrations = glob(database_path('migrations/*'.$migrationName.'.php')); 171 | if (!empty($existingMigrations)) { 172 | info("Pivot table migration already exists for {$pivotTable}"); 173 | return; 174 | } 175 | 176 | $hasAdditionalFields = confirm( 177 | label: "Would you like to add additional fields to the {$pivotTable} pivot table?", 178 | default: false 179 | ); 180 | 181 | $stub = File::get(config('rapids.stubs.migration.migration')); 182 | 183 | $fields = "\n"; 184 | $fields .= "\$table->foreignId('{$foreignKey}')->constrained()->cascadeOnDelete();\n"; 185 | $fields .= "\$table->foreignId('{$relatedKey}')->constrained()->cascadeOnDelete();\n"; 186 | 187 | if ($hasAdditionalFields) { 188 | $promptService = new LaravelPromptService(); 189 | $modelFieldsGenerator = new ModelFieldsGenerator($this->modelName, null, $promptService); 190 | $additionalFields = $modelFieldsGenerator->generate(); 191 | 192 | foreach ($additionalFields as $field => $options) { 193 | if (!str_ends_with($field, '_id')) { // Skip foreign keys as we already have them 194 | if ('enum' === $options['type']) { 195 | $values = array_map(fn ($value) => "'{$value}'", $options['values']); 196 | $fields .= "\$table->enum('{$field}', [".implode(', ', $values)."])"; 197 | if (!empty($options['values'])) { 198 | $fields .= "->default('{$options['values'][0]}')"; 199 | } 200 | } else { 201 | $fields .= "\$table->{$options['type']}('{$field}')"; 202 | } 203 | if ($options['nullable']) { 204 | $fields .= "->nullable()"; 205 | } 206 | $fields .= ";\n"; 207 | } 208 | } 209 | } 210 | 211 | if ($withTimestamps) { 212 | $fields .= "\$table->timestamps();\n"; 213 | } 214 | 215 | $fields .= "\n"; 216 | 217 | $content = str_replace( 218 | ['{{ table }}', '{{ fields }}'], 219 | [$pivotTable, $fields], 220 | $stub 221 | ); 222 | 223 | File::put($migrationPath, $content); 224 | info("Created pivot table migration for {$model1} and {$model2}"); 225 | } 226 | 227 | private function generateHasManyMethod(string $methodName, string $model): string 228 | { 229 | // The foreign key is defined on the related model's table (handled by belongsTo) 230 | // Default foreign key: Str::snake($this->modelName) . '_id' 231 | // Default local key: 'id' 232 | return "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\HasMany\n". 233 | " {\n". 234 | " return \$this->hasMany({$model}::class); // Laravel defaults are usually sufficient\n". 235 | // " // Example with explicit keys: return \$this->hasMany({$model}::class, 'foreign_key', 'local_key');\n". 236 | " }"; 237 | } 238 | 239 | private function generateHasOneThroughMethod(string $methodName, string $model): string 240 | { 241 | // Ask for the intermediate model name 242 | $intermediateModel = text( 243 | label: "Enter the intermediate model name for the HasOneThrough relationship", 244 | placeholder: 'e.g. Car for a Mechanic->Car->Owner relationship', 245 | required: true 246 | ); 247 | 248 | // Optionally ask for foreign keys if they're non-standard 249 | $foreignKey = text( 250 | label: "Enter the foreign key on the intermediate model (or leave empty for default)", 251 | placeholder: "e.g. mechanic_id" 252 | ); 253 | 254 | $secondForeignKey = text( 255 | label: "Enter the foreign key on the target model (or leave empty for default)", 256 | placeholder: "e.g. car_id" 257 | ); 258 | 259 | $localKey = text( 260 | label: "Enter the local key on this model (or leave empty for default)", 261 | placeholder: "e.g. id" 262 | ); 263 | 264 | $secondLocalKey = text( 265 | label: "Enter the local key on the intermediate model (or leave empty for default)", 266 | placeholder: "e.g. id" 267 | ); 268 | 269 | // Build the relationship method with proper parameters 270 | $code = "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\HasOneThrough\n". 271 | " {\n". 272 | " return \$this->hasOneThrough(\n". 273 | " {$model}::class,\n". 274 | " {$intermediateModel}::class"; 275 | 276 | // Add optional parameters if provided 277 | if ( ! empty($foreignKey) || ! empty($secondForeignKey) || ! empty($localKey) || ! empty($secondLocalKey)) { 278 | if ( ! empty($foreignKey)) { 279 | $code .= ",\n'{$foreignKey}'"; 280 | } else { 281 | $code .= ",\nnull"; 282 | } 283 | 284 | if ( ! empty($secondForeignKey)) { 285 | $code .= ",\n'{$secondForeignKey}'"; 286 | } else { 287 | $code .= ",\nnull"; 288 | } 289 | 290 | if ( ! empty($localKey)) { 291 | $code .= ",\n{$localKey}'"; 292 | } else { 293 | $code .= ",\nnull"; 294 | } 295 | 296 | if ( ! empty($secondLocalKey)) { 297 | $code .= ",\n'{$secondLocalKey}'"; 298 | } else { 299 | $code .= ",\nnull"; 300 | } 301 | } 302 | 303 | $code .= "\n);\n }"; 304 | 305 | return $code; 306 | } 307 | 308 | private function generateHasManyThroughMethod(string $methodName, string $model): string 309 | { 310 | // Ask for the intermediate model name 311 | $intermediateModel = text( 312 | label: "Enter the intermediate model name for the HasManyThrough relationship", 313 | placeholder: 'e.g. Country for a Continent->Country->User relationship', 314 | required: true 315 | ); 316 | 317 | // Optionally ask for foreign keys if they're non-standard 318 | $foreignKey = text( 319 | label: "Enter the foreign key on the intermediate model (or leave empty for default)", 320 | placeholder: "e.g. continent_id" 321 | ); 322 | 323 | $secondForeignKey = text( 324 | label: "Enter the foreign key on the target model (or leave empty for default)", 325 | placeholder: "e.g. country_id" 326 | ); 327 | 328 | $localKey = text( 329 | label: "Enter the local key on this model (or leave empty for default)", 330 | placeholder: "e.g. id" 331 | ); 332 | 333 | $secondLocalKey = text( 334 | label: "Enter the local key on the intermediate model (or leave empty for default)", 335 | placeholder: "e.g. id" 336 | ); 337 | 338 | // Build the relationship method with proper parameters 339 | $code = "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\HasManyThrough\n". 340 | " {\n". 341 | " return \$this->hasManyThrough(\n". 342 | " {$model}::class,\n". 343 | " {$intermediateModel}::class"; 344 | 345 | // Add optional parameters if provided 346 | if (!empty($foreignKey) || !empty($secondForeignKey) || !empty($localKey) || !empty($secondLocalKey)) { 347 | if (!empty($foreignKey)) { 348 | $code .= ",\n '{$foreignKey}'"; 349 | } else { 350 | $code .= ",\n null"; 351 | } 352 | 353 | if (!empty($secondForeignKey)) { 354 | $code .= ",\n '{$secondForeignKey}'"; 355 | } else { 356 | $code .= ",\n null"; 357 | } 358 | 359 | if (!empty($localKey)) { 360 | $code .= ",\n '{$localKey}'"; 361 | } else { 362 | $code .= ",\n null"; 363 | } 364 | 365 | if (!empty($secondLocalKey)) { 366 | $code .= ",\n '{$secondLocalKey}'"; 367 | } else { 368 | $code .= ",\n null"; 369 | } 370 | } 371 | 372 | $code .= "\n );\n }"; 373 | 374 | return $code; 375 | } 376 | 377 | private function generateMorphOneMethod(string $methodName, string $model): string 378 | { 379 | // Morph name derived from the *current* model name + 'able' 380 | $morphName = Str::snake(class_basename($this->modelName)).'able'; 381 | 382 | // Ask for a custom morph name 383 | $customMorphName = text( 384 | label: "Enter the morph name for the relationship (or leave empty for default)", 385 | placeholder: $morphName, 386 | default: $morphName, 387 | ); 388 | 389 | // Use custom name if provided 390 | $actualMorphName = !empty($customMorphName) ? $customMorphName : $morphName; 391 | 392 | return "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\MorphOne\n". 393 | " {\n". 394 | " return \$this->morphOne({$model}::class, '{$actualMorphName}');\n". 395 | " }"; 396 | } 397 | 398 | private function generateMorphManyMethod(string $methodName, string $model): string 399 | { 400 | // Morph name derived from the *current* model name + 'able' 401 | $morphName = Str::snake(class_basename($this->modelName)).'able'; 402 | 403 | // Ask for a custom morph name 404 | $customMorphName = text( 405 | label: "Enter the morph name for the relationship (or leave empty for default)", 406 | placeholder: $morphName, 407 | default: $morphName, 408 | ); 409 | 410 | // Use custom name if provided 411 | $actualMorphName = !empty($customMorphName) ? $customMorphName : $morphName; 412 | 413 | return "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\MorphMany\n". 414 | " {\n". 415 | " return \$this->morphMany({$model}::class, '{$actualMorphName}');\n". 416 | " }"; 417 | } 418 | 419 | private function generateMorphToManyMethod(string $methodName, string $model): string 420 | { 421 | // Default morph name derived from method name 422 | $morphName = Str::singular(Str::snake($methodName)).'able'; 423 | 424 | // Ask for a custom morph name 425 | $customMorphName = text( 426 | label: "Enter the morph name for the relationship (or leave empty for default)", 427 | placeholder: $morphName, 428 | default: $morphName, 429 | ); 430 | 431 | // Use custom name if provided 432 | $actualMorphName = !empty($customMorphName) ? $customMorphName : $morphName; 433 | 434 | // Ask if we should add timestamps 435 | $withTimestamps = confirm( 436 | label: "Add timestamps to pivot table?", 437 | default: false 438 | ); 439 | 440 | // Ask if we should add a custom pivot table name 441 | $defaultTable = "{$actualMorphName}s"; // typical default for morphToMany 442 | $customTable = confirm( 443 | label: "Customize pivot table name?", 444 | default: false 445 | ); 446 | 447 | $tableName = $defaultTable; 448 | if ($customTable) { 449 | $tableName = text( 450 | label: "Enter custom pivot table name", 451 | placeholder: $defaultTable, 452 | default: $defaultTable 453 | ); 454 | } 455 | 456 | $code = "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\MorphToMany\n". 457 | " {\n". 458 | " return \$this->morphToMany({$model}::class, '{$actualMorphName}'"; 459 | 460 | // Add optional table name if custom 461 | if ($customTable) { 462 | $code .= ", '{$tableName}'"; 463 | } 464 | 465 | $code .= ")"; 466 | 467 | // Append withTimestamps if requested 468 | if ($withTimestamps) { 469 | $code .= "\n ->withTimestamps()"; 470 | } 471 | 472 | // Close the method 473 | $code .= ";\n }"; 474 | 475 | return $code; 476 | } 477 | 478 | private function generateMorphedByManyMethod(string $methodName, string $model): string 479 | { 480 | // Default morph name derived from method name 481 | $morphName = Str::singular(Str::snake($methodName)).'able'; 482 | 483 | // Ask for a custom morph name 484 | $customMorphName = text( 485 | label: "Enter the morph name for the relationship (or leave empty for default)", 486 | placeholder: $morphName, 487 | default: $morphName, 488 | ); 489 | 490 | // Use custom name if provided 491 | $actualMorphName = !empty($customMorphName) ? $customMorphName : $morphName; 492 | 493 | // Ask if we should add timestamps 494 | $withTimestamps = confirm( 495 | label: "Add timestamps to pivot table?", 496 | default: false 497 | ); 498 | 499 | // Ask if we should add a custom pivot table name 500 | $defaultTable = "{$actualMorphName}s"; // typical default 501 | $customTable = confirm( 502 | label: "Customize pivot table name?", 503 | default: false 504 | ); 505 | 506 | $tableName = $defaultTable; 507 | if ($customTable) { 508 | $tableName = text( 509 | label: "Enter custom pivot table name", 510 | placeholder: $defaultTable, 511 | default: $defaultTable 512 | ); 513 | } 514 | 515 | $code = "public function {$methodName}(): \Illuminate\Database\Eloquent\Relations\MorphedByMany\n". 516 | " {\n". 517 | " return \$this->morphedByMany({$model}::class, '{$actualMorphName}'"; 518 | 519 | // Add optional table name if custom 520 | if ($customTable) { 521 | $code .= ", '{$tableName}'"; 522 | } 523 | 524 | $code .= ")"; 525 | 526 | // Append withTimestamps if requested 527 | if ($withTimestamps) { 528 | $code .= "\n ->withTimestamps()"; 529 | } 530 | 531 | // Close the method 532 | $code .= ";\n }"; 533 | 534 | return $code; 535 | } 536 | } 537 | --------------------------------------------------------------------------------