diff --git a/src/Illuminate/Foundation/Console/ShowModelCommand.php b/src/Illuminate/Foundation/Console/ShowModelCommand.php new file mode 100644 index 000000000000..31ad7c10da45 --- /dev/null +++ b/src/Illuminate/Foundation/Console/ShowModelCommand.php @@ -0,0 +1,452 @@ +components->error( + 'Displaying model information requires [doctrine/dbal].' + ); + } + + $class = $this->qualifyModel($this->argument('model')); + + try { + $model = $this->laravel->make($class); + } catch (BindingResolutionException $e) { + return $this->components->error($e->getMessage()); + } + + if ($this->option('database')) { + $model->setConnection($this->option('database')); + } + + $this->display( + $class, + $model->getConnection()->getName(), + $model->getConnection()->getTablePrefix().$model->getTable(), + $this->getAttributes($model), + $this->getRelations($model), + ); + } + + /** + * Get the column attributes for the given model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Support\Collection + */ + protected function getAttributes($model) + { + $schema = $model->getConnection()->getDoctrineSchemaManager(); + $table = $model->getConnection()->getTablePrefix().$model->getTable(); + $columns = $schema->listTableColumns($table); + $indexes = $schema->listTableIndexes($table); + + return collect($columns) + ->values() + ->map(fn (Column $column) => [ + 'name' => $column->getName(), + 'type' => $this->getColumnType($column), + 'increments' => $column->getAutoincrement(), + 'nullable' => ! $column->getNotnull(), + 'default' => $this->getColumnDefault($column, $model), + 'unique' => $this->columnIsUnique($column->getName(), $indexes), + 'fillable' => $model->isFillable($column->getName()), + 'hidden' => $this->attributeIsHidden($column->getName(), $model), + 'appended' => null, + 'cast' => $this->getCastType($column->getName(), $model), + ]) + ->merge($this->getVirtualAttributes($model, $columns)); + } + + /** + * Get the virtual (non-column) attributes for the given model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param \Doctrine\DBAL\Schema\Column[] $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() !== get_class($model) + ) + ->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)->has($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() !== get_class($model) + ) + ->filter(function (ReflectionMethod $method) { + $file = new SplFileObject($method->getFileName()); + $file->seek($method->getStartLine() - 1); + $code = ''; + while ($file->key() < $method->getEndLine()) { + $code .= $file->current(); + $file->next(); + } + + return collect($this->relationMethods) + ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); + }) + ->values() + ->map(function (ReflectionMethod $method) use ($model) { + $relation = $method->invoke($model); + + return [ + 'name' => $method->getName(), + 'type' => Str::afterLast(get_class($relation), '\\'), + 'related' => get_class($relation->getRelated()), + ]; + }); + } + + /** + * Render the model information. + * + * @param string $class + * @param string $database + * @param string $table + * @param \Illuminate\Support\Collection $attributes + * @param \Illuminate\Support\Collection $relations + * @return void + */ + protected function display($class, $database, $table, $attributes, $relations) + { + $this->option('json') + ? $this->displayJson($class, $database, $table, $attributes, $relations) + : $this->displayCli($class, $database, $table, $attributes, $relations); + } + + /** + * Render the model information as JSON. + * + * @param string $class + * @param string $database + * @param string $table + * @param \Illuminate\Support\Collection $attributes + * @param \Illuminate\Support\Collection $relations + * @return void + */ + protected function displayJson($class, $database, $table, $attributes, $relations) + { + $this->output->writeln( + collect([ + 'class' => $class, + 'database' => $database, + 'table' => $table, + 'attributes' => $attributes, + 'relations' => $relations, + ])->toJson() + ); + } + + /** + * Render the model information for the CLI. + * + * @param string $class + * @param string $database + * @param string $table + * @param \Illuminate\Support\Collection $attributes + * @param \Illuminate\Support\Collection $relations + * @return void + */ + protected function displayCli($class, $database, $table, $attributes, $relations) + { + $this->newLine(); + + $this->components->twoColumnDetail(''.$class.''); + $this->components->twoColumnDetail('Database', $database); + $this->components->twoColumnDetail('Table', $table); + + $this->newLine(); + + $this->components->twoColumnDetail( + 'Attributes', + 'type / cast', + ); + + foreach ($attributes as $attribute) { + $first = trim(sprintf( + '%s %s', + $attribute['name'], + collect(['increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended']) + ->filter(fn ($property) => $attribute[$property]) + ->map(fn ($property) => sprintf('%s', $property)) + ->implode(', ') + )); + + $second = collect([ + $attribute['type'], + $attribute['cast'] ? ''.$attribute['cast'].'' : null, + ])->filter()->implode(' / '); + + $this->components->twoColumnDetail($first, $second); + + if ($attribute['default'] !== null) { + $this->components->bulletList( + [sprintf('default: %s', $attribute['default'])], + OutputInterface::VERBOSITY_VERBOSE + ); + } + } + + $this->newLine(); + + $this->components->twoColumnDetail('Relations'); + + foreach ($relations as $relation) { + $this->components->twoColumnDetail( + sprintf('%s %s', $relation['name'], $relation['type']), + $relation['related'] + ); + } + + $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([ + ...collect($model->getDates())->flip()->map(fn () => 'datetime'), + ...$model->getCasts(), + ]); + } + + /** + * Get the type of the given column. + * + * @param \Doctrine\DBAL\Schema\Column $column + * @return string + */ + protected function getColumnType($column) + { + $name = $column->getType()->getName(); + + $unsigned = $column->getUnsigned() ? ' unsigned' : ''; + + $details = match (get_class($column->getType())) { + DecimalType::class => $column->getPrecision().','.$column->getScale(), + default => $column->getLength(), + }; + + if ($details) { + return sprintf('%s(%s)%s', $name, $details, $unsigned); + } + + return sprintf('%s%s', $name, $unsigned); + } + + /** + * Get the default value for the given column. + * + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Illuminate\Database\Eloquent\Model $model + * @return string|null + */ + protected function getColumnDefault($column, $model) + { + return $model->getAttributes()[$column->getName()] ?? $column->getDefault(); + } + + /** + * 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 \Doctrine\DBAL\Schema\Index[] $indexes + * @return bool + */ + protected function columnIsUnique($column, $indexes) + { + return collect($indexes) + ->filter(fn (Index $index) => count($index->getColumns()) === 1 && $index->getColumns()[0] === $column) + ->contains(fn (Index $index) => $index->isUnique()); + } + + /** + * Qualify the given model class base name. + * + * @param string $model + * @return string + * + * @see \Illuminate\Console\GeneratorCommand + */ + protected function qualifyModel(string $model) + { + if (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; + } +} diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index 840ce184ff14..3bcdd1c3e1c3 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -55,6 +55,7 @@ use Illuminate\Foundation\Console\RuleMakeCommand; use Illuminate\Foundation\Console\ScopeMakeCommand; use Illuminate\Foundation\Console\ServeCommand; +use Illuminate\Foundation\Console\ShowModelCommand; use Illuminate\Foundation\Console\StorageLinkCommand; use Illuminate\Foundation\Console\StubPublishCommand; use Illuminate\Foundation\Console\TestMakeCommand; @@ -132,6 +133,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'ScheduleClearCache' => ScheduleClearCacheCommand::class, 'ScheduleTest' => ScheduleTestCommand::class, 'ScheduleWork' => ScheduleWorkCommand::class, + 'ShowModel' => ShowModelCommand::class, 'StorageLink' => StorageLinkCommand::class, 'Up' => UpCommand::class, 'ViewCache' => ViewCacheCommand::class, @@ -1002,6 +1004,16 @@ protected function registerScheduleWorkCommand() $this->app->singleton(ScheduleWorkCommand::class); } + /** + * Register the command. + * + * @return void + */ + protected function registerShowModelCommand() + { + $this->app->singleton(ShowModelCommand::class); + } + /** * Register the command. *