layfi-earn/vendor/illuminate/database/Eloquent/ModelInspector.php

413 lines
13 KiB
PHP
Raw Permalink Normal View History

2025-01-27 20:49:47 +08:00
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use SplFileObject;
use function Illuminate\Support\enum_value;
class ModelInspector
{
/**
* The Laravel application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* The methods that can be called in a model to indicate a relation.
*
* @var array<int, string>
*/
protected $relationMethods = [
'hasMany',
'hasManyThrough',
'hasOneThrough',
'belongsToMany',
'hasOne',
'belongsTo',
'morphOne',
'morphTo',
'morphMany',
'morphToMany',
'morphedByMany',
];
/**
* Create a new model inspector instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Extract model details for the given model.
*
* @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: class-string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>}
*
* @throws BindingResolutionException
*/
public function inspect($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),
'collection' => $this->getCollectedBy($model),
'builder' => $this->getBuilder($model),
];
}
/**
* Get the column attributes for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
*/
protected function getAttributes($model)
{
$connection = $model->getConnection();
$schema = $connection->getSchemaBuilder();
$table = $model->getTable();
$columns = $schema->getColumns($table);
$indexes = $schema->getIndexes($table);
return (new BaseCollection($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 (new BaseCollection($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) => (new BaseCollection($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 (new BaseCollection(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 (new BaseCollection($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 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
*/
protected function getEvents($model)
{
return (new BaseCollection($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 new BaseCollection($formatted);
}
/**
* Get the collection class being used by the model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return class-string<\Illuminate\Database\Eloquent\Collection>
*/
protected function getCollectedBy($model)
{
return $model->newCollection()::class;
}
/**
* Get the builder class being used by the model.
*
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @param TModel $model
* @return class-string<\Illuminate\Database\Eloquent\Builder<TModel>>
*/
protected function getBuilder($model)
{
return $model->newQuery()::class;
}
/**
* 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 (new BaseCollection($model->getDates()))
->filter()
->flip()
->map(fn () => 'datetime')
->merge($model->getCasts());
}
/**
* 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;
}
/**
* Get the default value for the given column.
*
* @param array<string, mixed> $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.
*
* @param string $column
* @param array $indexes
* @return bool
*/
protected function columnIsUnique($column, $indexes)
{
return (new BaseCollection($indexes))->contains(
fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique']
);
}
}