From 237e0247a2913742be75148105cacdb7432c7d6f Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:27:38 -0500 Subject: [PATCH 01/15] extract ShowModelCommand functionality to separate class --- .../Database/Eloquent/ModelInfoExtractor.php | 379 ++++++++++++++++++ .../Database/ModelInfoExtractorTest.php | 188 +++++++++ 2 files changed, 567 insertions(+) create mode 100644 src/Illuminate/Database/Eloquent/ModelInfoExtractor.php create mode 100644 tests/Integration/Database/ModelInfoExtractorTest.php diff --git a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php b/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php new file mode 100644 index 000000000000..88374d65e4f1 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php @@ -0,0 +1,379 @@ + + */ + protected $relationMethods = [ + 'hasMany', + 'hasManyThrough', + 'hasOneThrough', + 'belongsToMany', + 'hasOne', + 'belongsTo', + 'morphOne', + 'morphTo', + 'morphMany', + 'morphToMany', + 'morphedByMany', + ]; + + /** + * @param \Illuminate\Contracts\Foundation\Application $app + */ + public function __construct($app) + { + $this->app = $app; + } + + /** + * Extract model details. + * + * @param class-string<\Illuminate\Database\Eloquent\Model>|string $model + * @param string|null $connection + * @return array{"class": class-string<\Illuminate\Database\Eloquent\Model>, database: string, table: string, policy: string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection} + * + * @throws BindingResolutionException + */ + public function handle($model, $connection = null) + { + $class = $this->qualifyModel($model); + + /** @var \Illuminate\Database\Eloquent\Model $model */ + $model = $this->app->make($class); + + if ($connection !== null) { + $model->setConnection($connection); + } + + return [ + 'class' => get_class($model), + 'database' => $model->getConnection()->getName(), + 'table' => $model->getConnection()->getTablePrefix().$model->getTable(), + 'policy' => $this->getPolicy($model), + 'attributes' => $this->getAttributes($model), + 'relations' => $this->getRelations($model), + 'events' => $this->getEvents($model), + 'observers' => $this->getObservers($model) + ]; + } + + /** + * Get the first policy associated with this model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return string|null + */ + protected function getPolicy($model) + { + $policy = Gate::getPolicyFor($model::class); + + return $policy ? $policy::class : null; + } + + /** + * Get the column attributes for the given model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Support\Collection> + */ + protected function getAttributes($model) + { + $connection = $model->getConnection(); + $schema = $connection->getSchemaBuilder(); + $table = $model->getTable(); + $columns = $schema->getColumns($table); + $indexes = $schema->getIndexes($table); + + return collect($columns) + ->map(fn ($column) => [ + 'name' => $column['name'], + 'type' => $column['type'], + 'increments' => $column['auto_increment'], + 'nullable' => $column['nullable'], + 'default' => $this->getColumnDefault($column, $model), + 'unique' => $this->columnIsUnique($column['name'], $indexes), + 'fillable' => $model->isFillable($column['name']), + 'hidden' => $this->attributeIsHidden($column['name'], $model), + 'appended' => null, + 'cast' => $this->getCastType($column['name'], $model), + ]) + ->merge($this->getVirtualAttributes($model, $columns)); + } + + /** + * Get the virtual (non-column) attributes for the given model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param array $columns + * @return \Illuminate\Support\Collection + */ + protected function getVirtualAttributes($model, $columns) + { + $class = new ReflectionClass($model); + + return collect($class->getMethods()) + ->reject( + fn (ReflectionMethod $method) => $method->isStatic() + || $method->isAbstract() + || $method->getDeclaringClass()->getName() === Model::class + ) + ->mapWithKeys(function (ReflectionMethod $method) use ($model) { + if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) { + return [Str::snake($matches[1]) => 'accessor']; + } elseif ($model->hasAttributeMutator($method->getName())) { + return [Str::snake($method->getName()) => 'attribute']; + } else { + return []; + } + }) + ->reject(fn ($cast, $name) => collect($columns)->contains('name', $name)) + ->map(fn ($cast, $name) => [ + 'name' => $name, + 'type' => null, + 'increments' => false, + 'nullable' => null, + 'default' => null, + 'unique' => null, + 'fillable' => $model->isFillable($name), + 'hidden' => $this->attributeIsHidden($name, $model), + 'appended' => $model->hasAppended($name), + 'cast' => $cast, + ]) + ->values(); + } + + /** + * Get the relations from the given model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Support\Collection + */ + protected function getRelations($model) + { + return collect(get_class_methods($model)) + ->map(fn ($method) => new ReflectionMethod($model, $method)) + ->reject( + fn (ReflectionMethod $method) => $method->isStatic() + || $method->isAbstract() + || $method->getDeclaringClass()->getName() === Model::class + || $method->getNumberOfParameters() > 0 + ) + ->filter(function (ReflectionMethod $method) { + if ($method->getReturnType() instanceof ReflectionNamedType + && is_subclass_of($method->getReturnType()->getName(), Relation::class)) { + return true; + } + + $file = new SplFileObject($method->getFileName()); + $file->seek($method->getStartLine() - 1); + $code = ''; + while ($file->key() < $method->getEndLine()) { + $code .= trim($file->current()); + $file->next(); + } + + return collect($this->relationMethods) + ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); + }) + ->map(function (ReflectionMethod $method) use ($model) { + $relation = $method->invoke($model); + + if (! $relation instanceof Relation) { + return null; + } + + return [ + 'name' => $method->getName(), + 'type' => Str::afterLast(get_class($relation), '\\'), + 'related' => get_class($relation->getRelated()), + ]; + }) + ->filter() + ->values(); + } + + /** + * Get the Events that the model dispatches. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Support\Collection + */ + protected function getEvents($model) + { + return collect($model->dispatchesEvents()) + ->map(fn (string $class, string $event) => [ + 'event' => $event, + 'class' => $class, + ])->values(); + } + + /** + * Get the Observers watching this model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Support\Collection + * @throws BindingResolutionException + */ + protected function getObservers($model) + { + $listeners = $this->app->make('events')->getRawListeners(); + + // Get the Eloquent observers for this model... + $listeners = array_filter($listeners, function ($v, $key) use ($model) { + return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class); + }, ARRAY_FILTER_USE_BOTH); + + // Format listeners Eloquent verb => Observer methods... + $extractVerb = function ($key) { + preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches); + + return $matches[1] ?? '?'; + }; + + $formatted = []; + + foreach ($listeners as $key => $observerMethods) { + $formatted[] = [ + 'event' => $extractVerb($key), + 'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods), + ]; + } + + return collect($formatted); + } + + /** + * Qualify the given model class base name. + * + * @param string $model + * @return class-string<\Illuminate\Database\Eloquent\Model> + * + * @see \Illuminate\Console\GeneratorCommand + */ + protected function qualifyModel(string $model) + { + if (str_contains($model, '\\') && class_exists($model)) { + return $model; + } + + $model = ltrim($model, '\\/'); + + $model = str_replace('/', '\\', $model); + + $rootNamespace = $this->app->getNamespace(); + + if (Str::startsWith($model, $rootNamespace)) { + return $model; + } + + return is_dir(app_path('Models')) + ? $rootNamespace.'Models\\'.$model + : $rootNamespace.$model; + } + + /** + * Get the cast type for the given column. + * + * @param string $column + * @param \Illuminate\Database\Eloquent\Model $model + * @return string|null + */ + protected function getCastType($column, $model) + { + if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) { + return 'accessor'; + } + + if ($model->hasAttributeMutator($column)) { + return 'attribute'; + } + + return $this->getCastsWithDates($model)->get($column) ?? null; + } + + /** + * Get the model casts, including any date casts. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Support\Collection + */ + protected function getCastsWithDates($model) + { + return collect($model->getDates()) + ->filter() + ->flip() + ->map(fn () => 'datetime') + ->merge($model->getCasts()); + } + + /** + * Get the default value for the given column. + * + * @param array $column + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed|null + */ + protected function getColumnDefault($column, $model) + { + $attributeDefault = $model->getAttributes()[$column['name']] ?? null; + + return enum_value($attributeDefault, $column['default']); + } + + /** + * Determine if the given attribute is hidden. + * + * @param string $attribute + * @param \Illuminate\Database\Eloquent\Model $model + * @return bool + */ + protected function attributeIsHidden($attribute, $model) + { + if (count($model->getHidden()) > 0) { + return in_array($attribute, $model->getHidden()); + } + + if (count($model->getVisible()) > 0) { + return ! in_array($attribute, $model->getVisible()); + } + + return false; + } + + /** + * Determine if the given attribute is unique. + * + * @param string $column + * @param array $indexes + * @return bool + */ + protected function columnIsUnique($column, $indexes) + { + return collect($indexes)->contains( + fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique'] + ); + } +} diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php new file mode 100644 index 000000000000..7495b84f094c --- /dev/null +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -0,0 +1,188 @@ +id(); + }); + Schema::create('test_model1', function (Blueprint $table) { + $table->increments('id'); + $table->uuid(); + $table->string('name'); + $table->boolean('a_bool'); + $table->foreignId('parent_model_id')->constrained(); + $table->timestamp('nullable_date')->nullable(); + $table->timestamps(); + }); + } + + public function test_extracts_model_data() + { + $extractor = new ModelInfoExtractor($this->app); + $modelInfo = $extractor->handle(TestModel1::class); + + $this->assertEquals(TestModel1::class, $modelInfo['class']); + $this->assertEquals('testing', $modelInfo['database']); + $this->assertEquals('test_model1', $modelInfo['table']); + $this->assertNull($modelInfo['policy']); + $this->assertCount(8, $modelInfo['attributes']); + + $this->assertEqualsCanonicalizing([ + "name" => "id", + "type" => "integer", + "increments" => true, + "nullable" => false, + "default" => null, + "unique" => true, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => null, + ], $modelInfo['attributes'][0]); + + $this->assertEqualsCanonicalizing([ + "name" => "uuid", + "type" => "varchar", + "increments" => false, + "nullable" => false, + "default" => null, + "unique" => false, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => null, + ], $modelInfo['attributes'][1]); + + $this->assertEqualsCanonicalizing([ + "name" => "name", + "type" => "varchar", + "increments" => false, + "nullable" => false, + "default" => null, + "unique" => false, + "fillable" => false, + "hidden" => false, + "appended" => null, + "cast" => null + ], $modelInfo['attributes'][2]); + + $this->assertEqualsCanonicalizing([ + "name" => "a_bool", + "type" => "tinyint(1)", + "increments" => false, + "nullable" => false, + "default" => null, + "unique" => false, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => "bool", + ], $modelInfo['attributes'][3]); + + $this->assertEqualsCanonicalizing([ + "name" => "parent_model_id", + "type" => "integer", + "increments" => false, + "nullable" => false, + "default" => null, + "unique" => false, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => null, + ], $modelInfo['attributes'][4]); + + $this->assertEqualsCanonicalizing([ + "name" => "nullable_date", + "type" => "datetime", + "increments" => false, + "nullable" => true, + "default" => null, + "unique" => false, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => "datetime", + ], $modelInfo['attributes'][5]); + + $this->assertEqualsCanonicalizing([ + "name" => "created_at", + "type" => "datetime", + "increments" => false, + "nullable" => true, + "default" => null, + "unique" => false, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => "datetime", + ], $modelInfo['attributes'][6]); + + $this->assertEqualsCanonicalizing([ + "name" => "updated_at", + "type" => "datetime", + "increments" => false, + "nullable" => true, + "default" => null, + "unique" => false, + "fillable" => true, + "hidden" => false, + "appended" => null, + "cast" => "datetime", + ], $modelInfo['attributes'][7]); + + $this->assertCount(1, $modelInfo['relations']); + $this->assertEqualsCanonicalizing([ + "name" => "parentModel", + "type" => "BelongsTo", + "related" => "Illuminate\Tests\Integration\Database\ParentTestModel", + ], $modelInfo['relations'][0]); + + $this->assertEmpty($modelInfo['events']); + $this->assertCount(1, $modelInfo['observers']); + $this->assertEquals('created', $modelInfo['observers'][0]['event']); + $this->assertCount(1, $modelInfo['observers'][0]['observer']); + $this->assertEquals("Illuminate\Tests\Integration\Database\TestModel1Observer@created", $modelInfo['observers'][0]['observer'][0]); + } +} + + +#[ObservedBy(TestModel1Observer::class)] +class TestModel1 extends Model +{ + use HasUuids; + + public $table = 'test_model1'; + protected $guarded = ['name']; + protected $casts = ['nullable_date' => 'datetime', 'a_bool' => 'bool']; + + public function parentModel(): BelongsTo + { + return $this->belongsTo(ParentTestModel::class); + } +} + +class ParentTestModel extends Model +{ + public $table = 'parent_model'; + public $timestamps = false; +} + +class TestModel1Observer +{ + public function created() + { + } +} From e4693776766ef56f678f46abb64007beb97f1761 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:38:28 -0500 Subject: [PATCH 02/15] leverage ModelInfoExtractor --- .../Database/Console/ShowModelCommand.php | 361 +----------------- 1 file changed, 10 insertions(+), 351 deletions(-) diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index bae4dab60f53..02ea3c8e937e 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -3,19 +3,10 @@ namespace Illuminate\Database\Console; use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Str; -use ReflectionClass; -use ReflectionMethod; -use ReflectionNamedType; -use SplFileObject; +use Illuminate\Database\Eloquent\ModelInfoExtractor; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Output\OutputInterface; -use function Illuminate\Support\enum_value; - #[AsCommand(name: 'model:show')] class ShowModelCommand extends DatabaseInspectionCommand { @@ -42,253 +33,33 @@ class ShowModelCommand extends DatabaseInspectionCommand {--database= : The database connection to use} {--json : Output the model as JSON}'; - /** - * The methods that can be called in a model to indicate a relation. - * - * @var array - */ - protected $relationMethods = [ - 'hasMany', - 'hasManyThrough', - 'hasOneThrough', - 'belongsToMany', - 'hasOne', - 'belongsTo', - 'morphOne', - 'morphTo', - 'morphMany', - 'morphToMany', - 'morphedByMany', - ]; - /** * Execute the console command. * * @return int */ - public function handle() + public function handle(ModelInfoExtractor $extractor) { - $class = $this->qualifyModel($this->argument('model')); - try { - $model = $this->laravel->make($class); - - $class = get_class($model); + $info = $extractor->handle($this->argument('model'), $this->option('database')); } catch (BindingResolutionException $e) { $this->components->error($e->getMessage()); return 1; } - if ($this->option('database')) { - $model->setConnection($this->option('database')); - } - - $this->display( - $class, - $model->getConnection()->getName(), - $model->getConnection()->getTablePrefix().$model->getTable(), - $this->getPolicy($model), - $this->getAttributes($model), - $this->getRelations($model), - $this->getEvents($model), - $this->getObservers($model), - ); + $this->display(...$info); return 0; } - /** - * Get the first policy associated with this model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return string - */ - protected function getPolicy($model) - { - $policy = Gate::getPolicyFor($model::class); - - return $policy ? $policy::class : null; - } - - /** - * Get the column attributes for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getAttributes($model) - { - $connection = $model->getConnection(); - $schema = $connection->getSchemaBuilder(); - $table = $model->getTable(); - $columns = $schema->getColumns($table); - $indexes = $schema->getIndexes($table); - - return collect($columns) - ->map(fn ($column) => [ - 'name' => $column['name'], - 'type' => $column['type'], - 'increments' => $column['auto_increment'], - 'nullable' => $column['nullable'], - 'default' => $this->getColumnDefault($column, $model), - 'unique' => $this->columnIsUnique($column['name'], $indexes), - 'fillable' => $model->isFillable($column['name']), - 'hidden' => $this->attributeIsHidden($column['name'], $model), - 'appended' => null, - 'cast' => $this->getCastType($column['name'], $model), - ]) - ->merge($this->getVirtualAttributes($model, $columns)); - } - - /** - * Get the virtual (non-column) attributes for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $columns - * @return \Illuminate\Support\Collection - */ - protected function getVirtualAttributes($model, $columns) - { - $class = new ReflectionClass($model); - - return collect($class->getMethods()) - ->reject( - fn (ReflectionMethod $method) => $method->isStatic() - || $method->isAbstract() - || $method->getDeclaringClass()->getName() === Model::class - ) - ->mapWithKeys(function (ReflectionMethod $method) use ($model) { - if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) { - return [Str::snake($matches[1]) => 'accessor']; - } elseif ($model->hasAttributeMutator($method->getName())) { - return [Str::snake($method->getName()) => 'attribute']; - } else { - return []; - } - }) - ->reject(fn ($cast, $name) => collect($columns)->contains('name', $name)) - ->map(fn ($cast, $name) => [ - 'name' => $name, - 'type' => null, - 'increments' => false, - 'nullable' => null, - 'default' => null, - 'unique' => null, - 'fillable' => $model->isFillable($name), - 'hidden' => $this->attributeIsHidden($name, $model), - 'appended' => $model->hasAppended($name), - 'cast' => $cast, - ]) - ->values(); - } - - /** - * Get the relations from the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getRelations($model) - { - return collect(get_class_methods($model)) - ->map(fn ($method) => new ReflectionMethod($model, $method)) - ->reject( - fn (ReflectionMethod $method) => $method->isStatic() - || $method->isAbstract() - || $method->getDeclaringClass()->getName() === Model::class - || $method->getNumberOfParameters() > 0 - ) - ->filter(function (ReflectionMethod $method) { - if ($method->getReturnType() instanceof ReflectionNamedType - && is_subclass_of($method->getReturnType()->getName(), Relation::class)) { - return true; - } - - $file = new SplFileObject($method->getFileName()); - $file->seek($method->getStartLine() - 1); - $code = ''; - while ($file->key() < $method->getEndLine()) { - $code .= trim($file->current()); - $file->next(); - } - - return collect($this->relationMethods) - ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); - }) - ->map(function (ReflectionMethod $method) use ($model) { - $relation = $method->invoke($model); - - if (! $relation instanceof Relation) { - return null; - } - - return [ - 'name' => $method->getName(), - 'type' => Str::afterLast(get_class($relation), '\\'), - 'related' => get_class($relation->getRelated()), - ]; - }) - ->filter() - ->values(); - } - - /** - * Get the Events that the model dispatches. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getEvents($model) - { - return collect($model->dispatchesEvents()) - ->map(fn (string $class, string $event) => [ - 'event' => $event, - 'class' => $class, - ])->values(); - } - - /** - * Get the Observers watching this model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getObservers($model) - { - $listeners = $this->getLaravel()->make('events')->getRawListeners(); - - // Get the Eloquent observers for this model... - $listeners = array_filter($listeners, function ($v, $key) use ($model) { - return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class); - }, ARRAY_FILTER_USE_BOTH); - - // Format listeners Eloquent verb => Observer methods... - $extractVerb = function ($key) { - preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches); - - return $matches[1] ?? '?'; - }; - - $formatted = []; - - foreach ($listeners as $key => $observerMethods) { - $formatted[] = [ - 'event' => $extractVerb($key), - 'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods), - ]; - } - - return collect($formatted); - } - /** * Render the model information. * - * @param string $class + * @param class-string<\Illuminate\Database\Eloquent\Model> $class * @param string $database * @param string $table - * @param string $policy + * @param class-string|null $policy * @param \Illuminate\Support\Collection $attributes * @param \Illuminate\Support\Collection $relations * @param \Illuminate\Support\Collection $events @@ -305,10 +76,10 @@ protected function display($class, $database, $table, $policy, $attributes, $rel /** * Render the model information as JSON. * - * @param string $class + * @param class-string<\Illuminate\Database\Eloquent\Model> $class * @param string $database * @param string $table - * @param string $policy + * @param class-string|null $policy * @param \Illuminate\Support\Collection $attributes * @param \Illuminate\Support\Collection $relations * @param \Illuminate\Support\Collection $events @@ -334,10 +105,10 @@ protected function displayJson($class, $database, $table, $policy, $attributes, /** * Render the model information for the CLI. * - * @param string $class + * @param class-string<\Illuminate\Database\Eloquent\Model> $class * @param string $database * @param string $table - * @param string $policy + * @param class-string|null $policy * @param \Illuminate\Support\Collection $attributes * @param \Illuminate\Support\Collection $relations * @param \Illuminate\Support\Collection $events @@ -427,116 +198,4 @@ protected function displayCli($class, $database, $table, $policy, $attributes, $ $this->newLine(); } - - /** - * Get the cast type for the given column. - * - * @param string $column - * @param \Illuminate\Database\Eloquent\Model $model - * @return string|null - */ - protected function getCastType($column, $model) - { - if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) { - return 'accessor'; - } - - if ($model->hasAttributeMutator($column)) { - return 'attribute'; - } - - return $this->getCastsWithDates($model)->get($column) ?? null; - } - - /** - * Get the model casts, including any date casts. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getCastsWithDates($model) - { - return collect($model->getDates()) - ->filter() - ->flip() - ->map(fn () => 'datetime') - ->merge($model->getCasts()); - } - - /** - * Get the default value for the given column. - * - * @param array $column - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed|null - */ - protected function getColumnDefault($column, $model) - { - $attributeDefault = $model->getAttributes()[$column['name']] ?? null; - - return enum_value($attributeDefault, $column['default']); - } - - /** - * Determine if the given attribute is hidden. - * - * @param string $attribute - * @param \Illuminate\Database\Eloquent\Model $model - * @return bool - */ - protected function attributeIsHidden($attribute, $model) - { - if (count($model->getHidden()) > 0) { - return in_array($attribute, $model->getHidden()); - } - - if (count($model->getVisible()) > 0) { - return ! in_array($attribute, $model->getVisible()); - } - - return false; - } - - /** - * Determine if the given attribute is unique. - * - * @param string $column - * @param array $indexes - * @return bool - */ - protected function columnIsUnique($column, $indexes) - { - return collect($indexes)->contains( - fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique'] - ); - } - - /** - * Qualify the given model class base name. - * - * @param string $model - * @return string - * - * @see \Illuminate\Console\GeneratorCommand - */ - protected function qualifyModel(string $model) - { - if (str_contains($model, '\\') && class_exists($model)) { - return $model; - } - - $model = ltrim($model, '\\/'); - - $model = str_replace('/', '\\', $model); - - $rootNamespace = $this->laravel->getNamespace(); - - if (Str::startsWith($model, $rootNamespace)) { - return $model; - } - - return is_dir(app_path('Models')) - ? $rootNamespace.'Models\\'.$model - : $rootNamespace.$model; - } } From a7fa962039ad85191b072ac60db691c7dfdc66d7 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:40:03 -0500 Subject: [PATCH 03/15] rename model --- .../Database/ModelInfoExtractorTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 7495b84f094c..80b079c9a700 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -17,7 +17,7 @@ protected function afterRefreshingDatabase() Schema::create('parent_test_model', function (Blueprint $table) { $table->id(); }); - Schema::create('test_model1', function (Blueprint $table) { + Schema::create('model_info_extractor_test_model', function (Blueprint $table) { $table->increments('id'); $table->uuid(); $table->string('name'); @@ -31,11 +31,11 @@ protected function afterRefreshingDatabase() public function test_extracts_model_data() { $extractor = new ModelInfoExtractor($this->app); - $modelInfo = $extractor->handle(TestModel1::class); + $modelInfo = $extractor->handle(ModelInfoExtractorTestModel::class); - $this->assertEquals(TestModel1::class, $modelInfo['class']); + $this->assertEquals(ModelInfoExtractorTestModel::class, $modelInfo['class']); $this->assertEquals('testing', $modelInfo['database']); - $this->assertEquals('test_model1', $modelInfo['table']); + $this->assertEquals('model_info_extractor_test_model', $modelInfo['table']); $this->assertNull($modelInfo['policy']); $this->assertCount(8, $modelInfo['attributes']); @@ -154,17 +154,17 @@ public function test_extracts_model_data() $this->assertCount(1, $modelInfo['observers']); $this->assertEquals('created', $modelInfo['observers'][0]['event']); $this->assertCount(1, $modelInfo['observers'][0]['observer']); - $this->assertEquals("Illuminate\Tests\Integration\Database\TestModel1Observer@created", $modelInfo['observers'][0]['observer'][0]); + $this->assertEquals("Illuminate\Tests\Integration\Database\ModelInfoExtractorTestModelObserver@created", $modelInfo['observers'][0]['observer'][0]); } } -#[ObservedBy(TestModel1Observer::class)] -class TestModel1 extends Model +#[ObservedBy(ModelInfoExtractorTestModelObserver::class)] +class ModelInfoExtractorTestModel extends Model { use HasUuids; - public $table = 'test_model1'; + public $table = 'model_info_extractor_test_model'; protected $guarded = ['name']; protected $casts = ['nullable_date' => 'datetime', 'a_bool' => 'bool']; @@ -180,7 +180,7 @@ class ParentTestModel extends Model public $timestamps = false; } -class TestModel1Observer +class ModelInfoExtractorTestModelObserver { public function created() { From 0e72d33ee16541262afce1731c2c240bbbb5add2 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:41:05 -0500 Subject: [PATCH 04/15] style --- .../Database/Eloquent/ModelInfoExtractor.php | 6 +- .../Database/ModelInfoExtractorTest.php | 167 +++++++++--------- 2 files changed, 87 insertions(+), 86 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php b/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php index 88374d65e4f1..6e8c69d51c91 100644 --- a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php +++ b/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php @@ -10,6 +10,7 @@ use ReflectionMethod; use ReflectionNamedType; use SplFileObject; + use function Illuminate\Support\enum_value; class ModelInfoExtractor @@ -41,7 +42,7 @@ class ModelInfoExtractor ]; /** - * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Illuminate\Contracts\Foundation\Application $app */ public function __construct($app) { @@ -76,7 +77,7 @@ public function handle($model, $connection = null) 'attributes' => $this->getAttributes($model), 'relations' => $this->getRelations($model), 'events' => $this->getEvents($model), - 'observers' => $this->getObservers($model) + 'observers' => $this->getObservers($model), ]; } @@ -235,6 +236,7 @@ protected function getEvents($model) * * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Support\Collection + * * @throws BindingResolutionException */ protected function getObservers($model) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 80b079c9a700..a27ddf536c9b 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -40,114 +40,114 @@ public function test_extracts_model_data() $this->assertCount(8, $modelInfo['attributes']); $this->assertEqualsCanonicalizing([ - "name" => "id", - "type" => "integer", - "increments" => true, - "nullable" => false, - "default" => null, - "unique" => true, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => null, + 'name' => 'id', + 'type' => 'integer', + 'increments' => true, + 'nullable' => false, + 'default' => null, + 'unique' => true, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => null, ], $modelInfo['attributes'][0]); $this->assertEqualsCanonicalizing([ - "name" => "uuid", - "type" => "varchar", - "increments" => false, - "nullable" => false, - "default" => null, - "unique" => false, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => null, + 'name' => 'uuid', + 'type' => 'varchar', + 'increments' => false, + 'nullable' => false, + 'default' => null, + 'unique' => false, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => null, ], $modelInfo['attributes'][1]); $this->assertEqualsCanonicalizing([ - "name" => "name", - "type" => "varchar", - "increments" => false, - "nullable" => false, - "default" => null, - "unique" => false, - "fillable" => false, - "hidden" => false, - "appended" => null, - "cast" => null + 'name' => 'name', + 'type' => 'varchar', + 'increments' => false, + 'nullable' => false, + 'default' => null, + 'unique' => false, + 'fillable' => false, + 'hidden' => false, + 'appended' => null, + 'cast' => null, ], $modelInfo['attributes'][2]); $this->assertEqualsCanonicalizing([ - "name" => "a_bool", - "type" => "tinyint(1)", - "increments" => false, - "nullable" => false, - "default" => null, - "unique" => false, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => "bool", + 'name' => 'a_bool', + 'type' => 'tinyint(1)', + 'increments' => false, + 'nullable' => false, + 'default' => null, + 'unique' => false, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => 'bool', ], $modelInfo['attributes'][3]); $this->assertEqualsCanonicalizing([ - "name" => "parent_model_id", - "type" => "integer", - "increments" => false, - "nullable" => false, - "default" => null, - "unique" => false, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => null, + 'name' => 'parent_model_id', + 'type' => 'integer', + 'increments' => false, + 'nullable' => false, + 'default' => null, + 'unique' => false, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => null, ], $modelInfo['attributes'][4]); $this->assertEqualsCanonicalizing([ - "name" => "nullable_date", - "type" => "datetime", - "increments" => false, - "nullable" => true, - "default" => null, - "unique" => false, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => "datetime", + 'name' => 'nullable_date', + 'type' => 'datetime', + 'increments' => false, + 'nullable' => true, + 'default' => null, + 'unique' => false, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => 'datetime', ], $modelInfo['attributes'][5]); $this->assertEqualsCanonicalizing([ - "name" => "created_at", - "type" => "datetime", - "increments" => false, - "nullable" => true, - "default" => null, - "unique" => false, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => "datetime", + 'name' => 'created_at', + 'type' => 'datetime', + 'increments' => false, + 'nullable' => true, + 'default' => null, + 'unique' => false, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => 'datetime', ], $modelInfo['attributes'][6]); $this->assertEqualsCanonicalizing([ - "name" => "updated_at", - "type" => "datetime", - "increments" => false, - "nullable" => true, - "default" => null, - "unique" => false, - "fillable" => true, - "hidden" => false, - "appended" => null, - "cast" => "datetime", + 'name' => 'updated_at', + 'type' => 'datetime', + 'increments' => false, + 'nullable' => true, + 'default' => null, + 'unique' => false, + 'fillable' => true, + 'hidden' => false, + 'appended' => null, + 'cast' => 'datetime', ], $modelInfo['attributes'][7]); $this->assertCount(1, $modelInfo['relations']); $this->assertEqualsCanonicalizing([ - "name" => "parentModel", - "type" => "BelongsTo", - "related" => "Illuminate\Tests\Integration\Database\ParentTestModel", + 'name' => 'parentModel', + 'type' => 'BelongsTo', + 'related' => "Illuminate\Tests\Integration\Database\ParentTestModel", ], $modelInfo['relations'][0]); $this->assertEmpty($modelInfo['events']); @@ -158,7 +158,6 @@ public function test_extracts_model_data() } } - #[ObservedBy(ModelInfoExtractorTestModelObserver::class)] class ModelInfoExtractorTestModel extends Model { From 31d2cdc654a972824c2e480ace02c3d12f652bcc Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:45:33 -0500 Subject: [PATCH 05/15] style --- tests/Integration/Database/ModelInfoExtractorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index a27ddf536c9b..624e5cc24f1e 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -175,7 +175,7 @@ public function parentModel(): BelongsTo class ParentTestModel extends Model { - public $table = 'parent_model'; + public $table = 'parent_test_model'; public $timestamps = false; } From 000123c32bcd3ff89d653109d4b9cfd0f275d2b2 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:48:11 -0500 Subject: [PATCH 06/15] fix table --- tests/Integration/Database/ModelInfoExtractorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 624e5cc24f1e..8f60b7271947 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -22,7 +22,7 @@ protected function afterRefreshingDatabase() $table->uuid(); $table->string('name'); $table->boolean('a_bool'); - $table->foreignId('parent_model_id')->constrained(); + $table->foreignId('parent_test_model_id')->constrained(); $table->timestamp('nullable_date')->nullable(); $table->timestamps(); }); From b9d3ec2ff906fceb77be38629f38c337707e054c Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 17:51:27 -0500 Subject: [PATCH 07/15] fix table --- tests/Integration/Database/ModelInfoExtractorTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 8f60b7271947..b20024045bd0 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -14,7 +14,7 @@ class ModelInfoExtractorTest extends DatabaseTestCase { protected function afterRefreshingDatabase() { - Schema::create('parent_test_model', function (Blueprint $table) { + Schema::create('parent_test_models', function (Blueprint $table) { $table->id(); }); Schema::create('model_info_extractor_test_model', function (Blueprint $table) { @@ -34,7 +34,7 @@ public function test_extracts_model_data() $modelInfo = $extractor->handle(ModelInfoExtractorTestModel::class); $this->assertEquals(ModelInfoExtractorTestModel::class, $modelInfo['class']); - $this->assertEquals('testing', $modelInfo['database']); + $this->assertEquals(Schema::getConnection()->getConfig()['name'], $modelInfo['database']); $this->assertEquals('model_info_extractor_test_model', $modelInfo['table']); $this->assertNull($modelInfo['policy']); $this->assertCount(8, $modelInfo['attributes']); @@ -92,7 +92,7 @@ public function test_extracts_model_data() ], $modelInfo['attributes'][3]); $this->assertEqualsCanonicalizing([ - 'name' => 'parent_model_id', + 'name' => 'parent_test_model_id', 'type' => 'integer', 'increments' => false, 'nullable' => false, @@ -175,7 +175,7 @@ public function parentModel(): BelongsTo class ParentTestModel extends Model { - public $table = 'parent_test_model'; + public $table = 'parent_test_models'; public $timestamps = false; } From e74ac54978b3f3de5e3b9bc9fc0ed0fc164fb129 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 18:08:06 -0500 Subject: [PATCH 08/15] ignore type --- .../Database/ModelInfoExtractorTest.php | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index b20024045bd0..dfbbc82ce022 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -39,9 +39,11 @@ public function test_extracts_model_data() $this->assertNull($modelInfo['policy']); $this->assertCount(8, $modelInfo['attributes']); + // We ignore type because it will vary by DB engine + $keys = ['name', 'increments', 'nullable', 'default', 'unique', 'fillable', 'hidden', 'appended', 'cast']; + $this->assertEqualsCanonicalizing([ 'name' => 'id', - 'type' => 'integer', 'increments' => true, 'nullable' => false, 'default' => null, @@ -50,11 +52,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], $modelInfo['attributes'][0]); + ], collect($modelInfo['attributes'][0])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'uuid', - 'type' => 'varchar', 'increments' => false, 'nullable' => false, 'default' => null, @@ -63,11 +64,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], $modelInfo['attributes'][1]); + ], collect($modelInfo['attributes'][1])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'name', - 'type' => 'varchar', 'increments' => false, 'nullable' => false, 'default' => null, @@ -76,11 +76,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], $modelInfo['attributes'][2]); + ], collect($modelInfo['attributes'][2])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'a_bool', - 'type' => 'tinyint(1)', 'increments' => false, 'nullable' => false, 'default' => null, @@ -89,11 +88,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'bool', - ], $modelInfo['attributes'][3]); + ], collect($modelInfo['attributes'][3])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'parent_test_model_id', - 'type' => 'integer', 'increments' => false, 'nullable' => false, 'default' => null, @@ -102,11 +100,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], $modelInfo['attributes'][4]); + ], collect($modelInfo['attributes'][4])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'nullable_date', - 'type' => 'datetime', 'increments' => false, 'nullable' => true, 'default' => null, @@ -115,11 +112,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'datetime', - ], $modelInfo['attributes'][5]); + ], collect($modelInfo['attributes'][5])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'created_at', - 'type' => 'datetime', 'increments' => false, 'nullable' => true, 'default' => null, @@ -128,11 +124,10 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'datetime', - ], $modelInfo['attributes'][6]); + ], collect($modelInfo['attributes'][6])->only($keys)->all()); $this->assertEqualsCanonicalizing([ 'name' => 'updated_at', - 'type' => 'datetime', 'increments' => false, 'nullable' => true, 'default' => null, @@ -141,7 +136,7 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'datetime', - ], $modelInfo['attributes'][7]); + ], collect($modelInfo['attributes'][7])->only($keys)->all()); $this->assertCount(1, $modelInfo['relations']); $this->assertEqualsCanonicalizing([ From 5c44fb2483be376f99f9107022136a5a2bae334a Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 18:16:47 -0500 Subject: [PATCH 09/15] ignoring type and avoid assertEqualsCanonicalizing --- .../Database/ModelInfoExtractorTest.php | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index dfbbc82ce022..966aa553517b 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -39,10 +39,7 @@ public function test_extracts_model_data() $this->assertNull($modelInfo['policy']); $this->assertCount(8, $modelInfo['attributes']); - // We ignore type because it will vary by DB engine - $keys = ['name', 'increments', 'nullable', 'default', 'unique', 'fillable', 'hidden', 'appended', 'cast']; - - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'id', 'increments' => true, 'nullable' => false, @@ -52,9 +49,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], collect($modelInfo['attributes'][0])->only($keys)->all()); + ], $modelInfo['attributes'][0]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'uuid', 'increments' => false, 'nullable' => false, @@ -64,9 +61,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], collect($modelInfo['attributes'][1])->only($keys)->all()); + ], $modelInfo['attributes'][1]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'name', 'increments' => false, 'nullable' => false, @@ -76,9 +73,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], collect($modelInfo['attributes'][2])->only($keys)->all()); + ], $modelInfo['attributes'][2]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'a_bool', 'increments' => false, 'nullable' => false, @@ -88,9 +85,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'bool', - ], collect($modelInfo['attributes'][3])->only($keys)->all()); + ], $modelInfo['attributes'][3]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'parent_test_model_id', 'increments' => false, 'nullable' => false, @@ -100,9 +97,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => null, - ], collect($modelInfo['attributes'][4])->only($keys)->all()); + ], $modelInfo['attributes'][4]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'nullable_date', 'increments' => false, 'nullable' => true, @@ -112,9 +109,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'datetime', - ], collect($modelInfo['attributes'][5])->only($keys)->all()); + ], $modelInfo['attributes'][5]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'created_at', 'increments' => false, 'nullable' => true, @@ -124,9 +121,9 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'datetime', - ], collect($modelInfo['attributes'][6])->only($keys)->all()); + ], $modelInfo['attributes'][6]); - $this->assertEqualsCanonicalizing([ + $this->assertAttributes([ 'name' => 'updated_at', 'increments' => false, 'nullable' => true, @@ -136,7 +133,7 @@ public function test_extracts_model_data() 'hidden' => false, 'appended' => null, 'cast' => 'datetime', - ], collect($modelInfo['attributes'][7])->only($keys)->all()); + ], $modelInfo['attributes'][7]); $this->assertCount(1, $modelInfo['relations']); $this->assertEqualsCanonicalizing([ @@ -151,6 +148,15 @@ public function test_extracts_model_data() $this->assertCount(1, $modelInfo['observers'][0]['observer']); $this->assertEquals("Illuminate\Tests\Integration\Database\ModelInfoExtractorTestModelObserver@created", $modelInfo['observers'][0]['observer'][0]); } + + private function assertAttributes($expectedAttributes, $actualAttributes) + { + foreach(['name', 'increments', 'nullable', 'default', 'unique', 'fillable', 'hidden', 'appended', 'cast'] as $key) { + $this->assertEquals($expectedAttributes[$key], $actualAttributes[$key]); + } + // We ignore type because it varies from DB to DB + $this->assertArrayHasKey('type', $actualAttributes); + } } #[ObservedBy(ModelInfoExtractorTestModelObserver::class)] From 83f21bea38ab7262a0a53143455731de47e23300 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 18:29:38 -0500 Subject: [PATCH 10/15] ignore default --- tests/Integration/Database/ModelInfoExtractorTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 966aa553517b..4b9aa380aec2 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -151,11 +151,12 @@ public function test_extracts_model_data() private function assertAttributes($expectedAttributes, $actualAttributes) { - foreach(['name', 'increments', 'nullable', 'default', 'unique', 'fillable', 'hidden', 'appended', 'cast'] as $key) { + foreach(['name', 'increments', 'nullable', 'unique', 'fillable', 'hidden', 'appended', 'cast'] as $key) { $this->assertEquals($expectedAttributes[$key], $actualAttributes[$key]); } // We ignore type because it varies from DB to DB $this->assertArrayHasKey('type', $actualAttributes); + $this->assertArrayHasKey('default', $actualAttributes); } } From 93067d47bc9bcbcbc1fba56df99f03abd230e020 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 19:15:13 -0500 Subject: [PATCH 11/15] test command --- .../Database/Console/ShowModelCommand.php | 7 +++++-- .../Database/ModelInfoExtractorTest.php | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index 02ea3c8e937e..ae3e35f47517 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -38,10 +38,13 @@ class ShowModelCommand extends DatabaseInspectionCommand * * @return int */ - public function handle(ModelInfoExtractor $extractor) + public function handle() { try { - $info = $extractor->handle($this->argument('model'), $this->option('database')); + $info = (new ModelInfoExtractor($this->laravel))->handle( + $this->argument('model'), + $this->option('database') + ); } catch (BindingResolutionException $e) { $this->components->error($e->getMessage()); diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 4b9aa380aec2..28dedbae8137 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\ModelInfoExtractor; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Schema; class ModelInfoExtractorTest extends DatabaseTestCase @@ -32,7 +33,19 @@ public function test_extracts_model_data() { $extractor = new ModelInfoExtractor($this->app); $modelInfo = $extractor->handle(ModelInfoExtractorTestModel::class); + $this->assertModelInfo($modelInfo); + } + public function test_command_returns_json() + { + $this->withoutMockingConsoleOutput()->artisan('model:show', ['model' => ModelInfoExtractorTestModel::class, '--json' => true]); + $o = Artisan::output(); + $this->assertJson($o); + $modelInfo = json_decode($o, true); + $this->assertModelInfo($modelInfo); + } + + private function assertModelInfo(array $modelInfo) { $this->assertEquals(ModelInfoExtractorTestModel::class, $modelInfo['class']); $this->assertEquals(Schema::getConnection()->getConfig()['name'], $modelInfo['database']); $this->assertEquals('model_info_extractor_test_model', $modelInfo['table']); @@ -151,13 +164,14 @@ public function test_extracts_model_data() private function assertAttributes($expectedAttributes, $actualAttributes) { - foreach(['name', 'increments', 'nullable', 'unique', 'fillable', 'hidden', 'appended', 'cast'] as $key) { + foreach (['name', 'increments', 'nullable', 'unique', 'fillable', 'hidden', 'appended', 'cast'] as $key) { $this->assertEquals($expectedAttributes[$key], $actualAttributes[$key]); } // We ignore type because it varies from DB to DB $this->assertArrayHasKey('type', $actualAttributes); $this->assertArrayHasKey('default', $actualAttributes); } + } #[ObservedBy(ModelInfoExtractorTestModelObserver::class)] From 6b60bac5d6168bcdeec44d06da92ee45b3f0b5e9 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sat, 16 Nov 2024 19:16:35 -0500 Subject: [PATCH 12/15] style --- tests/Integration/Database/ModelInfoExtractorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInfoExtractorTest.php index 28dedbae8137..412e98c1f3e7 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInfoExtractorTest.php @@ -45,7 +45,8 @@ public function test_command_returns_json() $this->assertModelInfo($modelInfo); } - private function assertModelInfo(array $modelInfo) { + private function assertModelInfo(array $modelInfo) + { $this->assertEquals(ModelInfoExtractorTestModel::class, $modelInfo['class']); $this->assertEquals(Schema::getConnection()->getConfig()['name'], $modelInfo['database']); $this->assertEquals('model_info_extractor_test_model', $modelInfo['table']); @@ -171,7 +172,6 @@ private function assertAttributes($expectedAttributes, $actualAttributes) $this->assertArrayHasKey('type', $actualAttributes); $this->assertArrayHasKey('default', $actualAttributes); } - } #[ObservedBy(ModelInfoExtractorTestModelObserver::class)] From c332250de5a7bbe174f4df914b5c2038c36ee312 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Sun, 17 Nov 2024 12:30:21 -0500 Subject: [PATCH 13/15] explicitly type Application --- src/Illuminate/Database/Console/ShowModelCommand.php | 4 ++-- src/Illuminate/Database/Eloquent/ModelInfoExtractor.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index ae3e35f47517..45f649b46355 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -38,10 +38,10 @@ class ShowModelCommand extends DatabaseInspectionCommand * * @return int */ - public function handle() + public function handle(ModelInfoExtractor $modelInfoExtractor) { try { - $info = (new ModelInfoExtractor($this->laravel))->handle( + $info = $modelInfoExtractor->handle( $this->argument('model'), $this->option('database') ); diff --git a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php b/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php index 6e8c69d51c91..c5c2c08ce8cb 100644 --- a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php +++ b/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; @@ -44,7 +45,7 @@ class ModelInfoExtractor /** * @param \Illuminate\Contracts\Foundation\Application $app */ - public function __construct($app) + public function __construct(Application $app) { $this->app = $app; } From 14b2c03461da17360c96683d0fd9161f091a22cb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 18 Nov 2024 10:12:03 -0600 Subject: [PATCH 14/15] rename files --- .../Database/Console/ShowModelCommand.php | 4 +- ...elInfoExtractor.php => ModelInspector.php} | 65 ++++++++++--------- ...tractorTest.php => ModelInspectorTest.php} | 20 +++--- 3 files changed, 46 insertions(+), 43 deletions(-) rename src/Illuminate/Database/Eloquent/{ModelInfoExtractor.php => ModelInspector.php} (98%) rename tests/Integration/Database/{ModelInfoExtractorTest.php => ModelInspectorTest.php} (90%) diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index 45f649b46355..0143e669e40d 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -3,7 +3,7 @@ namespace Illuminate\Database\Console; use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Database\Eloquent\ModelInfoExtractor; +use Illuminate\Database\Eloquent\ModelInspector; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Output\OutputInterface; @@ -38,7 +38,7 @@ class ShowModelCommand extends DatabaseInspectionCommand * * @return int */ - public function handle(ModelInfoExtractor $modelInfoExtractor) + public function handle(ModelInspector $modelInfoExtractor) { try { $info = $modelInfoExtractor->handle( diff --git a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php b/src/Illuminate/Database/Eloquent/ModelInspector.php similarity index 98% rename from src/Illuminate/Database/Eloquent/ModelInfoExtractor.php rename to src/Illuminate/Database/Eloquent/ModelInspector.php index c5c2c08ce8cb..bf2396e0c74a 100644 --- a/src/Illuminate/Database/Eloquent/ModelInfoExtractor.php +++ b/src/Illuminate/Database/Eloquent/ModelInspector.php @@ -14,7 +14,7 @@ use function Illuminate\Support\enum_value; -class ModelInfoExtractor +class ModelInspector { /** * The Laravel application instance. @@ -43,7 +43,10 @@ class ModelInfoExtractor ]; /** + * Create a new model inspector instance. + * * @param \Illuminate\Contracts\Foundation\Application $app + * @return void */ public function __construct(Application $app) { @@ -51,7 +54,7 @@ public function __construct(Application $app) } /** - * Extract model details. + * Extract model details for the given model. * * @param class-string<\Illuminate\Database\Eloquent\Model>|string $model * @param string|null $connection @@ -82,19 +85,6 @@ public function handle($model, $connection = null) ]; } - /** - * Get the first policy associated with this model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return string|null - */ - protected function getPolicy($model) - { - $policy = Gate::getPolicyFor($model::class); - - return $policy ? $policy::class : null; - } - /** * Get the column attributes for the given model. * @@ -218,7 +208,20 @@ protected function getRelations($model) } /** - * Get the Events that the model dispatches. + * Get the first policy associated with this model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return string|null + */ + protected function getPolicy($model) + { + $policy = Gate::getPolicyFor($model::class); + + return $policy ? $policy::class : null; + } + + /** + * Get the events that the model dispatches. * * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Support\Collection @@ -233,7 +236,7 @@ protected function getEvents($model) } /** - * Get the Observers watching this model. + * Get the observers watching this model. * * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Support\Collection @@ -332,20 +335,6 @@ protected function getCastsWithDates($model) ->merge($model->getCasts()); } - /** - * Get the default value for the given column. - * - * @param array $column - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed|null - */ - protected function getColumnDefault($column, $model) - { - $attributeDefault = $model->getAttributes()[$column['name']] ?? null; - - return enum_value($attributeDefault, $column['default']); - } - /** * Determine if the given attribute is hidden. * @@ -366,6 +355,20 @@ protected function attributeIsHidden($attribute, $model) return false; } + /** + * Get the default value for the given column. + * + * @param array $column + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed|null + */ + protected function getColumnDefault($column, $model) + { + $attributeDefault = $model->getAttributes()[$column['name']] ?? null; + + return enum_value($attributeDefault, $column['default']); + } + /** * Determine if the given attribute is unique. * diff --git a/tests/Integration/Database/ModelInfoExtractorTest.php b/tests/Integration/Database/ModelInspectorTest.php similarity index 90% rename from tests/Integration/Database/ModelInfoExtractorTest.php rename to tests/Integration/Database/ModelInspectorTest.php index 412e98c1f3e7..db41fb95cf5c 100644 --- a/tests/Integration/Database/ModelInfoExtractorTest.php +++ b/tests/Integration/Database/ModelInspectorTest.php @@ -5,13 +5,13 @@ use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\ModelInfoExtractor; +use Illuminate\Database\Eloquent\ModelInspector; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Schema; -class ModelInfoExtractorTest extends DatabaseTestCase +class ModelInspectorTest extends DatabaseTestCase { protected function afterRefreshingDatabase() { @@ -31,14 +31,14 @@ protected function afterRefreshingDatabase() public function test_extracts_model_data() { - $extractor = new ModelInfoExtractor($this->app); - $modelInfo = $extractor->handle(ModelInfoExtractorTestModel::class); + $extractor = new ModelInspector($this->app); + $modelInfo = $extractor->handle(ModelInspectorTestModel::class); $this->assertModelInfo($modelInfo); } public function test_command_returns_json() { - $this->withoutMockingConsoleOutput()->artisan('model:show', ['model' => ModelInfoExtractorTestModel::class, '--json' => true]); + $this->withoutMockingConsoleOutput()->artisan('model:show', ['model' => ModelInspectorTestModel::class, '--json' => true]); $o = Artisan::output(); $this->assertJson($o); $modelInfo = json_decode($o, true); @@ -47,7 +47,7 @@ public function test_command_returns_json() private function assertModelInfo(array $modelInfo) { - $this->assertEquals(ModelInfoExtractorTestModel::class, $modelInfo['class']); + $this->assertEquals(ModelInspectorTestModel::class, $modelInfo['class']); $this->assertEquals(Schema::getConnection()->getConfig()['name'], $modelInfo['database']); $this->assertEquals('model_info_extractor_test_model', $modelInfo['table']); $this->assertNull($modelInfo['policy']); @@ -160,7 +160,7 @@ private function assertModelInfo(array $modelInfo) $this->assertCount(1, $modelInfo['observers']); $this->assertEquals('created', $modelInfo['observers'][0]['event']); $this->assertCount(1, $modelInfo['observers'][0]['observer']); - $this->assertEquals("Illuminate\Tests\Integration\Database\ModelInfoExtractorTestModelObserver@created", $modelInfo['observers'][0]['observer'][0]); + $this->assertEquals("Illuminate\Tests\Integration\Database\ModelInspectorTestModelObserver@created", $modelInfo['observers'][0]['observer'][0]); } private function assertAttributes($expectedAttributes, $actualAttributes) @@ -174,8 +174,8 @@ private function assertAttributes($expectedAttributes, $actualAttributes) } } -#[ObservedBy(ModelInfoExtractorTestModelObserver::class)] -class ModelInfoExtractorTestModel extends Model +#[ObservedBy(ModelInspectorTestModelObserver::class)] +class ModelInspectorTestModel extends Model { use HasUuids; @@ -195,7 +195,7 @@ class ParentTestModel extends Model public $timestamps = false; } -class ModelInfoExtractorTestModelObserver +class ModelInspectorTestModelObserver { public function created() { From 75492866619ba56173f4feeabbe4065fd8cbc2d2 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 18 Nov 2024 10:12:44 -0600 Subject: [PATCH 15/15] rename method --- src/Illuminate/Database/Console/ShowModelCommand.php | 4 ++-- src/Illuminate/Database/Eloquent/ModelInspector.php | 2 +- tests/Integration/Database/ModelInspectorTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index 0143e669e40d..b6ec96a38b83 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -38,10 +38,10 @@ class ShowModelCommand extends DatabaseInspectionCommand * * @return int */ - public function handle(ModelInspector $modelInfoExtractor) + public function handle(ModelInspector $modelInspector) { try { - $info = $modelInfoExtractor->handle( + $info = $modelInspector->inspect( $this->argument('model'), $this->option('database') ); diff --git a/src/Illuminate/Database/Eloquent/ModelInspector.php b/src/Illuminate/Database/Eloquent/ModelInspector.php index bf2396e0c74a..093d5c6fdb3a 100644 --- a/src/Illuminate/Database/Eloquent/ModelInspector.php +++ b/src/Illuminate/Database/Eloquent/ModelInspector.php @@ -62,7 +62,7 @@ public function __construct(Application $app) * * @throws BindingResolutionException */ - public function handle($model, $connection = null) + public function inspect($model, $connection = null) { $class = $this->qualifyModel($model); diff --git a/tests/Integration/Database/ModelInspectorTest.php b/tests/Integration/Database/ModelInspectorTest.php index db41fb95cf5c..30ec0c4cc356 100644 --- a/tests/Integration/Database/ModelInspectorTest.php +++ b/tests/Integration/Database/ModelInspectorTest.php @@ -32,7 +32,7 @@ protected function afterRefreshingDatabase() public function test_extracts_model_data() { $extractor = new ModelInspector($this->app); - $modelInfo = $extractor->handle(ModelInspectorTestModel::class); + $modelInfo = $extractor->inspect(ModelInspectorTestModel::class); $this->assertModelInfo($modelInfo); }