diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a0face4..ec57510bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. -------------- ### Added - Generate PHPDoc for Laravel 8.x factories [\#1074 / ahmed-aliraqi](https://github.com/barryvdh/laravel-ide-helper/pull/1074) +- Model hooks for adding custom information from external sources to model classes through the ModelsCommand [\#945 / wimski](https://github.com/barryvdh/laravel-ide-helper/pull/945) ### Fixed - Error when generating helper for invokable classes [\#1124 / standaniels](https://github.com/barryvdh/laravel-ide-helper/pull/1124) diff --git a/README.md b/README.md index 9938b0520..ded21c115 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Generation is done based on the files in your project, so they are always up-to- - [Usage](#usage) - [Automatic PHPDoc generation for Laravel Facades](#automatic-phpdoc-generation-for-laravel-facades) - [Automatic PHPDocs for models](#automatic-phpdocs-for-models) + - [Model Directories](#model-directories) + - [Ignore Models](#ignore-models) + - [Model Hooks](#model-hooks) - [Automatic PHPDocs generation for Laravel Fluent methods](#automatic-phpdocs-generation-for-laravel-fluent-methods) - [Auto-completion for factory builders](#auto-completion-for-factory-builders) - [PhpStorm Meta for Container instances](#phpstorm-meta-for-container-instances) @@ -175,6 +178,8 @@ With the `--write-mixin (-M)` option */ ``` +#### Model Directories + By default, models in `app/models` are scanned. The optional argument tells what models to use (also outside app/models). ```bash @@ -189,6 +194,8 @@ php artisan ide-helper:models --dir="path/to/models" --dir="app/src/Model" You can publish the config file (`php artisan vendor:publish`) and set the default directories. +#### Ignore Models + Models can be ignored using the `--ignore (-I)` option ```bash @@ -243,6 +250,42 @@ For those special cases, you can map them via the config `custom_db_types`. Exam ], ``` +#### Model Hooks + +If you need additional information on your model from sources that are not handled by default, you can hook in to the + generation process with model hooks to add extra information on the fly. + Simply create a class that implements `ModelHookInterface` and add it to the `model_hooks` array in the config: + + ```php +'model_hooks' => [ + MyCustomHook::class, +], +``` + +The `run` method will be called during generation for every model and receives the current running `ModelsCommand` and the current `Model`, e.g.: + +```php +class MyCustomHook implements ModelHookInterface +{ + public function run(ModelsCommand $command, Model $model): void + { + if (! $model instanceof MyModel) { + return; + } + + $command->setProperty('custom', 'string', true, false, 'My custom property'); + } +} +``` + +```php +/** + * MyModel + * + * @property integer $id + * @property-read string $custom +``` + ### Automatic PHPDocs generation for Laravel Fluent methods If you need PHPDocs support for Fluent methods in migration, for example diff --git a/config/ide-helper.php b/config/ide-helper.php index 620b95c28..f395a440d 100644 --- a/config/ide-helper.php +++ b/config/ide-helper.php @@ -144,6 +144,21 @@ ], + /* + |-------------------------------------------------------------------------- + | Models hooks + |-------------------------------------------------------------------------- + | + | Define which hook classes you want to run for models to add custom information + | + | Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface. + | + */ + + 'model_hooks' => array( + // App\Support\IdeHelper\MyModelHook::class + ), + /* |-------------------------------------------------------------------------- | Extra classes diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php index 6fd0ab03a..df929081a 100644 --- a/src/Console/ModelsCommand.php +++ b/src/Console/ModelsCommand.php @@ -11,6 +11,7 @@ namespace Barryvdh\LaravelIdeHelper\Console; +use Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface; use Barryvdh\Reflection\DocBlock; use Barryvdh\Reflection\DocBlock\Context; use Barryvdh\Reflection\DocBlock\Serializer as DocBlockSerializer; @@ -279,6 +280,9 @@ protected function generateDocs($loadModels, $ignore = '') $this->getSoftDeleteMethods($model); $this->getCollectionMethods($model); $this->getFactoryMethods($model); + + $this->runModelHooks($model); + $output .= $this->createPhpDocs($name); $ignore[] = $name; $this->nullableColumns = []; @@ -336,7 +340,7 @@ protected function loadModels() * * @param \Illuminate\Database\Eloquent\Model $model */ - protected function castPropertiesType($model) + public function castPropertiesType($model) { $casts = $model->getCasts(); foreach ($casts as $name => $type) { @@ -412,7 +416,7 @@ protected function getTypeOverride($type) * * @param \Illuminate\Database\Eloquent\Model $model */ - protected function getPropertiesFromTable($model) + public function getPropertiesFromTable($model) { $table = $model->getConnection()->getTablePrefix() . $model->getTable(); $schema = $model->getConnection()->getDoctrineSchemaManager(); @@ -509,7 +513,7 @@ protected function getPropertiesFromTable($model) /** * @param \Illuminate\Database\Eloquent\Model $model */ - protected function getPropertiesFromMethods($model) + public function getPropertiesFromMethods($model) { $methods = get_class_methods($model); if ($methods) { @@ -713,7 +717,7 @@ protected function isRelationNullable(string $relation, Relation $relationObj): * @param string|null $comment * @param bool $nullable */ - protected function setProperty($name, $type = null, $read = null, $write = null, $comment = '', $nullable = false) + public function setProperty($name, $type = null, $read = null, $write = null, $comment = '', $nullable = false) { if (!isset($this->properties[$name])) { $this->properties[$name] = []; @@ -737,7 +741,7 @@ protected function setProperty($name, $type = null, $read = null, $write = null, } } - protected function setMethod($name, $type = '', $arguments = []) + public function setMethod($name, $type = '', $arguments = []) { $methods = array_change_key_case($this->methods, CASE_LOWER); @@ -1314,4 +1318,26 @@ protected function getReflectionNamedType(ReflectionNamedType $paramType): strin return $parameterName; } + + /** + * @param \Illuminate\Database\Eloquent\Model $model + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \RuntimeException + */ + protected function runModelHooks($model): void + { + $hooks = $this->laravel['config']->get('ide-helper.model_hooks', array()); + + foreach ($hooks as $hook) { + $hookInstance = $this->laravel->make($hook); + + if (! $hookInstance instanceof ModelHookInterface) { + throw new \RuntimeException( + 'Your IDE helper model hook must implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface' + ); + } + + $hookInstance->run($this, $model); + } + } } diff --git a/src/Contracts/ModelHookInterface.php b/src/Contracts/ModelHookInterface.php new file mode 100644 index 000000000..52124704e --- /dev/null +++ b/src/Contracts/ModelHookInterface.php @@ -0,0 +1,11 @@ +setProperty('custom', 'string', true, false); + } +} diff --git a/tests/Console/ModelsCommand/ModelHooks/Models/Simple.php b/tests/Console/ModelsCommand/ModelHooks/Models/Simple.php new file mode 100644 index 000000000..5f09d8bff --- /dev/null +++ b/tests/Console/ModelsCommand/ModelHooks/Models/Simple.php @@ -0,0 +1,9 @@ +set('ide-helper', [ + 'model_locations' => [ + // This is calculated from the base_path() which points to + // vendor/orchestra/testbench-core/laravel + '/../../../../tests/Console/ModelsCommand/ModelHooks/Models', + ], + 'model_hooks' => [ + CustomProperty::class, + ], + ]); + } + + public function test(): void + { + $actualContent = null; + + $mockFilesystem = Mockery::mock(Filesystem::class); + $mockFilesystem + ->shouldReceive('get') + ->andReturn(file_get_contents(__DIR__ . '/Models/Simple.php')) + ->once(); + $mockFilesystem + ->shouldReceive('put') + ->with( + Mockery::any(), + Mockery::capture($actualContent) + ) + ->andReturn(1) // Simulate we wrote _something_ to the file + ->once(); + + $this->instance(Filesystem::class, $mockFilesystem); + + $command = $this->app->make(ModelsCommand::class); + + $tester = $this->runCommand($command, [ + '--write' => true, + ]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay()); + + $expectedContent = <<<'PHP' +assertSame($expectedContent, $actualContent); + } +}