| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- <?php
- namespace Illuminate\Database\Console;
- use BackedEnum;
- 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 Symfony\Component\Console\Attribute\AsCommand;
- use Symfony\Component\Console\Output\OutputInterface;
- use UnitEnum;
- #[AsCommand(name: 'model:show')]
- class ShowModelCommand extends DatabaseInspectionCommand
- {
- /**
- * The console command name.
- *
- * @var string
- */
- protected $name = 'model:show {model}';
- /**
- * The console command description.
- *
- * @var string
- */
- protected $description = 'Show information about an Eloquent model';
- /**
- * The console command signature.
- *
- * @var string
- */
- protected $signature = 'model:show {model : The model to show}
- {--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()
- {
- $class = $this->qualifyModel($this->argument('model'));
- try {
- $model = $this->laravel->make($class);
- $class = get_class($model);
- } 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),
- );
- 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 string $database
- * @param string $table
- * @param string $policy
- * @param \Illuminate\Support\Collection $attributes
- * @param \Illuminate\Support\Collection $relations
- * @param \Illuminate\Support\Collection $events
- * @param \Illuminate\Support\Collection $observers
- * @return void
- */
- protected function display($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
- {
- $this->option('json')
- ? $this->displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
- : $this->displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers);
- }
- /**
- * Render the model information as JSON.
- *
- * @param string $class
- * @param string $database
- * @param string $table
- * @param string $policy
- * @param \Illuminate\Support\Collection $attributes
- * @param \Illuminate\Support\Collection $relations
- * @param \Illuminate\Support\Collection $events
- * @param \Illuminate\Support\Collection $observers
- * @return void
- */
- protected function displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
- {
- $this->output->writeln(
- collect([
- 'class' => $class,
- 'database' => $database,
- 'table' => $table,
- 'policy' => $policy,
- 'attributes' => $attributes,
- 'relations' => $relations,
- 'events' => $events,
- 'observers' => $observers,
- ])->toJson()
- );
- }
- /**
- * Render the model information for the CLI.
- *
- * @param string $class
- * @param string $database
- * @param string $table
- * @param string $policy
- * @param \Illuminate\Support\Collection $attributes
- * @param \Illuminate\Support\Collection $relations
- * @param \Illuminate\Support\Collection $events
- * @param \Illuminate\Support\Collection $observers
- * @return void
- */
- protected function displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
- {
- $this->newLine();
- $this->components->twoColumnDetail('<fg=green;options=bold>'.$class.'</>');
- $this->components->twoColumnDetail('Database', $database);
- $this->components->twoColumnDetail('Table', $table);
- if ($policy) {
- $this->components->twoColumnDetail('Policy', $policy);
- }
- $this->newLine();
- $this->components->twoColumnDetail(
- '<fg=green;options=bold>Attributes</>',
- 'type <fg=gray>/</> <fg=yellow;options=bold>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('<fg=gray>%s</>', $property))
- ->implode('<fg=gray>,</> ')
- ));
- $second = collect([
- $attribute['type'],
- $attribute['cast'] ? '<fg=yellow;options=bold>'.$attribute['cast'].'</>' : null,
- ])->filter()->implode(' <fg=gray>/</> ');
- $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('<fg=green;options=bold>Relations</>');
- foreach ($relations as $relation) {
- $this->components->twoColumnDetail(
- sprintf('%s <fg=gray>%s</>', $relation['name'], $relation['type']),
- $relation['related']
- );
- }
- $this->newLine();
- $this->components->twoColumnDetail('<fg=green;options=bold>Events</>');
- if ($events->count()) {
- foreach ($events as $event) {
- $this->components->twoColumnDetail(
- sprintf('%s', $event['event']),
- sprintf('%s', $event['class']),
- );
- }
- }
- $this->newLine();
- $this->components->twoColumnDetail('<fg=green;options=bold>Observers</>');
- if ($observers->count()) {
- foreach ($observers as $observer) {
- $this->components->twoColumnDetail(
- sprintf('%s', $observer['event']),
- implode(', ', $observer['observer'])
- );
- }
- }
- $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 match (true) {
- $attributeDefault instanceof BackedEnum => $attributeDefault->value,
- $attributeDefault instanceof UnitEnum => $attributeDefault->name,
- default => $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;
- }
- }
|