ShowModelCommand.php
TLDR
The ShowModelCommand.php
file in the Illuminate\Database\Console
namespace is used to display information about an Eloquent model. It provides methods to handle the console command, retrieve the model's policy, get the column attributes, get the virtual (non-column) attributes, get the model's relations, and get the observers watching the model. The file also contains methods for rendering the model information as JSON or for display in the CLI.
Methods
handle
This method is responsible for executing the console command. It ensures dependencies exist, qualifies the model, retrieves the model instance, sets the database connection if specified, and displays the model information.
getPolicy
This method is used to get the first policy associated with the model. It returns the policy class or null
if no policy is found.
getAttributes
This method fetches the column attributes for the given model. It retrieves the table columns and indexes from the database schema, maps them to an array of attribute details, and merges the virtual attributes.
getVirtualAttributes
This method retrieves the virtual (non-column) attributes for the given model. It uses reflection to get the class methods and filters out static, abstract, and inherited methods. It determines whether the method is an accessor, mutator, or neither, and formats the virtual attribute details.
getRelations
This method fetches the relations from the given model. It uses reflection to get the class methods, analyzes the code to check for relation method calls, and formats the relation details.
getObservers
This method retrieves the observers watching the model. It retrieves the raw event listeners and filters them based on the model's class name. It formats the observer details and returns them as a collection.
display
This method renders the model information. It calls either the displayJson
or the displayCli
method based on the command option.
displayJson
This method renders the model information as JSON. It converts the model details and collections to a JSON string and writes it to the output.
displayCli
This method renders the model information for the CLI. It uses the twoColumnDetail
and bulletList
methods from the Illuminate\Console\Concerns\InteractsWithIO
trait to format and display the model attributes, relations, and observers.
getCastType
This method gets the cast type for the given column. It checks if the column has a getter or setter mutator and returns 'accessor'. It then checks if the column has an attribute mutator and returns 'attribute'. Otherwise, it checks the model's casts and date casts and returns the cast type, or null
if not found.
getCastsWithDates
This method gets the model casts, including any date casts. It filters out any empty casts and merges the date casts with the regular casts.
getColumnType
This method gets the type of the given column. It checks the column type name, whether it is unsigned, and returns the formatted column type.
getColumnDefault
This method gets the default value for the given column. It checks if the attribute default is an instance of BackedEnum
or UnitEnum
and returns the respective value. Otherwise, it returns the attribute default or the column default.
attributeIsHidden
This method determines if the given attribute is hidden. It checks if the attribute is in the model's hidden or visible attributes and returns a boolean value accordingly.
columnIsUnique
This method determines if the given column is unique. It filters the indexes for a column match and checks if any of them are unique.
qualifyModel
This method qualifies the given model class base name. It checks if the model already contains a namespace or if the class exists. If not, it adds the root namespace or a Models
namespace if present.
<?php
namespace Illuminate\Database\Console;
use BackedEnum;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Types\DecimalType;
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 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()
{
if (! $this->ensureDependenciesExist()) {
return 1;
}
$class = $this->qualifyModel($this->argument('model'));
try {
$model = $this->laravel->make($class);
$class = get_class($model);
} 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->getPolicy($model),
$this->getAttributes($model),
$this->getRelations($model),
$this->getObservers($model),
);
}
/**
* 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->getDoctrineSchemaManager();
$this->registerTypeMappings($connection->getDoctrineConnection()->getDatabasePlatform());
$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() === 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)->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() === Model::class
)
->filter(function (ReflectionMethod $method) {
$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 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 $observers
* @return void
*/
protected function display($class, $database, $table, $policy, $attributes, $relations, $observers)
{
$this->option('json')
? $this->displayJson($class, $database, $table, $policy, $attributes, $relations, $observers)
: $this->displayCli($class, $database, $table, $policy, $attributes, $relations, $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 $observers
* @return void
*/
protected function displayJson($class, $database, $table, $policy, $attributes, $relations, $observers)
{
$this->output->writeln(
collect([
'class' => $class,
'database' => $database,
'table' => $table,
'policy' => $policy,
'attributes' => $attributes,
'relations' => $relations,
'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 $observers
* @return void
*/
protected function displayCli($class, $database, $table, $policy, $attributes, $relations, $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>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 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 mixed|null
*/
protected function getColumnDefault($column, $model)
{
$attributeDefault = $model->getAttributes()[$column->getName()] ?? null;
return match (true) {
$attributeDefault instanceof BackedEnum => $attributeDefault->value,
$attributeDefault instanceof UnitEnum => $attributeDefault->name,
default => $attributeDefault ?? $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 (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;
}
}