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->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 ) ->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(''.$class.''); $this->components->twoColumnDetail('Database', $database); $this->components->twoColumnDetail('Table', $table); if ($policy) { $this->components->twoColumnDetail('Policy', $policy); } $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(); $this->components->twoColumnDetail('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; } }