This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 additions and 0 deletions
@@ -0,0 +1,286 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Initialize;
use Illuminate\Database\Eloquent\Attributes\Unguarded;
trait GuardsAttributes
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>
*/
protected $guarded = ['*'];
/**
* Indicates if all mass assignment is enabled.
*
* @var bool
*/
protected static $unguarded = false;
/**
* The actual columns that exist on the database and can be guarded.
*
* @var array<class-string,list<string>>
*/
protected static $guardableColumns = [];
/**
* Initialize the GuardsAttributes trait.
*
* @return void
*/
#[Initialize]
public function initializeGuardsAttributes()
{
$this->mergeFillable(static::resolveClassAttribute(Fillable::class, 'columns') ?? []);
if ($this->guarded === ['*']) {
if (static::resolveClassAttribute(Unguarded::class) !== null) {
$this->guarded = [];
} else {
$this->guarded = static::resolveClassAttribute(Guarded::class, 'columns') ?? ['*'];
}
}
}
/**
* Get the fillable attributes for the model.
*
* @return array<string>
*/
public function getFillable()
{
return $this->fillable;
}
/**
* Set the fillable attributes for the model.
*
* @param array<string> $fillable
* @return $this
*/
public function fillable(array $fillable)
{
$this->fillable = $fillable;
return $this;
}
/**
* Merge new fillable attributes with existing fillable attributes on the model.
*
* @param array<string> $fillable
* @return $this
*/
public function mergeFillable(array $fillable)
{
$this->fillable = array_values(array_unique(array_merge($this->fillable, $fillable)));
return $this;
}
/**
* Get the guarded attributes for the model.
*
* @return array<string>
*/
public function getGuarded()
{
return self::$unguarded === true
? []
: $this->guarded;
}
/**
* Set the guarded attributes for the model.
*
* @param array<string> $guarded
* @return $this
*/
public function guard(array $guarded)
{
$this->guarded = $guarded;
return $this;
}
/**
* Merge new guarded attributes with existing guarded attributes on the model.
*
* @param array<string> $guarded
* @return $this
*/
public function mergeGuarded(array $guarded)
{
$this->guarded = array_values(array_unique(array_merge($this->guarded, $guarded)));
return $this;
}
/**
* Disable all mass assignable restrictions.
*
* @param bool $state
* @return void
*/
public static function unguard($state = true)
{
static::$unguarded = $state;
}
/**
* Enable the mass assignment restrictions.
*
* @return void
*/
public static function reguard()
{
static::$unguarded = false;
}
/**
* Determine if the current state is "unguarded".
*
* @return bool
*/
public static function isUnguarded()
{
return static::$unguarded;
}
/**
* Run the given callable while being unguarded.
*
* @template TReturn
*
* @param callable(): TReturn $callback
* @return TReturn
*/
public static function unguarded(callable $callback)
{
if (static::$unguarded) {
return $callback();
}
static::unguard();
try {
return $callback();
} finally {
static::reguard();
}
}
/**
* Determine if the given attribute may be mass assigned.
*
* @param string $key
* @return bool
*/
public function isFillable($key)
{
if (static::$unguarded) {
return true;
}
// If the key is in the "fillable" array, we can of course assume that it's
// a fillable attribute. Otherwise, we will check the guarded array when
// we need to determine if the attribute is black-listed on the model.
if (in_array($key, $this->getFillable())) {
return true;
}
// If the attribute is explicitly listed in the "guarded" array then we can
// return false immediately. This means this attribute is definitely not
// fillable and there is no point in going any further in this method.
if ($this->isGuarded($key)) {
return false;
}
return empty($this->getFillable()) &&
! str_contains($key, '.') &&
! str_starts_with($key, '_');
}
/**
* Determine if the given key is guarded.
*
* @param string $key
* @return bool
*/
public function isGuarded($key)
{
if (empty($this->getGuarded())) {
return false;
}
return $this->getGuarded() == ['*'] ||
! empty(preg_grep('/^'.preg_quote($key, '/').'$/i', $this->getGuarded())) ||
! $this->isGuardableColumn($key);
}
/**
* Determine if the given column is a valid, guardable column.
*
* @param string $key
* @return bool
*/
protected function isGuardableColumn($key)
{
if ($this->hasSetMutator($key) || $this->hasAttributeSetMutator($key) || $this->isClassCastable($key)) {
return true;
}
if (! isset(static::$guardableColumns[get_class($this)])) {
$columns = $this->getConnection()
->getSchemaBuilder()
->getColumnListing($this->getTable());
if (empty($columns)) {
return true;
}
static::$guardableColumns[get_class($this)] = $columns;
}
return in_array($key, static::$guardableColumns[get_class($this)]);
}
/**
* Determine if the model is totally guarded.
*
* @return bool
*/
public function totallyGuarded()
{
return count($this->getFillable()) === 0 && $this->getGuarded() == ['*'];
}
/**
* Get the fillable attributes of a given array.
*
* @param array<string, mixed> $attributes
* @return array<string, mixed>
*/
protected function fillableFromArray(array $attributes)
{
if (count($this->getFillable()) > 0 && ! static::$unguarded) {
return array_intersect_key($attributes, array_flip($this->getFillable()));
}
return $attributes;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,461 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use ReflectionClass;
trait HasEvents
{
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array<string, class-string>
*/
protected $dispatchesEvents = [];
/**
* User exposed observable events.
*
* These are extra user-defined events observers may subscribe to.
*
* @var string[]
*/
protected $observables = [];
/**
* Boot the has event trait for a model.
*
* @return void
*/
public static function bootHasEvents()
{
static::whenBooted(fn () => static::observe(static::resolveObserveAttributes()));
}
/**
* Resolve the observe class names from the attributes.
*
* @return array
*/
public static function resolveObserveAttributes()
{
$reflectionClass = new ReflectionClass(static::class);
$isEloquentGrandchild = is_subclass_of(static::class, Model::class)
&& get_parent_class(static::class) !== Model::class;
return (new Collection($reflectionClass->getAttributes(ObservedBy::class)))
->map(fn ($attribute) => $attribute->getArguments())
->flatten()
->when($isEloquentGrandchild, function (Collection $attributes) {
return (new Collection(get_parent_class(static::class)::resolveObserveAttributes()))
->merge($attributes);
})
->all();
}
/**
* Register observers with the model.
*
* @param object|string[]|string $classes
* @return void
*
* @throws \RuntimeException
*/
public static function observe($classes)
{
$instance = new static;
foreach (Arr::wrap($classes) as $class) {
$instance->registerObserver($class);
}
}
/**
* Register a single observer with the model.
*
* @param object|string $class
* @return void
*
* @throws \RuntimeException
*/
protected function registerObserver($class)
{
$className = $this->resolveObserverClassName($class);
// When registering a model observer, we will spin through the possible events
// and determine if this observer has that method. If it does, we will hook
// it into the model's event system, making it convenient to watch these.
foreach ($this->getObservableEvents() as $event) {
if (method_exists($class, $event)) {
static::registerModelEvent($event, $className.'@'.$event);
}
}
}
/**
* Resolve the observer's class name from an object or string.
*
* @param object|string $class
* @return class-string
*
* @throws \InvalidArgumentException
*/
private function resolveObserverClassName($class)
{
if (is_object($class)) {
return get_class($class);
}
if (class_exists($class)) {
return $class;
}
throw new InvalidArgumentException('Unable to find observer: '.$class);
}
/**
* Get the observable event names.
*
* @return string[]
*/
public function getObservableEvents()
{
return array_merge(
[
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored', 'replicating',
'trashed', 'deleting', 'deleted', 'forceDeleting', 'forceDeleted',
],
$this->observables
);
}
/**
* Set the observable event names.
*
* @param string[] $observables
* @return $this
*/
public function setObservableEvents(array $observables)
{
$this->observables = $observables;
return $this;
}
/**
* Add an observable event name.
*
* @param string|string[] $observables
* @return void
*/
public function addObservableEvents($observables)
{
$this->observables = array_unique(array_merge(
$this->observables, is_array($observables) ? $observables : func_get_args()
));
}
/**
* Remove an observable event name.
*
* @param string|string[] $observables
* @return void
*/
public function removeObservableEvents($observables)
{
$this->observables = array_diff(
$this->observables, is_array($observables) ? $observables : func_get_args()
);
}
/**
* Register a model event with the dispatcher.
*
* @param string $event
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
protected static function registerModelEvent($event, $callback)
{
if (isset(static::$dispatcher)) {
$name = static::class;
static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback);
}
}
/**
* Fire the given event for the model.
*
* @param string $event
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent($event, $halt = true)
{
if (! isset(static::$dispatcher)) {
return true;
}
// First, we will get the proper method to call on the event dispatcher, and then we
// will attempt to fire a custom, object based event for the given event. If that
// returns a result we can return that result, or we'll call the string events.
$method = $halt ? 'until' : 'dispatch';
$result = $this->filterModelEventResults(
$this->fireCustomModelEvent($event, $method)
);
if ($result === false) {
return false;
}
return ! empty($result) ? $result : static::$dispatcher->{$method}(
"eloquent.{$event}: ".static::class, $this
);
}
/**
* Fire a custom model event for the given event.
*
* @param string $event
* @param 'until'|'dispatch' $method
* @return array|null|void
*/
protected function fireCustomModelEvent($event, $method)
{
if (! isset($this->dispatchesEvents[$event])) {
return;
}
$result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this));
if (! is_null($result)) {
return $result;
}
}
/**
* Filter the model event results.
*
* @param mixed $result
* @return mixed
*/
protected function filterModelEventResults($result)
{
if (is_array($result)) {
$result = array_filter($result, function ($response) {
return ! is_null($response);
});
}
return $result;
}
/**
* Register a retrieved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function retrieved($callback)
{
static::registerModelEvent('retrieved', $callback);
}
/**
* Register a saving model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function saving($callback)
{
static::registerModelEvent('saving', $callback);
}
/**
* Register a saved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function saved($callback)
{
static::registerModelEvent('saved', $callback);
}
/**
* Register an updating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function updating($callback)
{
static::registerModelEvent('updating', $callback);
}
/**
* Register an updated model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function updated($callback)
{
static::registerModelEvent('updated', $callback);
}
/**
* Register a creating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function creating($callback)
{
static::registerModelEvent('creating', $callback);
}
/**
* Register a created model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function created($callback)
{
static::registerModelEvent('created', $callback);
}
/**
* Register a replicating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function replicating($callback)
{
static::registerModelEvent('replicating', $callback);
}
/**
* Register a deleting model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function deleting($callback)
{
static::registerModelEvent('deleting', $callback);
}
/**
* Register a deleted model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function deleted($callback)
{
static::registerModelEvent('deleted', $callback);
}
/**
* Remove all the event listeners for the model.
*
* @return void
*/
public static function flushEventListeners()
{
if (! isset(static::$dispatcher)) {
return;
}
$instance = new static;
foreach ($instance->getObservableEvents() as $event) {
static::$dispatcher->forget("eloquent.{$event}: ".static::class);
}
foreach ($instance->dispatchesEvents as $event) {
static::$dispatcher->forget($event);
}
}
/**
* Get the event map for the model.
*
* @return array
*/
public function dispatchesEvents()
{
return $this->dispatchesEvents;
}
/**
* Get the event dispatcher instance.
*
* @return \Illuminate\Contracts\Events\Dispatcher|null
*/
public static function getEventDispatcher()
{
return static::$dispatcher;
}
/**
* Set the event dispatcher instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public static function setEventDispatcher(Dispatcher $dispatcher)
{
static::$dispatcher = $dispatcher;
}
/**
* Unset the event dispatcher for models.
*
* @return void
*/
public static function unsetEventDispatcher()
{
static::$dispatcher = null;
}
/**
* Execute a callback without firing any model events for any model type.
*
* @param callable $callback
* @return mixed
*/
public static function withoutEvents(callable $callback)
{
$dispatcher = static::getEventDispatcher();
if ($dispatcher) {
static::setEventDispatcher(new NullDispatcher($dispatcher));
}
try {
return $callback();
} finally {
if ($dispatcher) {
static::setEventDispatcher($dispatcher);
}
}
}
}
@@ -0,0 +1,153 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use ReflectionAttribute;
use ReflectionClass;
trait HasGlobalScopes
{
/**
* Boot the has global scopes trait for a model.
*
* @return void
*/
public static function bootHasGlobalScopes()
{
static::addGlobalScopes(static::resolveGlobalScopeAttributes());
}
/**
* Resolve the global scope class names from the attributes.
*
* @return array
*/
public static function resolveGlobalScopeAttributes()
{
$reflectionClass = new ReflectionClass(static::class);
$attributes = (new Collection($reflectionClass->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF)));
foreach ($reflectionClass->getTraits() as $trait) {
$attributes->push(...$trait->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF));
}
$isEloquentGrandchild = is_subclass_of(static::class, Model::class)
&& get_parent_class(static::class) !== Model::class;
return $attributes->map(fn ($attribute) => $attribute->getArguments())
->flatten()
->when($isEloquentGrandchild, function (Collection $attributes) {
return (new Collection(get_parent_class(static::class)::resolveGlobalScopeAttributes()))
->merge($attributes);
})
->all();
}
/**
* Register a new global scope on the model.
*
* @param \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder<static>): mixed)|string $scope
* @param \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder<static>): mixed)|null $implementation
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function addGlobalScope($scope, $implementation = null)
{
if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) {
return static::$globalScopes[static::class][$scope] = $implementation;
} elseif ($scope instanceof Closure) {
return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
} elseif ($scope instanceof Scope) {
return static::$globalScopes[static::class][get_class($scope)] = $scope;
} elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) {
return static::$globalScopes[static::class][$scope] = new $scope;
}
throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope or be a class name of a class extending '.Scope::class);
}
/**
* Register multiple global scopes on the model.
*
* @param array $scopes
* @return void
*/
public static function addGlobalScopes(array $scopes)
{
foreach ($scopes as $key => $scope) {
if (is_string($key)) {
static::addGlobalScope($key, $scope);
} else {
static::addGlobalScope($scope);
}
}
}
/**
* Determine if a model has a global scope.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return bool
*/
public static function hasGlobalScope($scope)
{
return ! is_null(static::getGlobalScope($scope));
}
/**
* Get a global scope registered with the model.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder<static>): mixed)|null
*/
public static function getGlobalScope($scope)
{
if (is_string($scope)) {
return Arr::get(static::$globalScopes, static::class.'.'.$scope);
}
return Arr::get(
static::$globalScopes, static::class.'.'.get_class($scope)
);
}
/**
* Get all of the global scopes that are currently registered.
*
* @return array
*/
public static function getAllGlobalScopes()
{
return static::$globalScopes;
}
/**
* Set the current global scopes.
*
* @param array $scopes
* @return void
*/
public static function setAllGlobalScopes($scopes)
{
static::$globalScopes = $scopes;
}
/**
* Get the global scopes for this class instance.
*
* @return array
*/
public function getGlobalScopes()
{
return Arr::get(static::$globalScopes, static::class, []);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,256 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\Attributes\Initialize;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Date;
trait HasTimestamps
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
/**
* The list of models classes that have timestamps temporarily disabled.
*
* @var array
*/
protected static $ignoreTimestampsOn = [];
/**
* Initialize the HasTimestamps trait.
*
* @return void
*/
#[Initialize]
public function initializeHasTimestamps()
{
if ($this->timestamps === true) {
if (static::resolveClassAttribute(WithoutTimestamps::class) !== null) {
$this->timestamps = false;
} elseif (($table = static::resolveClassAttribute(Table::class)) && $table->timestamps !== null) {
$this->timestamps = $table->timestamps;
}
}
}
/**
* Update the model's update timestamp.
*
* @param array|string|null $attribute
* @return bool
*/
public function touch($attribute = null)
{
if ($attribute) {
$time = $this->freshTimestamp();
foreach (Arr::wrap($attribute) as $column) {
$this->{$column} = $time;
}
return $this->save();
}
if (! $this->usesTimestamps()) {
return false;
}
$this->updateTimestamps();
return $this->save();
}
/**
* Update the model's update timestamp without raising any events.
*
* @param array|string|null $attribute
* @return bool
*/
public function touchQuietly($attribute = null)
{
return static::withoutEvents(fn () => $this->touch($attribute));
}
/**
* Update the creation and update timestamps.
*
* @return $this
*/
public function updateTimestamps()
{
$time = $this->freshTimestamp();
$updatedAtColumn = $this->getUpdatedAtColumn();
if (! is_null($updatedAtColumn) && ! $this->isDirty($updatedAtColumn)) {
$this->setUpdatedAt($time);
}
$createdAtColumn = $this->getCreatedAtColumn();
if (! $this->exists && ! is_null($createdAtColumn) && ! $this->isDirty($createdAtColumn)) {
$this->setCreatedAt($time);
}
return $this;
}
/**
* Set the value of the "created at" attribute.
*
* @param mixed $value
* @return $this
*/
public function setCreatedAt($value)
{
$this->{$this->getCreatedAtColumn()} = $value;
return $this;
}
/**
* Set the value of the "updated at" attribute.
*
* @param mixed $value
* @return $this
*/
public function setUpdatedAt($value)
{
$this->{$this->getUpdatedAtColumn()} = $value;
return $this;
}
/**
* Get a fresh timestamp for the model.
*
* @return \Illuminate\Support\Carbon
*/
public function freshTimestamp()
{
return Date::now();
}
/**
* Get a fresh timestamp for the model.
*
* @return string
*/
public function freshTimestampString()
{
return $this->fromDateTime($this->freshTimestamp());
}
/**
* Determine if the model uses timestamps.
*
* @return bool
*/
public function usesTimestamps()
{
return $this->timestamps && ! static::isIgnoringTimestamps($this::class);
}
/**
* Get the name of the "created at" column.
*
* @return string|null
*/
public function getCreatedAtColumn()
{
return static::CREATED_AT;
}
/**
* Get the name of the "updated at" column.
*
* @return string|null
*/
public function getUpdatedAtColumn()
{
return static::UPDATED_AT;
}
/**
* Get the fully-qualified "created at" column.
*
* @return string|null
*/
public function getQualifiedCreatedAtColumn()
{
$column = $this->getCreatedAtColumn();
return $column ? $this->qualifyColumn($column) : null;
}
/**
* Get the fully-qualified "updated at" column.
*
* @return string|null
*/
public function getQualifiedUpdatedAtColumn()
{
$column = $this->getUpdatedAtColumn();
return $column ? $this->qualifyColumn($column) : null;
}
/**
* Disable timestamps for the current class during the given callback scope.
*
* @return mixed
*/
public static function withoutTimestamps(callable $callback)
{
return static::withoutTimestampsOn([static::class], $callback);
}
/**
* Disable timestamps for the given model classes during the given callback scope.
*
* @param array $models
* @param callable $callback
* @return mixed
*/
public static function withoutTimestampsOn($models, $callback)
{
static::$ignoreTimestampsOn = array_values(array_merge(static::$ignoreTimestampsOn, $models));
try {
return $callback();
} finally {
foreach ($models as $model) {
if (($key = array_search($model, static::$ignoreTimestampsOn, true)) !== false) {
unset(static::$ignoreTimestampsOn[$key]);
}
}
}
}
/**
* Determine if the given model is ignoring timestamps / touches.
*
* @param string|null $class
* @return bool
*/
public static function isIgnoringTimestamps($class = null)
{
$class ??= static::class;
foreach (static::$ignoreTimestampsOn as $ignoredClass) {
if ($class === $ignoredClass || is_subclass_of($class, $ignoredClass)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,31 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Str;
trait HasUlids
{
use HasUniqueStringIds;
/**
* Generate a new unique key for the model.
*
* @return string
*/
public function newUniqueId()
{
return strtolower((string) Str::ulid());
}
/**
* Determine if given key is valid.
*
* @param mixed $value
* @return bool
*/
protected function isValidUniqueId($value): bool
{
return Str::isUlid($value);
}
}
@@ -0,0 +1,57 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
trait HasUniqueIds
{
/**
* Indicates if the model uses unique IDs.
*
* @var bool
*/
public $usesUniqueIds = false;
/**
* Determine if the model uses unique IDs.
*
* @return bool
*/
public function usesUniqueIds()
{
return $this->usesUniqueIds;
}
/**
* Generate unique keys for the model.
*
* @return void
*/
public function setUniqueIds()
{
foreach ($this->uniqueIds() as $column) {
if (empty($this->{$column})) {
$this->{$column} = $this->newUniqueId();
}
}
}
/**
* Generate a new key for the model.
*
* @return string
*/
public function newUniqueId()
{
return null;
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return [];
}
}
@@ -0,0 +1,108 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\ModelNotFoundException;
trait HasUniqueStringIds
{
/**
* Generate a new unique key for the model.
*
* @return mixed
*/
abstract public function newUniqueId();
/**
* Determine if given key is valid.
*
* @param mixed $value
* @return bool
*/
abstract protected function isValidUniqueId($value): bool;
/**
* Initialize the trait.
*
* @return void
*/
public function initializeHasUniqueStringIds()
{
$this->usesUniqueIds = true;
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return $this->usesUniqueIds() ? [$this->getKeyName()] : parent::uniqueIds();
}
/**
* Retrieve the model for a bound value.
*
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $query
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Contracts\Database\Eloquent\Builder
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function resolveRouteBindingQuery($query, $value, $field = null)
{
if ($field && in_array($field, $this->uniqueIds()) && ! $this->isValidUniqueId($value)) {
$this->handleInvalidUniqueId($value, $field);
}
if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! $this->isValidUniqueId($value)) {
$this->handleInvalidUniqueId($value, $field);
}
return parent::resolveRouteBindingQuery($query, $value, $field);
}
/**
* Get the auto-incrementing key type.
*
* @return string
*/
public function getKeyType()
{
if (in_array($this->getKeyName(), $this->uniqueIds())) {
return 'string';
}
return parent::getKeyType();
}
/**
* Get the value indicating whether the IDs are incrementing.
*
* @return bool
*/
public function getIncrementing()
{
if (in_array($this->getKeyName(), $this->uniqueIds())) {
return false;
}
return parent::getIncrementing();
}
/**
* Throw an exception for the given invalid unique ID.
*
* @param mixed $value
* @param string|null $field
* @return never
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
protected function handleInvalidUniqueId($value, $field)
{
throw (new ModelNotFoundException)->setModel(get_class($this), $value);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Str;
trait HasUuids
{
use HasUniqueStringIds;
/**
* Generate a new unique key for the model.
*
* @return string
*/
public function newUniqueId()
{
return (string) Str::uuid7();
}
/**
* Determine if given key is valid.
*
* @param mixed $value
* @return bool
*/
protected function isValidUniqueId($value): bool
{
return Str::isUuid($value);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Str;
trait HasVersion4Uuids
{
use HasUuids;
/**
* Generate a new UUID (version 4) for the model.
*
* @return string
*/
public function newUniqueId()
{
return (string) Str::orderedUuid();
}
}
@@ -0,0 +1,166 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\Initialize;
use Illuminate\Database\Eloquent\Attributes\Visible;
trait HidesAttributes
{
/**
* The attributes that should be hidden for serialization.
*
* @var array<string>
*/
protected $hidden = [];
/**
* The attributes that should be visible in serialization.
*
* @var array<string>
*/
protected $visible = [];
/**
* Initialize the HidesAttributes trait.
*
* @return void
*/
#[Initialize]
public function initializeHidesAttributes()
{
$this->mergeHidden(static::resolveClassAttribute(Hidden::class, 'columns') ?? []);
$this->mergeVisible(static::resolveClassAttribute(Visible::class, 'columns') ?? []);
}
/**
* Get the hidden attributes for the model.
*
* @return array<string>
*/
public function getHidden()
{
return $this->hidden;
}
/**
* Set the hidden attributes for the model.
*
* @param array<string> $hidden
* @return $this
*/
public function setHidden(array $hidden)
{
$this->hidden = $hidden;
return $this;
}
/**
* Merge new hidden attributes with existing hidden attributes on the model.
*
* @param array<string> $hidden
* @return $this
*/
public function mergeHidden(array $hidden)
{
$this->hidden = array_values(array_unique(array_merge($this->hidden, $hidden)));
return $this;
}
/**
* Get the visible attributes for the model.
*
* @return array<string>
*/
public function getVisible()
{
return $this->visible;
}
/**
* Set the visible attributes for the model.
*
* @param array<string> $visible
* @return $this
*/
public function setVisible(array $visible)
{
$this->visible = $visible;
return $this;
}
/**
* Merge new visible attributes with existing visible attributes on the model.
*
* @param array<string> $visible
* @return $this
*/
public function mergeVisible(array $visible)
{
$this->visible = array_values(array_unique(array_merge($this->visible, $visible)));
return $this;
}
/**
* Make the given, typically hidden, attributes visible.
*
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeVisible($attributes)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->hidden = array_diff($this->hidden, $attributes);
if (! empty($this->visible)) {
$this->visible = array_values(array_unique(array_merge($this->visible, $attributes)));
}
return $this;
}
/**
* Make the given, typically hidden, attributes visible if the given truth test passes.
*
* @param bool|\Closure $condition
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeVisibleIf($condition, $attributes)
{
return value($condition, $this) ? $this->makeVisible($attributes) : $this;
}
/**
* Make the given, typically visible, attributes hidden.
*
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeHidden($attributes)
{
$this->hidden = array_values(array_unique(array_merge(
$this->hidden, is_array($attributes) ? $attributes : func_get_args()
)));
return $this;
}
/**
* Make the given, typically visible, attributes hidden if the given truth test passes.
*
* @param bool|\Closure $condition
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeHiddenIf($condition, $attributes)
{
return value($condition, $this) ? $this->makeHidden($attributes) : $this;
}
}
@@ -0,0 +1,107 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Arr;
use Illuminate\Support\Onceable;
use WeakMap;
trait PreventsCircularRecursion
{
/**
* The cache of objects processed to prevent infinite recursion.
*
* @var WeakMap<static, array<string, mixed>>
*/
protected static $recursionCache;
/**
* Prevent a method from being called multiple times on the same object within the same call stack.
*
* @param callable $callback
* @param mixed $default
* @return mixed
*/
protected function withoutRecursion($callback, $default = null)
{
$trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$onceable = Onceable::tryFromTrace($trace, $callback);
if (is_null($onceable)) {
return call_user_func($callback);
}
$stack = static::getRecursiveCallStack($this);
if (array_key_exists($onceable->hash, $stack)) {
return is_callable($stack[$onceable->hash])
? static::setRecursiveCallValue($this, $onceable->hash, call_user_func($stack[$onceable->hash]))
: $stack[$onceable->hash];
}
try {
static::setRecursiveCallValue($this, $onceable->hash, $default);
return call_user_func($onceable->callable);
} finally {
static::clearRecursiveCallValue($this, $onceable->hash);
}
}
/**
* Remove an entry from the recursion cache for an object.
*
* @param object $object
* @param string $hash
*/
protected static function clearRecursiveCallValue($object, string $hash)
{
if ($stack = Arr::except(static::getRecursiveCallStack($object), $hash)) {
static::getRecursionCache()->offsetSet($object, $stack);
} elseif (static::getRecursionCache()->offsetExists($object)) {
static::getRecursionCache()->offsetUnset($object);
}
}
/**
* Get the stack of methods being called recursively for the current object.
*
* @param object $object
* @return array
*/
protected static function getRecursiveCallStack($object): array
{
return static::getRecursionCache()->offsetExists($object)
? static::getRecursionCache()->offsetGet($object)
: [];
}
/**
* Get the current recursion cache being used by the model.
*
* @return WeakMap
*/
protected static function getRecursionCache()
{
return static::$recursionCache ??= new WeakMap();
}
/**
* Set a value in the recursion cache for the given object and method.
*
* @param object $object
* @param string $hash
* @param mixed $value
* @return mixed
*/
protected static function setRecursiveCallValue($object, string $hash, $value)
{
static::getRecursionCache()->offsetSet(
$object,
tap(static::getRecursiveCallStack($object), fn (&$stack) => $stack[$hash] = $value),
);
return static::getRecursiveCallStack($object)[$hash];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,99 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\Attributes\UseResource;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Str;
use LogicException;
use ReflectionClass;
trait TransformsToResource
{
/**
* Create a new resource object for the given resource.
*
* @param class-string<\Illuminate\Http\Resources\Json\JsonResource>|null $resourceClass
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function toResource(?string $resourceClass = null): JsonResource
{
if ($resourceClass === null) {
return $this->guessResource();
}
return $resourceClass::make($this);
}
/**
* Guess the resource class for the model.
*
* @return \Illuminate\Http\Resources\Json\JsonResource
*
* @throws \LogicException
*/
protected function guessResource(): JsonResource
{
$resourceClass = $this->resolveResourceFromAttribute(static::class);
if ($resourceClass !== null && class_exists($resourceClass)) {
return $resourceClass::make($this);
}
foreach (static::guessResourceName() as $resourceClass) {
if (is_string($resourceClass) && class_exists($resourceClass)) {
return $resourceClass::make($this);
}
}
throw new LogicException(sprintf('Failed to find resource class for model [%s].', get_class($this)));
}
/**
* Guess the resource class name for the model.
*
* @return array{class-string<\Illuminate\Http\Resources\Json\JsonResource>, class-string<\Illuminate\Http\Resources\Json\JsonResource>}
*/
public static function guessResourceName(): array
{
$modelClass = static::class;
if (! Str::contains($modelClass, '\\Models\\')) {
return [];
}
$relativeNamespace = Str::after($modelClass, '\\Models\\');
$relativeNamespace = Str::contains($relativeNamespace, '\\')
? Str::before($relativeNamespace, '\\'.class_basename($modelClass))
: '';
$potentialResource = sprintf(
'%s\\Http\\Resources\\%s%s',
Str::before($modelClass, '\\Models'),
(string) $relativeNamespace !== '' ? $relativeNamespace.'\\' : '',
class_basename($modelClass)
);
return [$potentialResource.'Resource', $potentialResource];
}
/**
* Get the resource class from the class attribute.
*
* @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $class
* @return class-string<*>|null
*/
protected function resolveResourceFromAttribute(string $class): ?string
{
if (! class_exists($class)) {
return null;
}
$attributes = (new ReflectionClass($class))->getAttributes(UseResource::class);
return $attributes !== []
? $attributes[0]->newInstance()->class
: null;
}
}