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,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Appends
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Boot
{
//
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class CollectedBy
{
/**
* Create a new attribute instance.
*
* @param class-string<\Illuminate\Database\Eloquent\Collection<*, *>> $collectionClass
*/
public function __construct(public string $collectionClass)
{
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
use UnitEnum;
#[Attribute(Attribute::TARGET_CLASS)]
class Connection
{
/**
* Create a new attribute instance.
*
* @param UnitEnum|string $name
*/
public function __construct(public UnitEnum|string $name)
{
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class DateFormat
{
/**
* Create a new attribute instance.
*
* @param string $format
*/
public function __construct(public string $format)
{
//
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Fillable
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Guarded
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Hidden
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Initialize
{
//
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ObservedBy
{
/**
* Create a new attribute instance.
*
* @param array|string $classes
*/
public function __construct(public array|string $classes)
{
}
}
@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Scope
{
/**
* Create a new attribute instance.
*/
public function __construct()
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ScopedBy
{
/**
* Create a new attribute instance.
*
* @param array|string $classes
*/
public function __construct(public array|string $classes)
{
}
}
@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Table
{
/**
* Create a new attribute instance.
*
* @param string|null $name
* @param string|null $key
* @param string|null $keyType
* @param bool|null $incrementing
* @param bool|null $timestamps
* @param string|null $dateFormat
*/
public function __construct(
public ?string $name = null,
public ?string $key = null,
public ?string $keyType = null,
public ?bool $incrementing = null,
public ?bool $timestamps = null,
public ?string $dateFormat = null,
) {
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Touches
{
/**
* @var array<int, string>
*/
public array $relations;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$relations
*/
public function __construct(array|string ...$relations)
{
$this->relations = is_array($relations[0]) ? $relations[0] : $relations;
}
}
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Unguarded
{
//
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseEloquentBuilder
{
/**
* Create a new attribute instance.
*
* @param class-string<\Illuminate\Database\Eloquent\Builder> $builderClass
*/
public function __construct(public string $builderClass)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseFactory
{
/**
* Create a new attribute instance.
*
* @param class-string<\Illuminate\Database\Eloquent\Factories\Factory> $factoryClass
*/
public function __construct(public string $factoryClass)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UsePolicy
{
/**
* Create a new attribute instance.
*
* @param class-string<*> $class
*/
public function __construct(public string $class)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseResource
{
/**
* Create a new attribute instance.
*
* @param class-string<*> $class
*/
public function __construct(public string $class)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseResourceCollection
{
/**
* Create a new attribute instance.
*
* @param class-string<*> $class
*/
public function __construct(public string $class)
{
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Visible
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class WithoutIncrementing
{
/**
* Create a new attribute instance.
*/
public function __construct()
{
//
}
}
@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class WithoutTimestamps
{
/**
* Create a new attribute instance.
*/
public function __construct()
{
//
}
}
@@ -0,0 +1,144 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection as BaseCollection;
class BroadcastableModelEventOccurred implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
/**
* The model instance corresponding to the event.
*
* @var \Illuminate\Database\Eloquent\Model
*/
public $model;
/**
* The event name (created, updated, etc.).
*
* @var string
*/
protected $event;
/**
* The channels that the event should be broadcast on.
*
* @var array
*/
protected $channels = [];
/**
* The queue connection that should be used to queue the broadcast job.
*
* @var string
*/
public $connection;
/**
* The queue that should be used to queue the broadcast job.
*
* @var string
*/
public $queue;
/**
* Indicates whether the job should be dispatched after all database transactions have committed.
*
* @var bool|null
*/
public $afterCommit;
/**
* Create a new event instance.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $event
*/
public function __construct($model, $event)
{
$this->model = $model;
$this->event = $event;
}
/**
* The channels the event should broadcast on.
*
* @return array
*/
public function broadcastOn()
{
$channels = empty($this->channels)
? ($this->model->broadcastOn($this->event) ?: [])
: $this->channels;
return (new BaseCollection($channels))
->map(fn ($channel) => $channel instanceof Model ? new PrivateChannel($channel) : $channel)
->all();
}
/**
* The name the event should broadcast as.
*
* @return string
*/
public function broadcastAs()
{
$default = class_basename($this->model).ucfirst($this->event);
return method_exists($this->model, 'broadcastAs')
? ($this->model->broadcastAs($this->event) ?: $default)
: $default;
}
/**
* Get the data that should be sent with the broadcasted event.
*
* @return array|null
*/
public function broadcastWith()
{
return method_exists($this->model, 'broadcastWith')
? $this->model->broadcastWith($this->event)
: null;
}
/**
* Manually specify the channels the event should broadcast on.
*
* @param array $channels
* @return $this
*/
public function onChannels(array $channels)
{
$this->channels = $channels;
return $this;
}
/**
* Determine if the event should be broadcast synchronously.
*
* @return bool
*/
public function shouldBroadcastNow()
{
return $this->event === 'deleted' &&
! method_exists($this->model, 'bootSoftDeletes');
}
/**
* Get the event name.
*
* @return string
*/
public function event()
{
return $this->event;
}
}
+197
View File
@@ -0,0 +1,197 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Support\Arr;
trait BroadcastsEvents
{
/**
* Boot the event broadcasting trait.
*
* @return void
*/
public static function bootBroadcastsEvents()
{
static::created(function ($model) {
$model->broadcastCreated();
});
static::updated(function ($model) {
$model->broadcastUpdated();
});
if (method_exists(static::class, 'bootSoftDeletes')) {
static::softDeleted(function ($model) {
$model->broadcastTrashed();
});
static::restored(function ($model) {
$model->broadcastRestored();
});
}
static::deleted(function ($model) {
$model->broadcastDeleted();
});
}
/**
* Broadcast that the model was created.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastCreated($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('created'), 'created', $channels
);
}
/**
* Broadcast that the model was updated.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastUpdated($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('updated'), 'updated', $channels
);
}
/**
* Broadcast that the model was trashed.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastTrashed($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('trashed'), 'trashed', $channels
);
}
/**
* Broadcast that the model was restored.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastRestored($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('restored'), 'restored', $channels
);
}
/**
* Broadcast that the model was deleted.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastDeleted($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('deleted'), 'deleted', $channels
);
}
/**
* Broadcast the given event instance if channels are configured for the model event.
*
* @param mixed $instance
* @param string $event
* @param mixed $channels
* @return \Illuminate\Broadcasting\PendingBroadcast|null
*/
protected function broadcastIfBroadcastChannelsExistForEvent($instance, $event, $channels = null)
{
if (! static::$isBroadcasting) {
return;
}
if (! empty($this->broadcastOn($event)) || ! empty($channels)) {
return broadcast($instance->onChannels(Arr::wrap($channels)));
}
}
/**
* Create a new broadcastable model event event.
*
* @param string $event
* @return mixed
*/
public function newBroadcastableModelEvent($event)
{
return tap($this->newBroadcastableEvent($event), function ($event) {
$event->connection = property_exists($this, 'broadcastConnection')
? $this->broadcastConnection
: $this->broadcastConnection();
$event->queue = property_exists($this, 'broadcastQueue')
? $this->broadcastQueue
: $this->broadcastQueue();
$event->afterCommit = property_exists($this, 'broadcastAfterCommit')
? $this->broadcastAfterCommit
: $this->broadcastAfterCommit();
});
}
/**
* Create a new broadcastable model event for the model.
*
* @param string $event
* @return \Illuminate\Database\Eloquent\BroadcastableModelEventOccurred
*/
protected function newBroadcastableEvent(string $event)
{
return new BroadcastableModelEventOccurred($this, $event);
}
/**
* Get the channels that model events should broadcast on.
*
* @param string $event
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn($event)
{
return [$this];
}
/**
* Get the queue connection that should be used to broadcast model events.
*
* @return string|null
*/
public function broadcastConnection()
{
//
}
/**
* Get the queue that should be used to broadcast model events.
*
* @return string|null
*/
public function broadcastQueue()
{
//
}
/**
* Determine if the model event broadcast queued job should be dispatched after all transactions are committed.
*
* @return bool
*/
public function broadcastAfterCommit()
{
return false;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent;
trait BroadcastsEventsAfterCommit
{
use BroadcastsEvents;
/**
* Determine if the model event broadcast queued job should be dispatched after all transactions are committed.
*
* @return bool
*/
public function broadcastAfterCommit()
{
return true;
}
}
+2368
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use ArrayObject as BaseArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use JsonSerializable;
/**
* @template TKey of array-key
* @template TItem
*
* @extends \ArrayObject<TKey, TItem>
*/
class ArrayObject extends BaseArrayObject implements Arrayable, JsonSerializable
{
/**
* Get a collection containing the underlying array.
*
* @return \Illuminate\Support\Collection
*/
public function collect()
{
return new Collection($this->getArrayCopy());
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return $this->getArrayCopy();
}
/**
* Get the array that should be JSON serialized.
*
* @return array
*/
public function jsonSerialize(): array
{
return $this->getArrayCopy();
}
}
@@ -0,0 +1,42 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class AsArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
return is_array($data) ? new ArrayObject($data, ArrayObject::ARRAY_AS_PROPS) : null;
}
public function set($model, $key, $value, $attributes)
{
return [$key => Json::encode($value)];
}
public function serialize($model, string $key, $value, array $attributes)
{
return $value->getArrayCopy();
}
};
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\BinaryCodec;
use InvalidArgumentException;
class AsBinary implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array{string} $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes
*
* @throws \InvalidArgumentException
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected string $format;
public function __construct(protected array $arguments)
{
$this->format = $this->arguments[0]
?? throw new InvalidArgumentException('The binary codec format is required.');
if (! in_array($this->format, BinaryCodec::formats(), true)) {
throw new InvalidArgumentException(sprintf(
'Unsupported binary codec format [%s]. Allowed formats are: %s.',
$this->format,
implode(', ', BinaryCodec::formats()),
));
}
}
public function get($model, $key, $value, $attributes)
{
return BinaryCodec::decode($attributes[$key] ?? null, $this->format);
}
public function set($model, $key, $value, $attributes)
{
return [$key => BinaryCodec::encode($value, $this->format)];
}
};
}
/**
* Encode / decode values as binary UUIDs.
*/
public static function uuid(): string
{
return self::class.':uuid';
}
/**
* Encode / decode values as binary ULIDs.
*/
public static function ulid(): string
{
return self::class.':ulid';
}
/**
* Encode / decode values using the given format.
*/
public static function of(string $format): string
{
return self::class.':'.$format;
}
}
@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use InvalidArgumentException;
class AsCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection<array-key, mixed>, iterable>
*
* @throws \InvalidArgumentException
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
public function __construct(protected array $arguments)
{
$this->arguments = array_pad(array_values($this->arguments), 2, '');
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
$collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0];
if (! is_a($collectionClass, Collection::class, true)) {
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
}
if (! is_array($data)) {
return null;
}
$instance = new $collectionClass($data);
if (! isset($this->arguments[1]) || ! $this->arguments[1]) {
return $instance;
}
if (is_string($this->arguments[1])) {
$this->arguments[1] = Str::parseCallback($this->arguments[1]);
}
return is_callable($this->arguments[1])
? $instance->map($this->arguments[1])
: $instance->mapInto($this->arguments[1][0]);
}
public function set($model, $key, $value, $attributes)
{
return [$key => Json::encode($value)];
}
};
}
/**
* Specify the type of object each item in the collection should be mapped to.
*
* @param array{class-string, string}|class-string $map
* @return string
*/
public static function of($map)
{
return static::using('', $map);
}
/**
* Specify the collection type for the cast.
*
* @param class-string $class
* @param array{class-string, string}|class-string|null $map
* @return string
*/
public static function using($class, $map = null)
{
if (is_array($map) && is_callable($map)) {
$map = $map[0].'@'.$map[1];
}
return static::class.':'.implode(',', [$class, $map]);
}
}
@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Facades\Crypt;
class AsEncryptedArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
if (isset($attributes[$key])) {
return new ArrayObject(Json::decode(Crypt::decryptString($attributes[$key])), ArrayObject::ARRAY_AS_PROPS);
}
return null;
}
public function set($model, $key, $value, $attributes)
{
if (! is_null($value)) {
return [$key => Crypt::encryptString(Json::encode($value))];
}
return null;
}
public function serialize($model, string $key, $value, array $attributes)
{
return ! is_null($value) ? $value->getArrayCopy() : null;
}
};
}
}
@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use InvalidArgumentException;
class AsEncryptedCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection<array-key, mixed>, iterable>
*
* @throws \InvalidArgumentException
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
public function __construct(protected array $arguments)
{
$this->arguments = array_pad(array_values($this->arguments), 2, '');
}
public function get($model, $key, $value, $attributes)
{
$collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0];
if (! is_a($collectionClass, Collection::class, true)) {
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
}
if (! isset($attributes[$key])) {
return null;
}
$instance = new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key])));
if (! isset($this->arguments[1]) || ! $this->arguments[1]) {
return $instance;
}
if (is_string($this->arguments[1])) {
$this->arguments[1] = Str::parseCallback($this->arguments[1]);
}
return is_callable($this->arguments[1])
? $instance->map($this->arguments[1])
: $instance->mapInto($this->arguments[1][0]);
}
public function set($model, $key, $value, $attributes)
{
if (! is_null($value)) {
return [$key => Crypt::encryptString(Json::encode($value))];
}
return null;
}
};
}
/**
* Specify the type of object each item in the collection should be mapped to.
*
* @param array{class-string, string}|class-string $map
* @return string
*/
public static function of($map)
{
return static::using('', $map);
}
/**
* Specify the collection for the cast.
*
* @param class-string $class
* @param array{class-string, string}|class-string|null $map
* @return string
*/
public static function using($class, $map = null)
{
if (is_array($map) && is_callable($map)) {
$map = $map[0].'@'.$map[1];
}
return static::class.':'.implode(',', [$class, $map]);
}
}
@@ -0,0 +1,97 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use function Illuminate\Support\enum_value;
class AsEnumArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum of \UnitEnum
*
* @param array{class-string<TEnum>} $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, TEnum>, iterable<TEnum>>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected $arguments;
public function __construct(array $arguments)
{
$this->arguments = $arguments;
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
if (! is_array($data)) {
return;
}
$enumClass = $this->arguments[0];
return new ArrayObject((new Collection($data))->map(function ($value) use ($enumClass) {
return is_subclass_of($enumClass, BackedEnum::class)
? $enumClass::from($value)
: constant($enumClass.'::'.$value);
})->toArray());
}
public function set($model, $key, $value, $attributes)
{
if ($value === null) {
return [$key => null];
}
$storable = [];
foreach ($value as $enum) {
$storable[] = $this->getStorableEnumValue($enum);
}
return [$key => Json::encode($storable)];
}
public function serialize($model, string $key, $value, array $attributes)
{
return (new Collection($value->getArrayCopy()))
->map(fn ($enum) => $this->getStorableEnumValue($enum))
->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return enum_value($enum);
}
};
}
/**
* Specify the Enum for the cast.
*
* @param class-string $class
* @return string
*/
public static function of($class)
{
return static::class.':'.$class;
}
}
@@ -0,0 +1,93 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use function Illuminate\Support\enum_value;
class AsEnumCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum of \UnitEnum
*
* @param array{class-string<TEnum>} $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection<array-key, TEnum>, iterable<TEnum>>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected $arguments;
public function __construct(array $arguments)
{
$this->arguments = $arguments;
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
if (! is_array($data)) {
return;
}
$enumClass = $this->arguments[0];
return (new Collection($data))->map(function ($value) use ($enumClass) {
return is_subclass_of($enumClass, BackedEnum::class)
? $enumClass::from($value)
: constant($enumClass.'::'.$value);
});
}
public function set($model, $key, $value, $attributes)
{
$value = $value !== null
? Json::encode((new Collection($value))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->jsonSerialize())
: null;
return [$key => $value];
}
public function serialize($model, string $key, $value, array $attributes)
{
return (new Collection($value))
->map(fn ($enum) => $this->getStorableEnumValue($enum))
->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return enum_value($enum);
}
};
}
/**
* Specify the Enum for the cast.
*
* @param class-string $class
* @return string
*/
public static function of($class)
{
return static::class.':'.$class;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Fluent;
class AsFluent implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Fluent, string>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new Fluent(Json::decode($value)) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? [$key => Json::encode($value)] : null;
}
};
}
}
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\HtmlString;
class AsHtmlString implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\HtmlString, string|HtmlString>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new HtmlString($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Stringable;
class AsStringable implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Stringable, string|\Stringable>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new Stringable($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Uri;
class AsUri implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Uri, string|Uri>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new Uri($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
class Attribute
{
/**
* The attribute accessor.
*
* @var callable
*/
public $get;
/**
* The attribute mutator.
*
* @var callable
*/
public $set;
/**
* Indicates if caching is enabled for this attribute.
*
* @var bool
*/
public $withCaching = false;
/**
* Indicates if caching of objects is enabled for this attribute.
*
* @var bool
*/
public $withObjectCaching = true;
/**
* Create a new attribute accessor / mutator.
*
* @param callable|null $get
* @param callable|null $set
*/
public function __construct(?callable $get = null, ?callable $set = null)
{
$this->get = $get;
$this->set = $set;
}
/**
* Create a new attribute accessor / mutator.
*
* @param callable|null $get
* @param callable|null $set
* @return static
*/
public static function make(?callable $get = null, ?callable $set = null): static
{
return new static($get, $set);
}
/**
* Create a new attribute accessor.
*
* @param callable $get
* @return static
*/
public static function get(callable $get)
{
return new static($get);
}
/**
* Create a new attribute mutator.
*
* @param callable $set
* @return static
*/
public static function set(callable $set)
{
return new static(null, $set);
}
/**
* Disable object caching for the attribute.
*
* @return static
*/
public function withoutObjectCaching()
{
$this->withObjectCaching = false;
return $this;
}
/**
* Enable caching for the attribute.
*
* @return static
*/
public function shouldCache()
{
$this->withCaching = true;
return $this;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
class Json
{
/**
* The custom JSON encoder.
*
* @var callable|null
*/
protected static $encoder;
/**
* The custom JSON decode.
*
* @var callable|null
*/
protected static $decoder;
/**
* Encode the given value.
*/
public static function encode(mixed $value, int $flags = 0): mixed
{
return isset(static::$encoder)
? (static::$encoder)($value, $flags)
: json_encode($value, $flags);
}
/**
* Decode the given value.
*/
public static function decode(mixed $value, ?bool $associative = true): mixed
{
return isset(static::$decoder)
? (static::$decoder)($value, $associative)
: json_decode($value, $associative);
}
/**
* Encode all values using the given callable.
*/
public static function encodeUsing(?callable $encoder): void
{
static::$encoder = $encoder;
}
/**
* Decode all values using the given callable.
*/
public static function decodeUsing(?callable $decoder): void
{
static::$decoder = $decoder;
}
}
+946
View File
@@ -0,0 +1,946 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection as BaseCollection;
use LogicException;
/**
* @template TKey of array-key
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Support\Collection<TKey, TModel>
*/
class Collection extends BaseCollection implements QueueableCollection
{
use InteractsWithDictionary;
/**
* Find a model in the collection by key.
*
* @template TFindDefault
*
* @param mixed $key
* @param TFindDefault $default
* @return ($key is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? static : TModel|TFindDefault)
*/
public function find($key, $default = null)
{
if ($key instanceof Model) {
$key = $key->getKey();
}
if ($key instanceof Arrayable) {
$key = $key->toArray();
}
if (is_array($key)) {
if ($this->isEmpty()) {
return new static;
}
return $this->whereIn($this->first()->getKeyName(), $key);
}
return Arr::first($this->items, fn ($model) => $model->getKey() == $key, $default);
}
/**
* Find a model in the collection by key or throw an exception.
*
* @param mixed $key
* @return TModel
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($key)
{
$result = $this->find($key);
if (is_array($key) && count($result) === count(array_unique($key))) {
return $result;
} elseif (! is_array($key) && ! is_null($result)) {
return $result;
}
$exception = new ModelNotFoundException;
if (! $model = head($this->items)) {
throw $exception;
}
$ids = is_array($key) ? array_diff($key, $result->modelKeys()) : $key;
$exception->setModel(get_class($model), $ids);
throw $exception;
}
/**
* Load a set of relationships onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @return $this
*/
public function load($relations)
{
if ($this->isNotEmpty()) {
if (is_string($relations)) {
$relations = func_get_args();
}
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
$this->items = $query->eagerLoadRelations($this->items);
}
return $this;
}
/**
* Load a set of aggregations over relationship's column onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @param string $column
* @param string|null $function
* @return $this
*/
public function loadAggregate($relations, $column, $function = null)
{
if ($this->isEmpty()) {
return $this;
}
$models = $this->first()->newModelQuery()
->whereKey($this->modelKeys())
->select($this->first()->getKeyName())
->withAggregate($relations, $column, $function)
->get()
->keyBy($this->first()->getKeyName());
$attributes = Arr::except(
array_keys($models->first()->getAttributes()),
$models->first()->getKeyName()
);
$this->each(function ($model) use ($models, $attributes) {
$extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes);
$model->forceFill($extraAttributes)
->syncOriginalAttributes($attributes)
->mergeCasts($models->get($model->getKey())->getCasts());
});
return $this;
}
/**
* Load a set of relationship counts onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @return $this
*/
public function loadCount($relations)
{
return $this->loadAggregate($relations, '*', 'count');
}
/**
* Load a set of relationship's max column values onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadMax($relations, $column)
{
return $this->loadAggregate($relations, $column, 'max');
}
/**
* Load a set of relationship's min column values onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadMin($relations, $column)
{
return $this->loadAggregate($relations, $column, 'min');
}
/**
* Load a set of relationship's column summations onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadSum($relations, $column)
{
return $this->loadAggregate($relations, $column, 'sum');
}
/**
* Load a set of relationship's average column values onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadAvg($relations, $column)
{
return $this->loadAggregate($relations, $column, 'avg');
}
/**
* Load a set of related existences onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @return $this
*/
public function loadExists($relations)
{
return $this->loadAggregate($relations, '*', 'exists');
}
/**
* Load a set of relationships onto the collection if they are not already eager loaded.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @return $this
*/
public function loadMissing($relations)
{
if (is_string($relations)) {
$relations = func_get_args();
}
if ($this->isNotEmpty()) {
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
foreach ($query->getEagerLoads() as $key => $value) {
$segments = explode('.', explode(':', $key)[0]);
if (str_contains($key, ':')) {
$segments[count($segments) - 1] .= ':'.explode(':', $key)[1];
}
$path = [];
foreach ($segments as $segment) {
$path[] = [$segment => $segment];
}
if (is_callable($value)) {
$path[count($segments) - 1][array_last($segments)] = $value;
}
$this->loadMissingRelation($this, $path);
}
}
return $this;
}
/**
* Load a relationship path for models of the given type if it is not already eager loaded.
*
* @param array<int, <string, class-string>> $tuples
* @return void
*/
public function loadMissingRelationshipChain(array $tuples)
{
[$relation, $class] = array_shift($tuples);
$this->filter(function ($model) use ($relation, $class) {
return ! is_null($model) &&
! $model->relationLoaded($relation) &&
$model::class === $class;
})->load($relation);
if (empty($tuples)) {
return;
}
$models = $this->pluck($relation)->whereNotNull();
if ($models->first() instanceof BaseCollection) {
$models = $models->collapse();
}
(new static($models))->loadMissingRelationshipChain($tuples);
}
/**
* Load a relationship path if it is not already eager loaded.
*
* @param \Illuminate\Database\Eloquent\Collection<int, TModel> $models
* @param array $path
* @return void
*/
protected function loadMissingRelation(self $models, array $path)
{
$relation = array_shift($path);
$name = explode(':', key($relation))[0];
if (is_string(reset($relation))) {
$relation = reset($relation);
}
$models->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name))->load($relation);
if (empty($path)) {
return;
}
$models = $models->pluck($name)->filter();
if ($models->first() instanceof BaseCollection) {
$models = $models->collapse();
}
$this->loadMissingRelation(new static($models), $path);
}
/**
* Load a set of relationships onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string> $relations
* @return $this
*/
public function loadMorph($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->load($relations[$className] ?? []));
return $this;
}
/**
* Load a set of relationship counts onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string> $relations
* @return $this
*/
public function loadMorphCount($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->loadCount($relations[$className] ?? []));
return $this;
}
/**
* Determine if a key exists in the collection.
*
* @param (callable(TModel, TKey): bool)|TModel|string|int $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function contains($key, $operator = null, $value = null)
{
if (func_num_args() > 1 || $this->useAsCallable($key)) {
return parent::contains(...func_get_args());
}
if ($key instanceof Model) {
return parent::contains(fn ($model) => $model->is($key));
}
return parent::contains(fn ($model) => $model->getKey() == $key);
}
/**
* Determine if a key does not exist in the collection.
*
* @param (callable(TModel, TKey): bool)|TModel|string|int $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function doesntContain($key, $operator = null, $value = null)
{
return ! $this->contains(...func_get_args());
}
/**
* Get the array of primary keys.
*
* @return array<int, array-key>
*/
public function modelKeys()
{
return array_map(fn ($model) => $model->getKey(), $this->items);
}
/**
* Merge the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function merge($items)
{
$dictionary = $this->getDictionary();
foreach ($items as $item) {
$key = $this->getDictionaryKey($item->getKey());
if ($key !== null) {
$dictionary[$key] = $item;
}
}
return new static(array_values($dictionary));
}
/**
* Run a map over each of the items.
*
* @template TMapValue
*
* @param callable(TModel, TKey): TMapValue $callback
* @return \Illuminate\Support\Collection<TKey, TMapValue>|static<TKey, TMapValue>
*/
public function map(callable $callback)
{
$result = parent::map($callback);
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
* Run an associative map over each of the items.
*
* The callback should return an associative array with a single key / value pair.
*
* @template TMapWithKeysKey of array-key
* @template TMapWithKeysValue
*
* @param callable(TModel, TKey): array<TMapWithKeysKey, TMapWithKeysValue> $callback
* @return \Illuminate\Support\Collection<TMapWithKeysKey, TMapWithKeysValue>|static<TMapWithKeysKey, TMapWithKeysValue>
*/
public function mapWithKeys(callable $callback)
{
$result = parent::mapWithKeys($callback);
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
* Reload a fresh model instance from the database for all the entities.
*
* @param array<array-key, string>|string $with
* @return static
*/
public function fresh($with = [])
{
if ($this->isEmpty()) {
return new static;
}
$model = $this->first();
$freshModels = $model->newQueryWithoutScopes()
->with(is_string($with) ? func_get_args() : $with)
->whereIn($model->getKeyName(), $this->modelKeys())
->get()
->getDictionary();
return $this->filter(fn ($model) => $model->exists && isset($freshModels[$model->getKey()]))
->map(fn ($model) => $freshModels[$model->getKey()]);
}
/**
* Diff the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function diff($items)
{
$diff = new static;
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
$key = $this->getDictionaryKey($item->getKey());
if ($key === null || ! isset($dictionary[$key])) {
$diff->add($item);
}
}
return $diff;
}
/**
* Intersect the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function intersect($items)
{
$intersect = new static;
if (empty($items)) {
return $intersect;
}
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
$key = $this->getDictionaryKey($item->getKey());
if ($key !== null && isset($dictionary[$key])) {
$intersect->add($item);
}
}
return $intersect;
}
/**
* Return only unique items from the collection.
*
* @param (callable(TModel, TKey): mixed)|string|null $key
* @param bool $strict
* @return static
*/
public function unique($key = null, $strict = false)
{
if (! is_null($key)) {
return parent::unique($key, $strict);
}
return new static(array_values($this->getDictionary()));
}
/**
* Returns only the models from the collection with the specified keys.
*
* @param array<array-key, mixed>|null $keys
* @return static
*/
public function only($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::only($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
return new static(array_values($dictionary));
}
/**
* Returns all models in the collection except the models with specified keys.
*
* @param array<array-key, mixed>|null $keys
* @return static
*/
public function except($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::except($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
return new static(array_values($dictionary));
}
/**
* Make the given, typically visible, attributes hidden across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeHidden($attributes)
{
return $this->each->makeHidden($attributes);
}
/**
* Merge the given, typically visible, attributes hidden across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function mergeHidden($attributes)
{
return $this->each->mergeHidden($attributes);
}
/**
* Set the hidden attributes across the entire collection.
*
* @param array<int, string> $hidden
* @return $this
*/
public function setHidden($hidden)
{
return $this->each->setHidden($hidden);
}
/**
* Make the given, typically hidden, attributes visible across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeVisible($attributes)
{
return $this->each->makeVisible($attributes);
}
/**
* Merge the given, typically hidden, attributes visible across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function mergeVisible($attributes)
{
return $this->each->mergeVisible($attributes);
}
/**
* Set the visible attributes across the entire collection.
*
* @param array<int, string> $visible
* @return $this
*/
public function setVisible($visible)
{
return $this->each->setVisible($visible);
}
/**
* Append an attribute across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function append($attributes)
{
return $this->each->append($attributes);
}
/**
* Sets the appends on every element of the collection, overwriting the existing appends for each.
*
* @param array<array-key, mixed> $appends
* @return $this
*/
public function setAppends(array $appends)
{
return $this->each->setAppends($appends);
}
/**
* Remove appended properties from every element in the collection.
*
* @return $this
*/
public function withoutAppends()
{
return $this->setAppends([]);
}
/**
* Get a dictionary keyed by primary keys.
*
* @param iterable<array-key, TModel>|null $items
* @return array<array-key, TModel>
*/
public function getDictionary($items = null)
{
$items = is_null($items) ? $this->items : $items;
$dictionary = [];
foreach ($items as $value) {
$key = $this->getDictionaryKey($value->getKey());
if ($key !== null) {
$dictionary[$key] = $value;
}
}
return $dictionary;
}
/**
* The following methods are intercepted to always return base collections.
*/
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<array-key, int>
*/
#[\Override]
public function countBy($countBy = null)
{
return $this->toBase()->countBy($countBy);
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int, mixed>
*/
#[\Override]
public function collapse()
{
return $this->toBase()->collapse();
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int, mixed>
*/
#[\Override]
public function flatten($depth = INF)
{
return $this->toBase()->flatten($depth);
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<TModel, TKey>
*/
#[\Override]
public function flip()
{
return $this->toBase()->flip();
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int, TKey>
*/
#[\Override]
public function keys()
{
return $this->toBase()->keys();
}
/**
* {@inheritDoc}
*
* @template TPadValue
*
* @return \Illuminate\Support\Collection<int, TModel|TPadValue>
*/
#[\Override]
public function pad($size, $value)
{
return $this->toBase()->pad($size, $value);
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int<0, 1>, static<TKey, TModel>>
*/
#[\Override]
public function partition($key, $operator = null, $value = null)
{
return parent::partition(...func_get_args())->toBase();
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<array-key, mixed>
*/
#[\Override]
public function pluck($value, $key = null)
{
return $this->toBase()->pluck($value, $key);
}
/**
* {@inheritDoc}
*
* @template TZipValue
*
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, TModel|TZipValue>>
*/
#[\Override]
public function zip($items)
{
return $this->toBase()->zip(...func_get_args());
}
/**
* Get the comparison function to detect duplicates.
*
* @return callable(TModel, TModel): bool
*/
protected function duplicateComparator($strict)
{
return fn ($a, $b) => $a->is($b);
}
/**
* Enable relationship autoloading for all models in this collection.
*
* @return $this
*/
public function withRelationshipAutoloading()
{
$callback = fn ($tuples) => $this->loadMissingRelationshipChain($tuples);
foreach ($this as $model) {
if (! $model->hasRelationAutoloadCallback()) {
$model->autoloadRelationsUsing($callback, $this);
}
}
return $this;
}
/**
* Get the type of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableClass()
{
if ($this->isEmpty()) {
return;
}
$class = $this->getQueueableModelClass($this->first());
$this->each(function ($model) use ($class) {
if ($this->getQueueableModelClass($model) !== $class) {
throw new LogicException('Queueing collections with multiple model types is not supported.');
}
});
return $class;
}
/**
* Get the queueable class name for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
protected function getQueueableModelClass($model)
{
return method_exists($model, 'getQueueableClassName')
? $model->getQueueableClassName()
: get_class($model);
}
/**
* Get the identifiers for all of the entities.
*
* @return array<int, mixed>
*/
public function getQueueableIds()
{
if ($this->isEmpty()) {
return [];
}
return $this->first() instanceof QueueableEntity
? $this->map->getQueueableId()->all()
: $this->modelKeys();
}
/**
* Get the relationships of the entities being queued.
*
* @return array<int, string>
*/
public function getQueueableRelations()
{
if ($this->isEmpty()) {
return [];
}
$relations = $this->map->getQueueableRelations()->all();
if (count($relations) === 0 || $relations === [[]]) {
return [];
} elseif (count($relations) === 1) {
return reset($relations);
} else {
return array_intersect(...array_values($relations));
}
}
/**
* Get the connection of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableConnection()
{
if ($this->isEmpty()) {
return;
}
$connection = $this->first()->getConnectionName();
$this->each(function ($model) use ($connection) {
if ($model->getConnectionName() !== $connection) {
throw new LogicException('Queueing collections with multiple model connections is not supported.');
}
});
return $connection;
}
/**
* Get the Eloquent query builder from the collection.
*
* @return \Illuminate\Database\Eloquent\Builder<TModel>
*
* @throws \LogicException
*/
public function toQuery()
{
$model = $this->first();
if (! $model) {
throw new LogicException('Unable to create query for empty collection.');
}
$class = get_class($model);
if ($this->reject(fn ($model) => $model instanceof $class)->isNotEmpty()) {
throw new LogicException('Unable to create query for collection with mixed types.');
}
return $model->newModelQuery()->whereKey($this->modelKeys());
}
}
@@ -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;
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Factories\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseModel
{
/**
* Create a new attribute instance.
*
* @param string $class
*/
public function __construct(public string $class)
{
//
}
}
@@ -0,0 +1,81 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class BelongsToManyRelationship
{
/**
* The related factory instance.
*
* @var \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array
*/
protected $factory;
/**
* The pivot attributes / attribute resolver.
*
* @var callable|array
*/
protected $pivot;
/**
* The relationship name.
*
* @var string
*/
protected $relationship;
/**
* Create a new attached relationship definition.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $factory
* @param callable|array $pivot
* @param string $relationship
*/
public function __construct($factory, $pivot, $relationship)
{
$this->factory = $factory;
$this->pivot = $pivot;
$this->relationship = $relationship;
}
/**
* Create the attached relationship for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function createFor(Model $model)
{
$factoryInstance = $this->factory instanceof Factory;
if ($factoryInstance) {
$relationship = $model->{$this->relationship}();
}
Collection::wrap($factoryInstance ? $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $model) : $this->factory)->each(function ($attachable) use ($model) {
$model->{$this->relationship}()->attach(
$attachable,
is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot
);
});
}
/**
* Specify the model instances to always use when creating relationships.
*
* @param \Illuminate\Support\Collection $recycle
* @return $this
*/
public function recycle($recycle)
{
if ($this->factory instanceof Factory) {
$this->factory = $this->factory->recycle($recycle);
}
return $this;
}
}
@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class BelongsToRelationship
{
/**
* The related factory instance.
*
* @var \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model
*/
protected $factory;
/**
* The relationship name.
*
* @var string
*/
protected $relationship;
/**
* The cached, resolved parent instance ID.
*
* @var mixed
*/
protected $resolved;
/**
* Create a new "belongs to" relationship definition.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model $factory
* @param string $relationship
*/
public function __construct($factory, $relationship)
{
$this->factory = $factory;
$this->relationship = $relationship;
}
/**
* Get the parent model attributes and resolvers for the given child model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return array
*/
public function attributesFor(Model $model)
{
$relationship = $model->{$this->relationship}();
return $relationship instanceof MorphTo ? [
$relationship->getMorphType() => $this->factory instanceof Factory ? $this->factory->newModel()->getMorphClass() : $this->factory->getMorphClass(),
$relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()),
] : [
$relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()),
];
}
/**
* Get the deferred resolver for this relationship's parent ID.
*
* @param string|null $key
* @return \Closure
*/
protected function resolver($key)
{
return function () use ($key) {
if (! $this->resolved) {
$instance = $this->factory instanceof Factory
? ($this->factory->getRandomRecycledModel($this->factory->modelName()) ?? $this->factory->create())
: $this->factory;
return $this->resolved = $key ? $instance->{$key} : $instance->getKey();
}
return $this->resolved;
};
}
/**
* Specify the model instances to always use when creating relationships.
*
* @param \Illuminate\Support\Collection $recycle
* @return $this
*/
public function recycle($recycle)
{
if ($this->factory instanceof Factory) {
$this->factory = $this->factory->recycle($recycle);
}
return $this;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Support\Arr;
class CrossJoinSequence extends Sequence
{
/**
* Create a new cross join sequence instance.
*
* @param array ...$sequences
*/
public function __construct(...$sequences)
{
$crossJoined = array_map(
function ($a) {
return array_merge(...$a);
},
Arr::crossJoin(...$sequences),
);
parent::__construct(...$crossJoined);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
/**
* @template TFactory of \Illuminate\Database\Eloquent\Factories\Factory
*/
trait HasFactory
{
/**
* Get a new factory instance for the model.
*
* @param (callable(array<string, mixed>, static|null): array<string, mixed>)|array<string, mixed>|int|null $count
* @param (callable(array<string, mixed>, static|null): array<string, mixed>)|array<string, mixed> $state
* @return TFactory
*/
public static function factory($count = null, $state = [])
{
$factory = static::newFactory() ?? Factory::factoryForModel(static::class);
return $factory
->count(is_numeric($count) ? $count : null)
->state(is_callable($count) || is_array($count) ? $count : $state);
}
/**
* Create a new factory instance for the model.
*
* @return TFactory|null
*/
protected static function newFactory()
{
if (isset(static::$factory)) {
return static::$factory::new();
}
return static::getUseFactoryAttribute() ?? null;
}
/**
* Get the factory from the UseFactory class attribute.
*
* @return TFactory|null
*/
protected static function getUseFactoryAttribute()
{
$attributes = (new \ReflectionClass(static::class))
->getAttributes(UseFactory::class);
if ($attributes !== []) {
$useFactory = $attributes[0]->newInstance();
$factory = $useFactory->factoryClass::new();
$factory->guessModelNamesUsing(fn () => static::class);
return $factory;
}
}
}
@@ -0,0 +1,76 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
class Relationship
{
/**
* The related factory instance.
*
* @var \Illuminate\Database\Eloquent\Factories\Factory
*/
protected $factory;
/**
* The relationship name.
*
* @var string
*/
protected $relationship;
/**
* Create a new child relationship instance.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory $factory
* @param string $relationship
*/
public function __construct(Factory $factory, $relationship)
{
$this->factory = $factory;
$this->relationship = $relationship;
}
/**
* Create the child relationship for the given parent model.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @return void
*/
public function createFor(Model $parent)
{
$relationship = $parent->{$this->relationship}();
if ($relationship instanceof MorphOneOrMany) {
$this->factory->state([
$relationship->getMorphType() => $relationship->getMorphClass(),
$relationship->getForeignKeyName() => $relationship->getParentKey(),
])->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent);
} elseif ($relationship instanceof HasOneOrMany) {
$this->factory->state([
$relationship->getForeignKeyName() => $relationship->getParentKey(),
])->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent);
} elseif ($relationship instanceof BelongsToMany) {
$relationship->attach(
$this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent)
);
}
}
/**
* Specify the model instances to always use when creating relationships.
*
* @param \Illuminate\Support\Collection $recycle
* @return $this
*/
public function recycle($recycle)
{
$this->factory = $this->factory->recycle($recycle);
return $this;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Countable;
class Sequence implements Countable
{
/**
* The sequence of return values.
*
* @var array
*/
protected $sequence;
/**
* The count of the sequence items.
*
* @var int
*/
public $count;
/**
* The current index of the sequence iteration.
*
* @var int
*/
public $index = 0;
/**
* Create a new sequence instance.
*
* @param mixed ...$sequence
*/
public function __construct(...$sequence)
{
$this->sequence = $sequence;
$this->count = count($sequence);
}
/**
* Get the current count of the sequence items.
*
* @return int
*/
public function count(): int
{
return $this->count;
}
/**
* Get the next value in the sequence.
*
* @param array<string, mixed> $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return mixed
*/
public function __invoke($attributes = [], $parent = null)
{
return tap(value($this->sequence[$this->index % $this->count], $this, $attributes, $parent), function () {
$this->index = $this->index + 1;
});
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace Illuminate\Database\Eloquent;
/**
* @template TBuilder of \Illuminate\Database\Eloquent\Builder
*/
trait HasBuilder
{
/**
* Begin querying the model.
*
* @return TBuilder
*/
public static function query()
{
return parent::query();
}
/**
* Create a new Eloquent query builder for the model.
*
* @param \Illuminate\Database\Query\Builder $query
* @return TBuilder
*/
public function newEloquentBuilder($query)
{
return parent::newEloquentBuilder($query);
}
/**
* Get a new query builder for the model's table.
*
* @return TBuilder
*/
public function newQuery()
{
return parent::newQuery();
}
/**
* Get a new query builder that doesn't have any global scopes or eager loading.
*
* @return TBuilder
*/
public function newModelQuery()
{
return parent::newModelQuery();
}
/**
* Get a new query builder with no relationships loaded.
*
* @return TBuilder
*/
public function newQueryWithoutRelationships()
{
return parent::newQueryWithoutRelationships();
}
/**
* Get a new query builder that doesn't have any global scopes.
*
* @return TBuilder
*/
public function newQueryWithoutScopes()
{
return parent::newQueryWithoutScopes();
}
/**
* Get a new query instance without a given scope.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return TBuilder
*/
public function newQueryWithoutScope($scope)
{
return parent::newQueryWithoutScope($scope);
}
/**
* Get a new query to restore one or more models by their queueable IDs.
*
* @param array|int $ids
* @return TBuilder
*/
public function newQueryForRestoration($ids)
{
return parent::newQueryForRestoration($ids);
}
/**
* Begin querying the model on a given connection.
*
* @param string|null $connection
* @return TBuilder
*/
public static function on($connection = null)
{
return parent::on($connection);
}
/**
* Begin querying the model on the write connection.
*
* @return TBuilder
*/
public static function onWriteConnection()
{
return parent::onWriteConnection();
}
/**
* Begin querying a model with eager loading.
*
* @param array|string $relations
* @return TBuilder
*/
public static function with($relations)
{
return parent::with($relations);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\CollectedBy;
use ReflectionClass;
/**
* @template TCollection of \Illuminate\Database\Eloquent\Collection
*/
trait HasCollection
{
/**
* The resolved collection class names by model.
*
* @var array<class-string<static>, class-string<TCollection>>
*/
protected static array $resolvedCollectionClasses = [];
/**
* Create a new Eloquent Collection instance.
*
* @param array<array-key, \Illuminate\Database\Eloquent\Model> $models
* @return TCollection
*/
public function newCollection(array $models = [])
{
static::$resolvedCollectionClasses[static::class] ??= ($this->resolveCollectionFromAttribute() ?? static::$collectionClass);
$collection = new static::$resolvedCollectionClasses[static::class]($models);
if (Model::isAutomaticallyEagerLoadingRelationships()) {
$collection->withRelationshipAutoloading();
}
return $collection;
}
/**
* Resolve the collection class name from the CollectedBy attribute.
*
* @return class-string<TCollection>|null
*/
public function resolveCollectionFromAttribute()
{
$reflection = new ReflectionClass(static::class);
do {
$attributes = $reflection->getAttributes(CollectedBy::class);
if (isset($attributes[0], $attributes[0]->getArguments()[0])) {
return $attributes[0]->getArguments()[0];
}
} while ($reflection = $reflection->getParentClass());
return null;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Illuminate\Database\Eloquent;
/**
* @mixin \Illuminate\Database\Eloquent\Builder
*/
class HigherOrderBuilderProxy
{
/**
* The collection being operated on.
*
* @var \Illuminate\Database\Eloquent\Builder<*>
*/
protected $builder;
/**
* The method being proxied.
*
* @var string
*/
protected $method;
/**
* Create a new proxy instance.
*
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
* @param string $method
*/
public function __construct(Builder $builder, $method)
{
$this->method = $method;
$this->builder = $builder;
}
/**
* Proxy a scope call onto the query builder.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->builder->{$this->method}(function ($value) use ($method, $parameters) {
return $value->{$method}(...$parameters);
});
}
}
@@ -0,0 +1,47 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class InvalidCastException extends RuntimeException
{
/**
* The name of the affected Eloquent model.
*
* @var string
*/
public $model;
/**
* The name of the column.
*
* @var string
*/
public $column;
/**
* The name of the cast type.
*
* @var string
*/
public $castType;
/**
* Create a new exception instance.
*
* @param object $model
* @param string $column
* @param string $castType
*/
public function __construct($model, $column, $castType)
{
$class = get_class($model);
parent::__construct("Call to undefined cast [{$castType}] on column [{$column}] in model [{$class}].");
$this->model = $class;
$this->column = $column;
$this->castType = $castType;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class JsonEncodingException extends RuntimeException
{
/**
* Create a new JSON encoding exception for the model.
*
* @param mixed $model
* @param string $message
* @return static
*/
public static function forModel($model, $message)
{
return new static('Error encoding model ['.get_class($model).'] with ID ['.$model->getKey().'] to JSON: '.$message);
}
/**
* Create a new JSON encoding exception for the resource.
*
* @param \Illuminate\Http\Resources\Json\JsonResource $resource
* @param string $message
* @return static
*/
public static function forResource($resource, $message)
{
$model = $resource->resource;
return new static('Error encoding resource ['.get_class($resource).'] with model ['.get_class($model).'] with ID ['.$model->getKey().'] to JSON: '.$message);
}
/**
* Create a new JSON encoding exception for an attribute.
*
* @param mixed $model
* @param mixed $key
* @param string $message
* @return static
*/
public static function forAttribute($model, $key, $message)
{
$class = get_class($model);
return new static("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}.");
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class MassAssignmentException extends RuntimeException
{
//
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Database\Events\ModelsPruned;
use LogicException;
trait MassPrunable
{
/**
* Prune all prunable models in the database.
*
* @param int $chunkSize
* @return int
*/
public function pruneAll(int $chunkSize = 1000)
{
$query = tap($this->prunable(), function ($query) use ($chunkSize) {
$query->when(! $query->getQuery()->limit, function ($query) use ($chunkSize) {
$query->limit($chunkSize);
});
});
$total = 0;
$softDeletable = static::isSoftDeletable();
do {
$total += $count = $softDeletable
? $query->forceDelete()
: $query->delete();
if ($count > 0) {
event(new ModelsPruned(static::class, $total));
}
} while ($count > 0);
return $total;
}
/**
* Get the prunable model query.
*
* @return \Illuminate\Database\Eloquent\Builder<static>
*
* @throws \LogicException
*/
public function prunable()
{
throw new LogicException('Please implement the prunable method on your model.');
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Illuminate\Database\Eloquent;
use OutOfBoundsException;
class MissingAttributeException extends OutOfBoundsException
{
/**
* Create a new missing attribute exception instance.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
*/
public function __construct($model, $key)
{
parent::__construct(sprintf(
'The attribute [%s] either does not exist or was not retrieved for model [%s].',
$key, get_class($model)
));
}
}
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
<?php
namespace Illuminate\Database\Eloquent;
use ArrayAccess;
use Illuminate\Contracts\Support\Arrayable;
use InvalidArgumentException;
use LogicException;
/**
* @implements Arrayable<string, mixed>
*
* @internal
*/
class ModelInfo implements Arrayable, ArrayAccess
{
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TModel> $class The model's fully-qualified class.
* @param string $database The database connection name.
* @param string $table The database table name.
* @param class-string|null $policy The policy that applies to the model.
* @param \Illuminate\Support\Collection<int, array<string, mixed>> $attributes The attributes available on the model.
* @param \Illuminate\Support\Collection<int, array{name: string, type: string, related: class-string<\Illuminate\Database\Eloquent\Model>}> $relations The relations defined on the model.
* @param \Illuminate\Support\Collection<int, array{event: string, class: string}> $events The events that the model dispatches.
* @param \Illuminate\Support\Collection<int, array{event: string, observer: array<int, string>}> $observers The observers registered for the model.
* @param class-string<\Illuminate\Database\Eloquent\Collection<TModel>> $collection The Collection class that collects the models.
* @param class-string<\Illuminate\Database\Eloquent\Builder<TModel>> $builder The Builder class registered for the model.
* @param \Illuminate\Http\Resources\Json\JsonResource|null $resource The JSON resource that represents the model.
*/
public function __construct(
public $class,
public $database,
public $table,
public $policy,
public $attributes,
public $relations,
public $events,
public $observers,
public $collection,
public $builder,
public $resource
) {
}
/**
* Convert the model info to an array.
*
* @return array{
* "class": class-string<\Illuminate\Database\Eloquent\Model>,
* database: string,
* table: string,
* policy: class-string|null,
* attributes: \Illuminate\Support\Collection<int, array<string, mixed>>,
* relations: \Illuminate\Support\Collection<int, array{name: string, type: string, related: class-string<\Illuminate\Database\Eloquent\Model>}>,
* events: \Illuminate\Support\Collection<int, array{event: string, class: string}>,
* observers: \Illuminate\Support\Collection<int, array{event: string, observer: array<int, string>}>, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>,
* builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>,
* resource: \Illuminate\Http\Resources\Json\JsonResource|null
* }
*/
public function toArray()
{
return [
'class' => $this->class,
'database' => $this->database,
'table' => $this->table,
'policy' => $this->policy,
'attributes' => $this->attributes,
'relations' => $this->relations,
'events' => $this->events,
'observers' => $this->observers,
'collection' => $this->collection,
'builder' => $this->builder,
'resource' => $this->resource,
];
}
/**
* Determine if the given offset exists.
*/
public function offsetExists(mixed $offset): bool
{
return property_exists($this, $offset);
}
/**
* Get the value for a given offset.
*
* @throws \InvalidArgumentException
*/
public function offsetGet(mixed $offset): mixed
{
return property_exists($this, $offset) ? $this->{$offset} : throw new InvalidArgumentException("Property {$offset} does not exist.");
}
/**
* Set the value at the given offset.
*
* @throws \LogicException
*/
public function offsetSet(mixed $offset, mixed $value): void
{
throw new LogicException(self::class.' may not be mutated using array access.');
}
/**
* Unset the value at the given offset.
*
* @throws \LogicException
*/
public function offsetUnset(mixed $offset): void
{
throw new LogicException(self::class.' may not be mutated using array access.');
}
}
+414
View File
@@ -0,0 +1,414 @@
<?php
namespace Illuminate\Database\Eloquent;
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 methods that can be called in a model to indicate a relation.
*
* @var list<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 The Laravel application instance.
*/
public function __construct(protected Application $app)
{
}
/**
* Extract model details for the given model.
*
* @param class-string<\Illuminate\Database\Eloquent\Model>|string $model
* @param string|null $connection
* @return \Illuminate\Database\Eloquent\ModelInfo
*
* @throws \Illuminate\Contracts\Container\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 new ModelInfo(
class: $model::class,
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),
resource: $this->getResource($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 \Illuminate\Contracts\Container\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;
}
/**
* Get the class used for JSON response transforming.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Http\Resources\Json\JsonResource|null
*/
protected function getResource($model)
{
return rescue(static fn () => $model->toResource()::class, null, false);
}
/**
* 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
*/
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']
);
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace Illuminate\Database\Eloquent;
use BackedEnum;
use Illuminate\Database\RecordsNotFoundException;
use Illuminate\Support\Arr;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*/
class ModelNotFoundException extends RecordsNotFoundException
{
/**
* Name of the affected Eloquent model.
*
* @var class-string<TModel>
*/
protected $model;
/**
* The affected model IDs.
*
* @var array<int, int|string>
*/
protected $ids;
/**
* Set the affected Eloquent model and instance ids.
*
* @param class-string<TModel> $model
* @param array<int, int|string>|int|string $ids
* @return $this
*/
public function setModel($model, $ids = [])
{
$this->model = $model;
$this->ids = array_map(
fn ($id) => $id instanceof BackedEnum ? $id->value : $id,
Arr::wrap($ids)
);
$this->message = "No query results for model [{$model}]";
if ($this->ids !== []) {
$this->message .= ' '.implode(', ', $this->ids);
} else {
$this->message .= '.';
}
return $this;
}
/**
* Get the affected Eloquent model.
*
* @return class-string<TModel>
*/
public function getModel()
{
return $this->model;
}
/**
* Get the affected Eloquent model IDs.
*
* @return array<int, int|string>
*/
public function getIds()
{
return $this->ids;
}
}
@@ -0,0 +1,119 @@
<?php
namespace Illuminate\Database\Eloquent;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
/**
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
* @template TLocalRelationship of \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TIntermediateModel, TDeclaringModel>
*/
class PendingHasThroughRelationship
{
/**
* The root model that the relationship exists on.
*
* @var TDeclaringModel
*/
protected $rootModel;
/**
* The local relationship.
*
* @var TLocalRelationship
*/
protected $localRelationship;
/**
* Create a pending has-many-through or has-one-through relationship.
*
* @param TDeclaringModel $rootModel
* @param TLocalRelationship $localRelationship
*/
public function __construct($rootModel, $localRelationship)
{
$this->rootModel = $rootModel;
$this->localRelationship = $localRelationship;
}
/**
* Define the distant relationship that this model has.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param string|(callable(TIntermediateModel): (\Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TIntermediateModel>|\Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, TIntermediateModel>|\Illuminate\Database\Eloquent\Relations\MorphOneOrMany<TRelatedModel, TIntermediateModel>)) $callback
* @return (
* $callback is string
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOneThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>
* : (
* TLocalRelationship is \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, TDeclaringModel>
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* : (
* $callback is callable(TIntermediateModel): \Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, TIntermediateModel>
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* : \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* )
* )
* )
*/
public function has($callback)
{
if (is_string($callback)) {
$callback = fn () => $this->localRelationship->getRelated()->{$callback}();
}
$distantRelation = $callback($this->localRelationship->getRelated());
if ($distantRelation instanceof HasMany || $this->localRelationship instanceof HasMany) {
$returnedRelation = $this->rootModel->hasManyThrough(
$distantRelation->getRelated()::class,
$this->localRelationship->getRelated()::class,
$this->localRelationship->getForeignKeyName(),
$distantRelation->getForeignKeyName(),
$this->localRelationship->getLocalKeyName(),
$distantRelation->getLocalKeyName(),
);
} else {
$returnedRelation = $this->rootModel->hasOneThrough(
$distantRelation->getRelated()::class,
$this->localRelationship->getRelated()::class,
$this->localRelationship->getForeignKeyName(),
$distantRelation->getForeignKeyName(),
$this->localRelationship->getLocalKeyName(),
$distantRelation->getLocalKeyName(),
);
}
if ($this->localRelationship instanceof MorphOneOrMany) {
$returnedRelation->where($this->localRelationship->getQualifiedMorphType(), $this->localRelationship->getMorphClass());
}
return $returnedRelation;
}
/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
if (Str::startsWith($method, 'has')) {
return $this->has((new Stringable($method))->after('has')->lcfirst()->toString());
}
throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()', static::class, $method
));
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Database\Events\ModelsPruned;
use LogicException;
use Throwable;
trait Prunable
{
/**
* Prune all prunable models in the database.
*
* @param int $chunkSize
* @return int
*
* @throws \Throwable
*/
public function pruneAll(int $chunkSize = 1000)
{
$total = 0;
$this->prunable()
->when(static::isSoftDeletable(), function ($query) {
$query->withTrashed();
})->chunkById($chunkSize, function ($models) use (&$total) {
$models->each(function ($model) use (&$total) {
try {
$model->prune();
$total++;
} catch (Throwable $e) {
$handler = app(ExceptionHandler::class);
if ($handler) {
$handler->report($e);
} else {
throw $e;
}
}
});
event(new ModelsPruned(static::class, $total));
});
return $total;
}
/**
* Get the prunable model query.
*
* @return \Illuminate\Database\Eloquent\Builder<static>
*
* @throws \LogicException
*/
public function prunable()
{
throw new LogicException('Please implement the prunable method on your model.');
}
/**
* Prune the model in the database.
*
* @return bool|null
*/
public function prune()
{
$this->pruning();
return static::isSoftDeletable()
? $this->forceDelete()
: $this->delete();
}
/**
* Prepare the model for pruning.
*
* @return void
*/
protected function pruning()
{
//
}
}
@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Contracts\Queue\EntityNotFoundException;
use Illuminate\Contracts\Queue\EntityResolver as EntityResolverContract;
class QueueEntityResolver implements EntityResolverContract
{
/**
* Resolve the entity for the given ID.
*
* @param string $type
* @param mixed $id
* @return mixed
*
* @throws \Illuminate\Contracts\Queue\EntityNotFoundException
*/
public function resolve($type, $id)
{
$instance = (new $type)->find($id);
if ($instance) {
return $instance;
}
throw new EntityNotFoundException($type, $id);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class RelationNotFoundException extends RuntimeException
{
/**
* The name of the affected Eloquent model.
*
* @var string
*/
public $model;
/**
* The name of the relation.
*
* @var string
*/
public $relation;
/**
* Create a new exception instance.
*
* @param object $model
* @param string $relation
* @param string|null $type
* @return static
*/
public static function make($model, $relation, $type = null)
{
$class = get_class($model);
$instance = new static(
is_null($type)
? "Call to undefined relationship [{$relation}] on model [{$class}]."
: "Call to undefined relationship [{$relation}] on model [{$class}] of type [{$type}].",
);
$instance->model = $class;
$instance->relation = $relation;
return $instance;
}
}
+384
View File
@@ -0,0 +1,384 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
use function Illuminate\Support\enum_value;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TDeclaringModel, ?TRelatedModel>
*/
class BelongsTo extends Relation
{
use ComparesRelatedModels,
InteractsWithDictionary,
SupportsDefaultModels;
/**
* The child model instance of the relation.
*
* @var TDeclaringModel
*/
protected $child;
/**
* The foreign key of the parent model.
*
* @var string
*/
protected $foreignKey;
/**
* The associated key on the parent model.
*
* @var string
*/
protected $ownerKey;
/**
* The name of the relationship.
*
* @var string
*/
protected $relationName;
/**
* Create a new belongs to relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $child
* @param string $foreignKey
* @param string $ownerKey
* @param string $relationName
*/
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)
{
$this->ownerKey = $ownerKey;
$this->relationName = $relationName;
$this->foreignKey = $foreignKey;
// In the underlying base relationship class, this variable is referred to as
// the "parent" since most relationships are not inversed. But, since this
// one is we will create a "child" variable for much better readability.
$this->child = $child;
parent::__construct($query, $child);
}
/** @inheritDoc */
public function getResults()
{
if (is_null($this->getForeignKeyFrom($this->child))) {
return $this->getDefaultFor($this->parent);
}
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints()
{
if (static::$constraints) {
// For belongs to relationships, which are essentially the inverse of has one
// or has many relationships, we need to actually query on the primary key
// of the related models matching on the foreign key that's on a parent.
$key = $this->getQualifiedOwnerKeyName();
$this->query->where($key, '=', $this->getForeignKeyFrom($this->child));
}
}
/** @inheritDoc */
public function addEagerConstraints(array $models)
{
// We'll grab the primary key name of the related models since it could be set to
// a non-standard name and not "id". We will then construct the constraint for
// our eagerly loading query so it returns the proper models from execution.
$key = $this->getQualifiedOwnerKeyName();
$whereIn = $this->whereInMethod($this->related, $this->ownerKey);
$this->whereInEager($whereIn, $key, $this->getEagerModelKeys($models));
}
/**
* Gather the keys from an array of related models.
*
* @param array<int, TDeclaringModel> $models
* @return array
*/
protected function getEagerModelKeys(array $models)
{
$keys = [];
// First we need to gather all of the keys from the parent models so we know what
// to query for via the eager loading query. We will add them to an array then
// execute a "where in" statement to gather up all of those related records.
foreach ($models as $model) {
if (! is_null($value = $this->getForeignKeyFrom($model))) {
$keys[] = $value;
}
}
sort($keys);
return array_values(array_unique($keys));
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->getDefaultFor($model));
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
// First we will get to build a dictionary of the child models by their primary
// key of the relationship, then we can easily match the children back onto
// the parents using that dictionary and the primary key of the children.
$dictionary = [];
foreach ($results as $result) {
$attribute = $this->getDictionaryKey($this->getRelatedKeyFrom($result));
if ($attribute !== null) {
$dictionary[$attribute] = $result;
}
}
// Once we have the dictionary constructed, we can loop through all the parents
// and match back onto their children using these keys of the dictionary and
// the primary key of the children to map them onto the correct instances.
foreach ($models as $model) {
$attribute = $this->getDictionaryKey($this->getForeignKeyFrom($model));
if ($attribute !== null && isset($dictionary[$attribute])) {
$model->setRelation($relation, $dictionary[$attribute]);
}
}
return $models;
}
/**
* Associate the model instance to the given parent.
*
* @param TRelatedModel|int|string|null $model
* @return TDeclaringModel
*/
public function associate($model)
{
$ownerKey = $model instanceof Model ? $model->getAttribute($this->ownerKey) : $model;
$this->child->setAttribute($this->foreignKey, $ownerKey);
if ($model instanceof Model) {
$this->child->setRelation($this->relationName, $model);
} else {
$this->child->unsetRelation($this->relationName);
}
return $this->child;
}
/**
* Dissociate previously associated model from the given parent.
*
* @return TDeclaringModel
*/
public function dissociate()
{
$this->child->setAttribute($this->foreignKey, null);
return $this->child->setRelation($this->relationName, null);
}
/**
* Alias of "dissociate" method.
*
* @return TDeclaringModel
*/
public function disassociate()
{
return $this->dissociate();
}
/**
* Touch all of the related models for the relationship.
*
* @return void
*/
public function touch()
{
if (! is_null($this->getParentKey())) {
parent::touch();
}
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($parentQuery->getQuery()->from == $query->getQuery()->from) {
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
}
return $query->select($columns)->whereColumn(
$this->getQualifiedForeignKeyName(), '=', $query->qualifyColumn($this->ownerKey)
);
}
/**
* Add the constraints for a relationship query on the same table.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
* @param mixed $columns
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query->select($columns)->from(
$query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()
);
$query->getModel()->setTable($hash);
return $query->whereColumn(
$hash.'.'.$this->ownerKey, '=', $this->getQualifiedForeignKeyName()
);
}
/**
* Determine if the related model has an auto-incrementing ID.
*
* @return bool
*/
protected function relationHasIncrementingId()
{
return $this->related->getIncrementing() &&
in_array($this->related->getKeyType(), ['int', 'integer']);
}
/**
* Make a new related instance for the given model.
*
* @param TDeclaringModel $parent
* @return TRelatedModel
*/
protected function newRelatedInstanceFor(Model $parent)
{
return $this->related->newInstance();
}
/**
* Get the child of the relationship.
*
* @return TDeclaringModel
*/
public function getChild()
{
return $this->child;
}
/**
* Get the foreign key of the relationship.
*
* @return string
*/
public function getForeignKeyName()
{
return $this->foreignKey;
}
/**
* Get the fully-qualified foreign key of the relationship.
*
* @return string
*/
public function getQualifiedForeignKeyName()
{
return $this->child->qualifyColumn($this->foreignKey);
}
/**
* Get the key value of the child's foreign key.
*
* @return mixed
*/
public function getParentKey()
{
return $this->getForeignKeyFrom($this->child);
}
/**
* Get the associated key of the relationship.
*
* @return string
*/
public function getOwnerKeyName()
{
return $this->ownerKey;
}
/**
* Get the fully-qualified associated key of the relationship.
*
* @return string
*/
public function getQualifiedOwnerKeyName()
{
return $this->related->qualifyColumn($this->ownerKey);
}
/**
* Get the value of the model's foreign key.
*
* @param TRelatedModel $model
* @return int|string
*/
protected function getRelatedKeyFrom(Model $model)
{
return $model->{$this->ownerKey};
}
/**
* Get the value of the model's foreign key.
*
* @param TDeclaringModel $model
* @return mixed
*/
protected function getForeignKeyFrom(Model $model)
{
$foreignKey = $model->{$this->foreignKey};
return enum_value($foreignKey);
}
/**
* Get the name of the relationship.
*
* @return string
*/
public function getRelationName()
{
return $this->relationName;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,356 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphPivot;
use Illuminate\Support\Str;
trait AsPivot
{
/**
* The parent model of the relationship.
*
* @var \Illuminate\Database\Eloquent\Model
*/
public $pivotParent;
/**
* The related model of the relationship.
*
* @var \Illuminate\Database\Eloquent\Model
*/
public $pivotRelated;
/**
* The name of the foreign key column.
*
* @var string
*/
protected $foreignKey;
/**
* The name of the "other key" column.
*
* @var string
*/
protected $relatedKey;
/**
* Create a new pivot model instance.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @param array $attributes
* @param string $table
* @param bool $exists
* @return static
*/
public static function fromAttributes(Model $parent, $attributes, $table, $exists = false)
{
$instance = new static;
$instance->timestamps = $instance->hasTimestampAttributes($attributes);
// The pivot model is a "dynamic" model since we will set the tables dynamically
// for the instance. This allows it work for any intermediate tables for the
// many to many relationship that are defined by this developer's classes.
$instance->setConnection($parent->getConnectionName())
->setTable($table)
->forceFill($attributes)
->syncOriginal();
// We store off the parent instance so we will access the timestamp column names
// for the model, since the pivot model timestamps aren't easily configurable
// from the developer's point of view. We can use the parents to get these.
$instance->pivotParent = $parent;
$instance->exists = $exists;
return $instance;
}
/**
* Create a new pivot model from raw values returned from a query.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @param array $attributes
* @param string $table
* @param bool $exists
* @return static
*/
public static function fromRawAttributes(Model $parent, $attributes, $table, $exists = false)
{
$instance = static::fromAttributes($parent, [], $table, $exists);
$instance->timestamps = $instance->hasTimestampAttributes($attributes);
$instance->setRawAttributes(
array_merge($instance->getRawOriginal(), $attributes), $exists
);
return $instance;
}
/**
* Set the keys for a select query.
*
* @param \Illuminate\Database\Eloquent\Builder<static> $query
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function setKeysForSelectQuery($query)
{
if (isset($this->attributes[$this->getKeyName()])) {
return parent::setKeysForSelectQuery($query);
}
$query->where($this->foreignKey, $this->getOriginal(
$this->foreignKey, $this->getAttribute($this->foreignKey)
));
return $query->where($this->relatedKey, $this->getOriginal(
$this->relatedKey, $this->getAttribute($this->relatedKey)
));
}
/**
* Set the keys for a save update query.
*
* @param \Illuminate\Database\Eloquent\Builder<static> $query
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function setKeysForSaveQuery($query)
{
return $this->setKeysForSelectQuery($query);
}
/**
* Delete the pivot model record from the database.
*
* @return int
*/
public function delete()
{
if (isset($this->attributes[$this->getKeyName()])) {
return (int) parent::delete();
}
if ($this->fireModelEvent('deleting') === false) {
return 0;
}
$this->touchOwners();
return tap($this->getDeleteQuery()->delete(), function () {
$this->exists = false;
$this->fireModelEvent('deleted', false);
});
}
/**
* Get the query builder for a delete operation on the pivot.
*
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function getDeleteQuery()
{
return $this->newQueryWithoutRelationships()->where([
$this->foreignKey => $this->getOriginal($this->foreignKey, $this->getAttribute($this->foreignKey)),
$this->relatedKey => $this->getOriginal($this->relatedKey, $this->getAttribute($this->relatedKey)),
]);
}
/**
* Get the table associated with the model.
*
* @return string
*/
public function getTable()
{
if (! isset($this->table) && (! $this instanceof MorphPivot)) {
$this->setTable(str_replace(
'\\', '', Str::snake(Str::singular(class_basename($this)))
));
}
return parent::getTable();
}
/**
* Get the foreign key column name.
*
* @return string
*/
public function getForeignKey()
{
return $this->foreignKey;
}
/**
* Get the "related key" column name.
*
* @return string
*/
public function getRelatedKey()
{
return $this->relatedKey;
}
/**
* Get the "related key" column name.
*
* @return string
*/
public function getOtherKey()
{
return $this->getRelatedKey();
}
/**
* Set the key names for the pivot model instance.
*
* @param string $foreignKey
* @param string $relatedKey
* @return $this
*/
public function setPivotKeys($foreignKey, $relatedKey)
{
$this->foreignKey = $foreignKey;
$this->relatedKey = $relatedKey;
return $this;
}
/**
* Set the related model of the relationship.
*
* @param \Illuminate\Database\Eloquent\Model|null $related
* @return $this
*/
public function setRelatedModel(?Model $related = null)
{
$this->pivotRelated = $related;
return $this;
}
/**
* Determine if the pivot model or given attributes has timestamp attributes.
*
* @param array|null $attributes
* @return bool
*/
public function hasTimestampAttributes($attributes = null)
{
return ($createdAt = $this->getCreatedAtColumn()) !== null
&& array_key_exists($createdAt, $attributes ?? $this->attributes);
}
/**
* Get the name of the "created at" column.
*
* @return string
*/
public function getCreatedAtColumn()
{
return $this->pivotParent
? $this->pivotParent->getCreatedAtColumn()
: parent::getCreatedAtColumn();
}
/**
* Get the name of the "updated at" column.
*
* @return string
*/
public function getUpdatedAtColumn()
{
return $this->pivotParent
? $this->pivotParent->getUpdatedAtColumn()
: parent::getUpdatedAtColumn();
}
/**
* Get the queueable identity for the entity.
*
* @return mixed
*/
public function getQueueableId()
{
if (isset($this->attributes[$this->getKeyName()])) {
return $this->getKey();
}
return sprintf(
'%s:%s:%s:%s',
$this->foreignKey, $this->getAttribute($this->foreignKey),
$this->relatedKey, $this->getAttribute($this->relatedKey)
);
}
/**
* Get a new query to restore one or more models by their queueable IDs.
*
* @param int[]|string[]|string $ids
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
public function newQueryForRestoration($ids)
{
if (is_array($ids)) {
return $this->newQueryForCollectionRestoration($ids);
}
if (! str_contains($ids, ':')) {
return parent::newQueryForRestoration($ids);
}
$segments = explode(':', $ids);
return $this->newQueryWithoutScopes()
->where($segments[0], $segments[1])
->where($segments[2], $segments[3]);
}
/**
* Get a new query to restore multiple models by their queueable IDs.
*
* @param int[]|string[] $ids
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function newQueryForCollectionRestoration(array $ids)
{
$ids = array_values($ids);
if (! str_contains($ids[0], ':')) {
return parent::newQueryForRestoration($ids);
}
$query = $this->newQueryWithoutScopes();
foreach ($ids as $id) {
$segments = explode(':', $id);
$query->orWhere(function ($query) use ($segments) {
return $query->where($segments[0], $segments[1])
->where($segments[2], $segments[3]);
});
}
return $query;
}
/**
* Unset all the loaded relations for the instance.
*
* @return $this
*/
public function unsetRelations()
{
$this->pivotParent = null;
$this->pivotRelated = null;
$this->relations = [];
return $this;
}
}
@@ -0,0 +1,332 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use InvalidArgumentException;
trait CanBeOneOfMany
{
/**
* Determines whether the relationship is one-of-many.
*
* @var bool
*/
protected $isOneOfMany = false;
/**
* The name of the relationship.
*
* @var string
*/
protected $relationName;
/**
* The one of many inner join subselect query builder instance.
*
* @var \Illuminate\Database\Eloquent\Builder<*>|null
*/
protected $oneOfManySubQuery;
/**
* Add constraints for inner join subselect for one of many relationships.
*
* @param \Illuminate\Database\Eloquent\Builder<*> $query
* @param string|null $column
* @param string|null $aggregate
* @return void
*/
abstract public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null);
/**
* Get the columns the determine the relationship groups.
*
* @return array|string
*/
abstract public function getOneOfManySubQuerySelectColumns();
/**
* Add join query constraints for one of many relationships.
*
* @param \Illuminate\Database\Query\JoinClause $join
* @return void
*/
abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join);
/**
* Indicate that the relation is a single result of a larger one-to-many relationship.
*
* @param string|array|null $column
* @param string|\Closure|null $aggregate
* @param string|null $relation
* @return $this
*
* @throws \InvalidArgumentException
*/
public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null)
{
$this->isOneOfMany = true;
$this->relationName = $relation ?: $this->getDefaultOneOfManyJoinAlias(
$this->guessRelationship()
);
$keyName = $this->query->getModel()->getKeyName();
$columns = is_string($columns = $column) ? [
$column => $aggregate,
$keyName => $aggregate,
] : $column;
if (! array_key_exists($keyName, $columns)) {
$columns[$keyName] = 'MAX';
}
if ($aggregate instanceof Closure) {
$closure = $aggregate;
}
foreach ($columns as $column => $aggregate) {
if (! in_array(strtolower($aggregate), ['min', 'max'])) {
throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX");
}
$subQuery = $this->newOneOfManySubQuery(
$this->getOneOfManySubQuerySelectColumns(),
array_merge([$column], $previous['columns'] ?? []),
$aggregate,
);
if (isset($previous)) {
$this->addOneOfManyJoinSubQuery(
$subQuery,
$previous['subQuery'],
$previous['columns'],
);
}
if (isset($closure)) {
$closure($subQuery);
}
if (! isset($previous)) {
$this->oneOfManySubQuery = $subQuery;
}
if (array_key_last($columns) == $column) {
$this->addOneOfManyJoinSubQuery(
$this->query,
$subQuery,
array_merge([$column], $previous['columns'] ?? []),
);
}
$previous = [
'subQuery' => $subQuery,
'columns' => array_merge([$column], $previous['columns'] ?? []),
];
}
$this->addConstraints();
$columns = $this->query->getQuery()->columns;
if (is_null($columns) || $columns === ['*']) {
$this->select([$this->qualifyColumn('*')]);
}
return $this;
}
/**
* Indicate that the relation is the latest single result of a larger one-to-many relationship.
*
* @param string|array|null $column
* @param string|null $relation
* @return $this
*/
public function latestOfMany($column = 'id', $relation = null)
{
return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) {
return [$column => 'MAX'];
})->all(), 'MAX', $relation);
}
/**
* Indicate that the relation is the oldest single result of a larger one-to-many relationship.
*
* @param string|array|null $column
* @param string|null $relation
* @return $this
*/
public function oldestOfMany($column = 'id', $relation = null)
{
return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) {
return [$column => 'MIN'];
})->all(), 'MIN', $relation);
}
/**
* Get the default alias for the one of many inner join clause.
*
* @param string $relation
* @return string
*/
protected function getDefaultOneOfManyJoinAlias($relation)
{
return $relation == $this->query->getModel()->getTable()
? $relation.'_of_many'
: $relation;
}
/**
* Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship.
*
* @param string|array $groupBy
* @param array<string>|null $columns
* @param string|null $aggregate
* @return \Illuminate\Database\Eloquent\Builder<*>
*/
protected function newOneOfManySubQuery($groupBy, $columns = null, $aggregate = null)
{
$subQuery = $this->query->getModel()
->newQuery()
->withoutGlobalScopes($this->removedScopes());
foreach (Arr::wrap($groupBy) as $group) {
$subQuery->groupBy($this->qualifyRelatedColumn($group));
}
if (! is_null($columns)) {
foreach ($columns as $key => $column) {
$aggregatedColumn = $subQuery->getQuery()->grammar->wrap($subQuery->qualifyColumn($column));
if ($key === 0) {
$aggregatedColumn = "{$aggregate}({$aggregatedColumn})";
} else {
$aggregatedColumn = "min({$aggregatedColumn})";
}
$subQuery->selectRaw($aggregatedColumn.' as '.$subQuery->getQuery()->grammar->wrap($column.'_aggregate'));
}
}
$this->addOneOfManySubQueryConstraints($subQuery, column: null, aggregate: $aggregate);
return $subQuery;
}
/**
* Add the join subquery to the given query on the given column and the relationship's foreign key.
*
* @param \Illuminate\Database\Eloquent\Builder<*> $parent
* @param \Illuminate\Database\Eloquent\Builder<*> $subQuery
* @param array<string> $on
* @return void
*/
protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on)
{
$parent->beforeQuery(function ($parent) use ($subQuery, $on) {
$subQuery->applyBeforeQueryCallbacks();
$parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) {
foreach ($on as $onColumn) {
$join->on($this->qualifySubSelectColumn($onColumn.'_aggregate'), '=', $this->qualifyRelatedColumn($onColumn));
}
$this->addOneOfManyJoinSubQueryConstraints($join);
});
});
}
/**
* Merge the relationship query joins to the given query builder.
*
* @param \Illuminate\Database\Eloquent\Builder<*> $query
* @return void
*/
protected function mergeOneOfManyJoinsTo(Builder $query)
{
$query->getQuery()->beforeQueryCallbacks = $this->query->getQuery()->beforeQueryCallbacks;
$query->applyBeforeQueryCallbacks();
}
/**
* Get the query builder that will contain the relationship constraints.
*
* @return \Illuminate\Database\Eloquent\Builder<*>
*/
protected function getRelationQuery()
{
return $this->isOneOfMany()
? $this->oneOfManySubQuery
: $this->query;
}
/**
* Get the one of many inner join subselect builder instance.
*
* @return \Illuminate\Database\Eloquent\Builder<*>|void
*/
public function getOneOfManySubQuery()
{
return $this->oneOfManySubQuery;
}
/**
* Get the qualified column name for the one-of-many relationship using the subselect join query's alias.
*
* @param string $column
* @return string
*/
public function qualifySubSelectColumn($column)
{
return $this->getRelationName().'.'.last(explode('.', $column));
}
/**
* Qualify related column using the related table name if it is not already qualified.
*
* @param string $column
* @return string
*/
protected function qualifyRelatedColumn($column)
{
return $this->query->getModel()->qualifyColumn($column);
}
/**
* Guess the "hasOne" relationship's name via backtrace.
*
* @return string
*/
protected function guessRelationship()
{
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
}
/**
* Determine whether the relationship is a one-of-many relationship.
*
* @return bool
*/
public function isOneOfMany()
{
return $this->isOneOfMany;
}
/**
* Get the name of the relationship.
*
* @return string
*/
public function getRelationName()
{
return $this->relationName;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
use Illuminate\Database\Eloquent\Model;
trait ComparesRelatedModels
{
/**
* Determine if the model is the related instance of the relationship.
*
* @param \Illuminate\Database\Eloquent\Model|null $model
* @return bool
*/
public function is($model)
{
$match = ! is_null($model) &&
$this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) &&
$this->related->getTable() === $model->getTable() &&
$this->related->getConnectionName() === $model->getConnectionName();
if ($match && $this instanceof SupportsPartialRelations && $this->isOneOfMany()) {
return $this->query
->whereKey($model->getKey())
->exists();
}
return $match;
}
/**
* Determine if the model is not the related instance of the relationship.
*
* @param \Illuminate\Database\Eloquent\Model|null $model
* @return bool
*/
public function isNot($model)
{
return ! $this->is($model);
}
/**
* Get the value of the parent model's key.
*
* @return mixed
*/
abstract public function getParentKey();
/**
* Get the value of the model's related key.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return mixed
*/
abstract protected function getRelatedKeyFrom(Model $model);
/**
* Compare the parent key with the related key.
*
* @param mixed $parentKey
* @param mixed $relatedKey
* @return bool
*/
protected function compareKeys($parentKey, $relatedKey)
{
if (empty($parentKey) || empty($relatedKey)) {
return false;
}
if (is_int($parentKey) || is_int($relatedKey)) {
return (int) $parentKey === (int) $relatedKey;
}
return $parentKey === $relatedKey;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use InvalidArgumentException;
use UnitEnum;
use function Illuminate\Support\enum_value;
trait InteractsWithDictionary
{
/**
* Get a dictionary key attribute - casting it to a string if necessary.
*
* @param mixed $attribute
* @return string|int|null
*
* @throws \InvalidArgumentException
*/
protected function getDictionaryKey($attribute)
{
if (is_null($attribute) || is_string($attribute) || is_int($attribute)) {
return $attribute;
}
if (is_object($attribute)) {
if (method_exists($attribute, '__toString')) {
return $attribute->__toString();
}
if ($attribute instanceof UnitEnum) {
return enum_value($attribute);
}
throw new InvalidArgumentException('Model attribute value is an object but does not have a __toString method.');
}
return (string) $attribute;
}
}
@@ -0,0 +1,803 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use BackedEnum;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Collection as BaseCollection;
trait InteractsWithPivotTable
{
/**
* Toggles a model (or models) from the parent.
*
* Each existing model is detached, and non existing ones are attached.
*
* @param mixed $ids
* @param bool $touch
* @return array
*/
public function toggle($ids, $touch = true)
{
$changes = [
'attached' => [], 'detached' => [],
];
$records = $this->formatRecordsList($this->parseIds($ids));
// Next, we will determine which IDs should get removed from the join table by
// checking which of the given ID/records is in the list of current records
// and removing all of those rows from this "intermediate" joining table.
$detach = array_values(array_intersect(
$this->newPivotQuery()->pluck($this->relatedPivotKey)->all(),
array_keys($records)
));
if ($detach !== []) {
$this->detach($detach, false);
$changes['detached'] = $this->castKeys($detach);
}
// Finally, for all of the records which were not "detached", we'll attach the
// records into the intermediate table. Then, we will add those attaches to
// this change list and get ready to return these results to the callers.
$attach = array_diff_key($records, array_flip($detach));
if ($attach !== []) {
$this->attach($attach, [], false);
$changes['attached'] = array_keys($attach);
}
// Once we have finished attaching or detaching the records, we will see if we
// have done any attaching or detaching, and if we have we will touch these
// relationships if they are configured to touch on any database updates.
if ($touch && (count($changes['attached']) ||
count($changes['detached']))) {
$this->touchIfTouching();
}
return $changes;
}
/**
* Toggles a model (or models) from the parent within a transaction.
*
* @param mixed $ids
* @param bool $touch
* @return array
*
* @throws \Throwable
*/
public function toggleOrFail($ids, $touch = true)
{
return $this->parent->getConnection()->transaction(fn () => $this->toggle($ids, $touch));
}
/**
* Sync the intermediate tables with a list of IDs without detaching.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
* @return array{attached: array, detached: array, updated: array}
*/
public function syncWithoutDetaching($ids)
{
return $this->sync($ids, false);
}
/**
* Sync the intermediate tables with a list of IDs or collection of models.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
* @param bool $detaching
* @return array{attached: array, detached: array, updated: array}
*/
public function sync($ids, $detaching = true)
{
$changes = [
'attached' => [], 'detached' => [], 'updated' => [],
];
$records = $this->formatRecordsList($this->parseIds($ids));
if (empty($records) && ! $detaching) {
return $changes;
}
// First we need to attach any of the associated models that are not currently
// in this joining table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->getCurrentlyAttachedPivots()
->pluck($this->relatedPivotKey)->all();
// Next, we will take the differences of the currents and given IDs and detach
// all of the entities that exist in the "current" array but are not in the
// array of the new IDs given to the method which will complete the sync.
if ($detaching) {
$detach = array_diff($current, array_keys($records));
if ($detach !== []) {
$this->detach($detach, false);
$changes['detached'] = $this->castKeys($detach);
}
}
// Now we are finally ready to attach the new records. Note that we'll disable
// touching until after the entire operation is complete so we don't fire a
// ton of touch operations until we are totally done syncing the records.
$changes = array_merge(
$changes, $this->attachNew($records, $current, false)
);
// Once we have finished attaching or detaching the records, we will see if we
// have done any attaching or detaching, and if we have we will touch these
// relationships if they are configured to touch on any database updates.
if (count($changes['attached']) ||
count($changes['updated']) ||
count($changes['detached'])) {
$this->touchIfTouching();
}
return $changes;
}
/**
* Sync the intermediate tables with a list of IDs or collection of models within a transaction.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @param bool $detaching
* @return array{attached: array, detached: array, updated: array}
*
* @throws \Throwable
*/
public function syncOrFail($ids, $detaching = true)
{
return $this->parent->getConnection()->transaction(fn () => $this->sync($ids, $detaching));
}
/**
* Sync the intermediate tables with a list of IDs without detaching within a transaction.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @return array{attached: array, detached: array, updated: array}
*
* @throws \Throwable
*/
public function syncWithoutDetachingOrFail($ids)
{
return $this->syncOrFail($ids, false);
}
/**
* Sync the intermediate tables with a list of IDs or collection of models with the given pivot values.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
* @param array $values
* @param bool $detaching
* @return array{attached: array, detached: array, updated: array}
*/
public function syncWithPivotValues($ids, array $values, bool $detaching = true)
{
return $this->sync((new BaseCollection($this->parseIds($ids)))->mapWithKeys(function ($id) use ($values) {
return [$id => $values];
}), $detaching);
}
/**
* Sync the intermediate tables with a list of IDs with the given pivot values within a transaction.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
* @param array $values
* @param bool $detaching
* @return array{attached: array, detached: array, updated: array}
*
* @throws \Throwable
*/
public function syncWithPivotValuesOrFail($ids, array $values, bool $detaching = true)
{
return $this->parent->getConnection()->transaction(fn () => $this->syncWithPivotValues($ids, $values, $detaching));
}
/**
* Format the sync / toggle record list so that it is keyed by ID.
*
* @param array $records
* @return array
*/
protected function formatRecordsList(array $records)
{
return (new BaseCollection($records))->mapWithKeys(function ($attributes, $id) {
if (! is_array($attributes)) {
[$id, $attributes] = [$attributes, []];
}
if ($id instanceof BackedEnum) {
$id = $id->value;
}
return [$id => $attributes];
})->all();
}
/**
* Attach all of the records that aren't in the given current records.
*
* @param array $records
* @param array $current
* @param bool $touch
* @return array
*/
protected function attachNew(array $records, array $current, $touch = true)
{
$changes = ['attached' => [], 'updated' => []];
foreach ($records as $id => $attributes) {
// If the ID is not in the list of existing pivot IDs, we will insert a new pivot
// record, otherwise, we will just update this existing record on this joining
// table, so that the developers will easily update these records pain free.
if (! in_array($id, $current)) {
$this->attach($id, $attributes, $touch);
$changes['attached'][] = $this->castKey($id);
}
// Now we'll try to update an existing pivot record with the attributes that were
// given to the method. If the model is actually updated we will add it to the
// list of updated pivot records so we return them back out to the consumer.
elseif (count($attributes) > 0 &&
$this->updateExistingPivot($id, $attributes, $touch)) {
$changes['updated'][] = $this->castKey($id);
}
}
return $changes;
}
/**
* Update an existing pivot record on the table.
*
* @param mixed $id
* @param array $attributes
* @param bool $touch
* @return int
*/
public function updateExistingPivot($id, array $attributes, $touch = true)
{
if ($this->using) {
return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch);
}
if ($this->hasPivotColumn($this->updatedAt())) {
$attributes = $this->addTimestampsToAttachment($attributes, true);
}
$updated = $this->newPivotStatementForId($id)->update(
$this->castAttributes($attributes)
);
if ($touch) {
$this->touchIfTouching();
}
return $updated;
}
/**
* Update an existing pivot record on the table within a transaction.
*
* @param mixed $id
* @param array $attributes
* @param bool $touch
* @return int
*
* @throws \Throwable
*/
public function updateExistingPivotOrFail($id, array $attributes, $touch = true)
{
return $this->parent->getConnection()->transaction(fn () => $this->updateExistingPivot($id, $attributes, $touch));
}
/**
* Update an existing pivot record on the table via a custom class.
*
* @param mixed $id
* @param array $attributes
* @param bool $touch
* @return int
*/
protected function updateExistingPivotUsingCustomClass($id, array $attributes, $touch)
{
$pivot = $this->getCurrentlyAttachedPivotsForIds($id)->first();
$updated = $pivot ? $pivot->fill($attributes)->isDirty() : false;
if ($updated) {
$pivot->save();
}
if ($touch) {
$this->touchIfTouching();
}
return (int) $updated;
}
/**
* Attach a model to the parent.
*
* @param mixed $ids
* @param array $attributes
* @param bool $touch
* @return void
*/
public function attach($ids, array $attributes = [], $touch = true)
{
if ($this->using) {
$this->attachUsingCustomClass($ids, $attributes);
} else {
// Here we will insert the attachment records into the pivot table. Once we have
// inserted the records, we will touch the relationships if necessary and the
// function will return. We can parse the IDs before inserting the records.
$this->newPivotStatement()->insert($this->formatAttachRecords(
$this->parseIds($ids), $attributes
));
}
if ($touch) {
$this->touchIfTouching();
}
}
/**
* Attach a model to the parent within a transaction.
*
* @param mixed $ids
* @param array $attributes
* @param bool $touch
* @return void
*
* @throws \Throwable
*/
public function attachOrFail($ids, array $attributes = [], $touch = true)
{
$this->parent->getConnection()->transaction(fn () => $this->attach($ids, $attributes, $touch));
}
/**
* Attach a model to the parent using a custom class.
*
* @param mixed $ids
* @param array $attributes
* @return void
*/
protected function attachUsingCustomClass($ids, array $attributes)
{
$records = $this->formatAttachRecords(
$this->parseIds($ids), $attributes
);
foreach ($records as $record) {
$this->newPivot($record, false)->save();
}
}
/**
* Create an array of records to insert into the pivot table.
*
* @param array $ids
* @param array $attributes
* @return array
*/
protected function formatAttachRecords($ids, array $attributes)
{
$records = [];
$hasTimestamps = ($this->hasPivotColumn($this->createdAt()) ||
$this->hasPivotColumn($this->updatedAt()));
// To create the attachment records, we will simply spin through the IDs given
// and create a new record to insert for each ID. Each ID may actually be a
// key in the array, with extra attributes to be placed in other columns.
foreach ($ids as $key => $value) {
$records[] = $this->formatAttachRecord(
$key, $value, $attributes, $hasTimestamps
);
}
return $records;
}
/**
* Create a full attachment record payload.
*
* @param int $key
* @param mixed $value
* @param array $attributes
* @param bool $hasTimestamps
* @return array
*/
protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps)
{
[$id, $attributes] = $this->extractAttachIdAndAttributes($key, $value, $attributes);
return array_merge(
$this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes)
);
}
/**
* Get the attach record ID and extra attributes.
*
* @param mixed $key
* @param mixed $value
* @param array $attributes
* @return array
*/
protected function extractAttachIdAndAttributes($key, $value, array $attributes)
{
return is_array($value)
? [$key, array_merge($value, $attributes)]
: [$value, $attributes];
}
/**
* Create a new pivot attachment record.
*
* @param int $id
* @param bool $timed
* @return array
*/
protected function baseAttachRecord($id, $timed)
{
$record[$this->relatedPivotKey] = $id;
$record[$this->foreignPivotKey] = $this->parent->{$this->parentKey};
// If the record needs to have creation and update timestamps, we will make
// them by calling the parent model's "freshTimestamp" method which will
// provide us with a fresh timestamp in this model's preferred format.
if ($timed) {
$record = $this->addTimestampsToAttachment($record);
}
foreach ($this->pivotValues as $value) {
$record[$value['column']] = $value['value'];
}
return $record;
}
/**
* Set the creation and update timestamps on an attach record.
*
* @param array $record
* @param bool $exists
* @return array
*/
protected function addTimestampsToAttachment(array $record, $exists = false)
{
$fresh = $this->parent->freshTimestamp();
if ($this->using) {
$pivotModel = new $this->using;
$fresh = $pivotModel->fromDateTime($fresh);
}
if (! $exists && $this->hasPivotColumn($this->createdAt())) {
$record[$this->createdAt()] = $fresh;
}
if ($this->hasPivotColumn($this->updatedAt())) {
$record[$this->updatedAt()] = $fresh;
}
return $record;
}
/**
* Determine whether the given column is defined as a pivot column.
*
* @param string $column
* @return bool
*/
public function hasPivotColumn($column)
{
return in_array($column, $this->pivotColumns);
}
/**
* Detach models from the relationship.
*
* @param mixed $ids
* @param bool $touch
* @return int
*/
public function detach($ids = null, $touch = true)
{
if ($this->using) {
$results = $this->detachUsingCustomClass($ids);
} else {
$query = $this->newPivotQuery();
// If associated IDs were passed to the method we will only delete those
// associations, otherwise all of the association ties will be broken.
// We'll return the numbers of affected rows when we do the deletes.
if (! is_null($ids)) {
$ids = $this->parseIds($ids);
if (empty($ids)) {
return 0;
}
$query->whereIn($this->getQualifiedRelatedPivotKeyName(), (array) $ids);
}
// Once we have all of the conditions set on the statement, we are ready
// to run the delete on the pivot table. Then, if the touch parameter
// is true, we will go ahead and touch all related models to sync.
$results = $query->delete();
}
if ($touch) {
$this->touchIfTouching();
}
return $results;
}
/**
* Detach models from the relationship within a transaction.
*
* @param mixed $ids
* @param bool $touch
* @return int
*
* @throws \Throwable
*/
public function detachOrFail($ids = null, $touch = true)
{
return $this->parent->getConnection()->transaction(fn () => $this->detach($ids, $touch));
}
/**
* Detach models from the relationship using a custom class.
*
* @param mixed $ids
* @return int
*/
protected function detachUsingCustomClass($ids)
{
$results = 0;
$records = $this->getCurrentlyAttachedPivotsForIds($ids);
foreach ($records as $record) {
$results += $record->delete();
}
return $results;
}
/**
* Get the pivot models that are currently attached.
*
* @return \Illuminate\Support\Collection
*/
protected function getCurrentlyAttachedPivots()
{
return $this->getCurrentlyAttachedPivotsForIds();
}
/**
* Get the pivot models that are currently attached, filtered by related model keys.
*
* @param mixed $ids
* @return \Illuminate\Support\Collection
*/
protected function getCurrentlyAttachedPivotsForIds($ids = null)
{
return $this->newPivotQuery()
->when(! is_null($ids), fn ($query) => $query->whereIn(
$this->getQualifiedRelatedPivotKeyName(), $this->parseIds($ids)
))
->get()
->map(function ($record) {
$class = $this->using ?: Pivot::class;
$pivot = $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true);
return $pivot
->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)
->setRelatedModel($this->related);
});
}
/**
* Create a new pivot model instance.
*
* @param array $attributes
* @param bool $exists
* @return \Illuminate\Database\Eloquent\Relations\Pivot
*/
public function newPivot(array $attributes = [], $exists = false)
{
$attributes = array_merge(array_column($this->pivotValues, 'value', 'column'), $attributes);
$pivot = $this->related->newPivot(
$this->parent, $attributes, $this->table, $exists, $this->using
);
return $pivot
->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)
->setRelatedModel($this->related);
}
/**
* Create a new existing pivot model instance.
*
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Relations\Pivot
*/
public function newExistingPivot(array $attributes = [])
{
return $this->newPivot($attributes, true);
}
/**
* Get a new plain query builder for the pivot table.
*
* @return \Illuminate\Database\Query\Builder
*/
public function newPivotStatement()
{
return $this->query->getQuery()->newQuery()->from($this->table);
}
/**
* Get a new pivot statement for a given "other" ID.
*
* @param mixed $id
* @return \Illuminate\Database\Query\Builder
*/
public function newPivotStatementForId($id)
{
return $this->newPivotQuery()->whereIn($this->getQualifiedRelatedPivotKeyName(), $this->parseIds($id));
}
/**
* Create a new query builder for the pivot table.
*
* @return \Illuminate\Database\Query\Builder
*/
public function newPivotQuery()
{
$query = $this->newPivotStatement();
foreach ($this->pivotWheres as $arguments) {
$query->where(...$arguments);
}
foreach ($this->pivotWhereIns as $arguments) {
$query->whereIn(...$arguments);
}
foreach ($this->pivotWhereNulls as $arguments) {
$query->whereNull(...$arguments);
}
return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey});
}
/**
* Set the columns on the pivot table to retrieve.
*
* @param mixed $columns
* @return $this
*/
public function withPivot($columns)
{
$this->pivotColumns = array_merge(
$this->pivotColumns, is_array($columns) ? $columns : func_get_args()
);
return $this;
}
/**
* Get all of the IDs from the given mixed value.
*
* @param mixed $value
* @return array
*/
protected function parseIds($value)
{
if ($value instanceof Model) {
return [$value->{$this->relatedKey}];
}
if ($value instanceof EloquentCollection) {
return $value->pluck($this->relatedKey)->all();
}
if ($value instanceof BaseCollection || is_array($value)) {
return (new BaseCollection($value))
->map(fn ($item) => $item instanceof Model ? $item->{$this->relatedKey} : $item)
->all();
}
return (array) $value;
}
/**
* Get the ID from the given mixed value.
*
* @param mixed $value
* @return mixed
*/
protected function parseId($value)
{
return $value instanceof Model ? $value->{$this->relatedKey} : $value;
}
/**
* Cast the given keys to integers if they are numeric and string otherwise.
*
* @param array $keys
* @return array
*/
protected function castKeys(array $keys)
{
return array_map(function ($v) {
return $this->castKey($v);
}, $keys);
}
/**
* Cast the given key to convert to primary key type.
*
* @param mixed $key
* @return mixed
*/
protected function castKey($key)
{
return $this->getTypeSwapValue(
$this->related->getKeyType(),
$key
);
}
/**
* Cast the given pivot attributes.
*
* @param array $attributes
* @return array
*/
protected function castAttributes($attributes)
{
return $this->using
? $this->newPivot()->fill($attributes)->getAttributes()
: $attributes;
}
/**
* Converts a given value to a given type value.
*
* @param string $type
* @param mixed $value
* @return mixed
*/
protected function getTypeSwapValue($type, $value)
{
return match (strtolower($type)) {
'int', 'integer' => (int) $value,
'real', 'float', 'double' => (float) $value,
'string' => (string) $value,
default => $value,
};
}
}
@@ -0,0 +1,63 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Database\Eloquent\Model;
trait SupportsDefaultModels
{
/**
* Indicates if a default model instance should be used.
*
* Alternatively, may be a Closure or array.
*
* @var \Closure|array|bool
*/
protected $withDefault;
/**
* Make a new related instance for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @return \Illuminate\Database\Eloquent\Model
*/
abstract protected function newRelatedInstanceFor(Model $parent);
/**
* Return a new model instance in case the relationship does not exist.
*
* @param \Closure|array|bool $callback
* @return $this
*/
public function withDefault($callback = true)
{
$this->withDefault = $callback;
return $this;
}
/**
* Get the default value for this relation.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @return \Illuminate\Database\Eloquent\Model|null
*/
protected function getDefaultFor(Model $parent)
{
if (! $this->withDefault) {
return;
}
$instance = $this->newRelatedInstanceFor($parent);
if (is_callable($this->withDefault)) {
return call_user_func($this->withDefault, $instance, $parent) ?: $instance;
}
if (is_array($this->withDefault)) {
$instance->forceFill($this->withDefault);
}
return $instance;
}
}
@@ -0,0 +1,157 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
trait SupportsInverseRelations
{
/**
* The name of the inverse relationship.
*
* @var string|null
*/
protected ?string $inverseRelationship = null;
/**
* Instruct Eloquent to link the related models back to the parent after the relationship query has run.
*
* Alias of "chaperone".
*
* @param string|null $relation
* @return $this
*/
public function inverse(?string $relation = null)
{
return $this->chaperone($relation);
}
/**
* Instruct Eloquent to link the related models back to the parent after the relationship query has run.
*
* @param string|null $relation
* @return $this
*/
public function chaperone(?string $relation = null)
{
$relation ??= $this->guessInverseRelation();
if (! $relation || ! $this->getModel()->isRelation($relation)) {
throw RelationNotFoundException::make($this->getModel(), $relation ?: 'null');
}
if ($this->inverseRelationship === null && $relation) {
$this->query->afterQuery(function ($result) {
return $this->inverseRelationship
? $this->applyInverseRelationToCollection($result, $this->getParent())
: $result;
});
}
$this->inverseRelationship = $relation;
return $this;
}
/**
* Guess the name of the inverse relationship.
*
* @return string|null
*/
protected function guessInverseRelation(): ?string
{
return Arr::first(
$this->getPossibleInverseRelations(),
fn ($relation) => $relation && $this->getModel()->isRelation($relation)
);
}
/**
* Get the possible inverse relations for the parent model.
*
* @return array<non-empty-string>
*/
protected function getPossibleInverseRelations(): array
{
return array_filter(array_unique([
Str::camel(Str::beforeLast($this->getForeignKeyName(), $this->getParent()->getKeyName())),
Str::camel(Str::beforeLast($this->getParent()->getForeignKey(), $this->getParent()->getKeyName())),
Str::camel(class_basename($this->getParent())),
'owner',
get_class($this->getParent()) === get_class($this->getModel()) ? 'parent' : null,
]));
}
/**
* Set the inverse relation on all models in a collection.
*
* @param \Illuminate\Database\Eloquent\Collection $models
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function applyInverseRelationToCollection($models, ?Model $parent = null)
{
$parent ??= $this->getParent();
foreach ($models as $model) {
$model instanceof Model && $this->applyInverseRelationToModel($model, $parent);
}
return $models;
}
/**
* Set the inverse relation on a model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return \Illuminate\Database\Eloquent\Model
*/
protected function applyInverseRelationToModel(Model $model, ?Model $parent = null)
{
if ($inverse = $this->getInverseRelationship()) {
$parent ??= $this->getParent();
$model->setRelation($inverse, $parent);
}
return $model;
}
/**
* Get the name of the inverse relationship.
*
* @return string|null
*/
public function getInverseRelationship()
{
return $this->inverseRelationship;
}
/**
* Remove the chaperone / inverse relationship for this query.
*
* Alias of "withoutChaperone".
*
* @return $this
*/
public function withoutInverse()
{
return $this->withoutChaperone();
}
/**
* Remove the chaperone / inverse relationship for this query.
*
* @return $this
*/
public function withoutChaperone()
{
$this->inverseRelationship = null;
return $this;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TRelatedModel, TDeclaringModel, \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>>
*/
class HasMany extends HasOneOrMany
{
/**
* Convert the relationship to a "has one" relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TDeclaringModel>
*/
public function one()
{
return HasOne::noConstraints(fn () => tap(
new HasOne(
$this->getQuery(),
$this->parent,
$this->foreignKey,
$this->localKey
),
function ($hasOne) {
if ($inverse = $this->getInverseRelationship()) {
$hasOne->inverse($inverse);
}
}
));
}
/** @inheritDoc */
public function getResults()
{
return ! is_null($this->getParentKey())
? $this->query->get()
: $this->related->newCollection();
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->related->newCollection());
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
return $this->matchMany($models, $results, $relation);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel, \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>>
*/
class HasManyThrough extends HasOneOrManyThrough
{
use InteractsWithDictionary;
/**
* Convert the relationship to a "has one through" relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
*/
public function one()
{
return HasOneThrough::noConstraints(fn () => new HasOneThrough(
tap($this->getQuery(), fn (Builder $query) => $query->getQuery()->joins = []),
$this->farParent,
$this->throughParent,
$this->getFirstKeyName(),
$this->getForeignKeyName(),
$this->getLocalKeyName(),
$this->getSecondLocalKeyName(),
));
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->related->newCollection());
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model) {
$key = $this->getDictionaryKey($model->getAttribute($this->localKey));
if ($key !== null && isset($dictionary[$key])) {
$model->setRelation(
$relation, $this->related->newCollection($dictionary[$key])
);
}
}
return $models;
}
/** @inheritDoc */
public function getResults()
{
return ! is_null($this->farParent->{$this->localKey})
? $this->get()
: $this->related->newCollection();
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
use Illuminate\Database\Query\JoinClause;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TRelatedModel, TDeclaringModel, ?TRelatedModel>
*/
class HasOne extends HasOneOrMany implements SupportsPartialRelations
{
use ComparesRelatedModels, CanBeOneOfMany, SupportsDefaultModels;
/** @inheritDoc */
public function getResults()
{
if (is_null($this->getParentKey())) {
return $this->getDefaultFor($this->parent);
}
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->getDefaultFor($model));
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
return $this->matchOne($models, $results, $relation);
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($this->isOneOfMany()) {
$this->mergeOneOfManyJoinsTo($query);
}
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}
/**
* Add constraints for inner join subselect for one of many relationships.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param string|null $column
* @param string|null $aggregate
* @return void
*/
public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null)
{
$query->addSelect($this->foreignKey);
}
/**
* Get the columns that should be selected by the one of many subquery.
*
* @return array|string
*/
public function getOneOfManySubQuerySelectColumns()
{
return $this->foreignKey;
}
/**
* Add join query constraints for one of many relationships.
*
* @param \Illuminate\Database\Query\JoinClause $join
* @return void
*/
public function addOneOfManyJoinSubQueryConstraints(JoinClause $join)
{
$join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey));
}
/**
* Make a new related instance for the given model.
*
* @param TDeclaringModel $parent
* @return TRelatedModel
*/
public function newRelatedInstanceFor(Model $parent)
{
return tap($this->related->newInstance(), function ($instance) use ($parent) {
$instance->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey});
$this->applyInverseRelationToModel($instance, $parent);
});
}
/**
* Get the value of the model's foreign key.
*
* @param TRelatedModel $model
* @return int|string
*/
protected function getRelatedKeyFrom(Model $model)
{
return $model->getAttribute($this->getForeignKeyName());
}
}
+629
View File
@@ -0,0 +1,629 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Arr;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
* @template TResult
*
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TDeclaringModel, TResult>
*/
abstract class HasOneOrMany extends Relation
{
use InteractsWithDictionary, SupportsInverseRelations;
/**
* The foreign key of the parent model.
*
* @var string
*/
protected $foreignKey;
/**
* The local key of the parent model.
*
* @var string
*/
protected $localKey;
/**
* Create a new has one or many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $foreignKey
* @param string $localKey
*/
public function __construct(Builder $query, Model $parent, $foreignKey, $localKey)
{
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
parent::__construct($query, $parent);
}
/**
* Create and return an un-saved instance of the related model.
*
* @param array $attributes
* @return TRelatedModel
*/
public function make(array $attributes = [])
{
return tap($this->related->newInstance($attributes), function ($instance) {
$this->setForeignAttributesForCreate($instance);
$this->applyInverseRelationToModel($instance);
});
}
/**
* Create and return an un-saved instance of the related models.
*
* @param iterable $records
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function makeMany($records)
{
$instances = $this->related->newCollection();
foreach ($records as $record) {
$instances->push($this->make($record));
}
return $instances;
}
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints()
{
if (static::$constraints) {
$query = $this->getRelationQuery();
$query->where($this->foreignKey, '=', $this->getParentKey());
$query->whereNotNull($this->foreignKey);
}
}
/** @inheritDoc */
public function addEagerConstraints(array $models)
{
$whereIn = $this->whereInMethod($this->parent, $this->localKey);
$this->whereInEager(
$whereIn,
$this->foreignKey,
$this->getKeys($models, $this->localKey),
$this->getRelationQuery()
);
}
/**
* Match the eagerly loaded results to their single parents.
*
* @param array<int, TDeclaringModel> $models
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @param string $relation
* @return array<int, TDeclaringModel>
*/
public function matchOne(array $models, EloquentCollection $results, $relation)
{
return $this->matchOneOrMany($models, $results, $relation, 'one');
}
/**
* Match the eagerly loaded results to their many parents.
*
* @param array<int, TDeclaringModel> $models
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @param string $relation
* @return array<int, TDeclaringModel>
*/
public function matchMany(array $models, EloquentCollection $results, $relation)
{
return $this->matchOneOrMany($models, $results, $relation, 'many');
}
/**
* Match the eagerly loaded results to their many parents.
*
* @param array<int, TDeclaringModel> $models
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @param string $relation
* @param string $type
* @return array<int, TDeclaringModel>
*/
protected function matchOneOrMany(array $models, EloquentCollection $results, $relation, $type)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model) {
$key = $this->getDictionaryKey($model->getAttribute($this->localKey));
if ($key !== null && isset($dictionary[$key])) {
$related = $this->getRelationValue($dictionary, $key, $type);
$model->setRelation($relation, $related);
// Apply the inverse relation if we have one...
$type === 'one'
? $this->applyInverseRelationToModel($related, $model)
: $this->applyInverseRelationToCollection($related, $model);
}
}
return $models;
}
/**
* Get the value of a relationship by one or many type.
*
* @param array $dictionary
* @param string $key
* @param string $type
* @return mixed
*/
protected function getRelationValue(array $dictionary, $key, $type)
{
$value = $dictionary[$key];
return $type === 'one' ? reset($value) : $this->related->newCollection($value);
}
/**
* Build model dictionary keyed by the relation's foreign key.
*
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @return array<array<array-key, TRelatedModel>>
*/
protected function buildDictionary(EloquentCollection $results)
{
$foreign = $this->getForeignKeyName();
$dictionary = [];
$isAssociative = Arr::isAssoc($results->all());
foreach ($results as $key => $item) {
$pairKey = $this->getDictionaryKey($item->{$foreign});
if ($pairKey === null) {
continue;
}
if ($isAssociative) {
$dictionary[$pairKey][$key] = $item;
} else {
$dictionary[$pairKey][] = $item;
}
}
return $dictionary;
}
/**
* Find a model by its primary key or return a new instance of the related model.
*
* @param mixed $id
* @param array $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
*/
public function findOrNew($id, $columns = ['*'])
{
if (is_null($instance = $this->find($id, $columns))) {
$instance = $this->related->newInstance();
$this->setForeignAttributesForCreate($instance);
}
return $instance;
}
/**
* Get the first related model record matching the attributes or instantiate it.
*
* @param array $attributes
* @param (\Closure(): array)|array $values
* @return TRelatedModel
*/
public function firstOrNew(array $attributes = [], Closure|array $values = [])
{
if (is_null($instance = $this->where($attributes)->first())) {
$instance = $this->related->newInstance(array_merge($attributes, value($values)));
$this->setForeignAttributesForCreate($instance);
}
return $instance;
}
/**
* Get the first record matching the attributes. If the record is not found, create it.
*
* @param array $attributes
* @param (\Closure(): array)|array $values
* @return TRelatedModel
*/
public function firstOrCreate(array $attributes = [], Closure|array $values = [])
{
if (is_null($instance = (clone $this)->where($attributes)->first())) {
$instance = $this->createOrFirst($attributes, $values);
}
return $instance;
}
/**
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
*
* @param array $attributes
* @param (\Closure(): array)|array $values
* @return TRelatedModel
*
* @throws \Illuminate\Database\UniqueConstraintViolationException
*/
public function createOrFirst(array $attributes = [], Closure|array $values = [])
{
try {
return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values))));
} catch (UniqueConstraintViolationException $e) {
return $this->useWritePdo()->where($attributes)->first() ?? throw $e;
}
}
/**
* Create or update a related record matching the attributes, and fill it with values.
*
* @param array $attributes
* @param (\Closure(): array)|array $values
* @return TRelatedModel
*/
public function updateOrCreate(array $attributes, Closure|array $values = [])
{
return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) {
if (! $instance->wasRecentlyCreated) {
$instance->fill(value($values))->save();
}
});
}
/**
* Insert new records or update the existing ones.
*
* @param array $values
* @param array|string $uniqueBy
* @param array|null $update
* @return int
*/
public function upsert(array $values, $uniqueBy, $update = null)
{
if (! empty($values) && ! is_array(array_first($values))) {
$values = [$values];
}
foreach ($values as $key => $value) {
$values[$key][$this->getForeignKeyName()] = $this->getParentKey();
}
return $this->getQuery()->upsert($values, $uniqueBy, $update);
}
/**
* Attach a model instance to the parent model.
*
* @param TRelatedModel $model
* @return TRelatedModel|false
*/
public function save(Model $model)
{
$this->setForeignAttributesForCreate($model);
return $model->save() ? $model : false;
}
/**
* Attach a model instance without raising any events to the parent model.
*
* @param TRelatedModel $model
* @return TRelatedModel|false
*/
public function saveQuietly(Model $model)
{
return Model::withoutEvents(function () use ($model) {
return $this->save($model);
});
}
/**
* Attach a collection of models to the parent instance.
*
* @param iterable<TRelatedModel> $models
* @return iterable<TRelatedModel>
*/
public function saveMany($models)
{
foreach ($models as $model) {
$this->save($model);
}
return $models;
}
/**
* Attach a collection of models to the parent instance without raising any events to the parent model.
*
* @param iterable<TRelatedModel> $models
* @return iterable<TRelatedModel>
*/
public function saveManyQuietly($models)
{
return Model::withoutEvents(function () use ($models) {
return $this->saveMany($models);
});
}
/**
* Create a new instance of the related model.
*
* @param array $attributes
* @return TRelatedModel
*/
public function create(array $attributes = [])
{
return tap($this->related->newInstance($attributes), function ($instance) {
$this->setForeignAttributesForCreate($instance);
$instance->save();
$this->applyInverseRelationToModel($instance);
});
}
/**
* Create a new instance of the related model without raising any events to the parent model.
*
* @param array $attributes
* @return TRelatedModel
*/
public function createQuietly(array $attributes = [])
{
return Model::withoutEvents(fn () => $this->create($attributes));
}
/**
* Create a new instance of the related model. Allow mass-assignment.
*
* @param array $attributes
* @return TRelatedModel
*/
public function forceCreate(array $attributes = [])
{
$attributes[$this->getForeignKeyName()] = $this->getParentKey();
return $this->applyInverseRelationToModel($this->related->forceCreate($attributes));
}
/**
* Create a new instance of the related model with mass assignment without raising model events.
*
* @param array $attributes
* @return TRelatedModel
*/
public function forceCreateQuietly(array $attributes = [])
{
return Model::withoutEvents(fn () => $this->forceCreate($attributes));
}
/**
* Create a Collection of new instances of the related model.
*
* @param iterable $records
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function createMany(iterable $records)
{
$instances = $this->related->newCollection();
foreach ($records as $record) {
$instances->push($this->create($record));
}
return $instances;
}
/**
* Create a Collection of new instances of the related model without raising any events to the parent model.
*
* @param iterable $records
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function createManyQuietly(iterable $records)
{
return Model::withoutEvents(fn () => $this->createMany($records));
}
/**
* Create a Collection of new instances of the related model, allowing mass-assignment.
*
* @param iterable $records
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function forceCreateMany(iterable $records)
{
$instances = $this->related->newCollection();
foreach ($records as $record) {
$instances->push($this->forceCreate($record));
}
return $instances;
}
/**
* Create a Collection of new instances of the related model, allowing mass-assignment and without raising any events to the parent model.
*
* @param iterable $records
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function forceCreateManyQuietly(iterable $records)
{
return Model::withoutEvents(fn () => $this->forceCreateMany($records));
}
/**
* Set the foreign ID for creating a related model.
*
* @param TRelatedModel $model
* @return void
*/
protected function setForeignAttributesForCreate(Model $model)
{
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
foreach ($this->getQuery()->pendingAttributes as $key => $value) {
$attributes ??= $model->getAttributes();
if (! array_key_exists($key, $attributes)) {
$model->setAttribute($key, $value);
}
}
$this->applyInverseRelationToModel($model);
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($query->getQuery()->from == $parentQuery->getQuery()->from) {
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
}
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}
/**
* Add the constraints for a relationship query on the same table.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
* @param mixed $columns
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());
$query->getModel()->setTable($hash);
return $query->select($columns)->whereColumn(
$this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->getForeignKeyName()
);
}
/**
* Alias to set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function take($value)
{
return $this->limit($value);
}
/**
* Set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function limit($value)
{
if ($this->parent->exists) {
$this->query->limit($value);
} else {
$this->query->groupLimit($value, $this->getExistenceCompareKey());
}
return $this;
}
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
public function getExistenceCompareKey()
{
return $this->getQualifiedForeignKeyName();
}
/**
* Get the key value of the parent's local key.
*
* @return mixed
*/
public function getParentKey()
{
return $this->parent->getAttribute($this->localKey);
}
/**
* Get the fully-qualified parent key name.
*
* @return string
*/
public function getQualifiedParentKeyName()
{
return $this->parent->qualifyColumn($this->localKey);
}
/**
* Get the plain foreign key.
*
* @return string
*/
public function getForeignKeyName()
{
$segments = explode('.', $this->getQualifiedForeignKeyName());
return array_last($segments);
}
/**
* Get the foreign key for the relationship.
*
* @return string
*/
public function getQualifiedForeignKeyName()
{
return $this->foreignKey;
}
/**
* Get the local key for the relationship.
*
* @return string
*/
public function getLocalKeyName()
{
return $this->localKey;
}
}
@@ -0,0 +1,872 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Arr;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
* @template TResult
*
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TIntermediateModel, TResult>
*/
abstract class HasOneOrManyThrough extends Relation
{
use InteractsWithDictionary;
/**
* The "through" parent model instance.
*
* @var TIntermediateModel
*/
protected $throughParent;
/**
* The far parent model instance.
*
* @var TDeclaringModel
*/
protected $farParent;
/**
* The near key on the relationship.
*
* @var string
*/
protected $firstKey;
/**
* The far key on the relationship.
*
* @var string
*/
protected $secondKey;
/**
* The local key on the relationship.
*
* @var string
*/
protected $localKey;
/**
* The local key on the intermediary model.
*
* @var string
*/
protected $secondLocalKey;
/**
* Create a new has many through relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $farParent
* @param TIntermediateModel $throughParent
* @param string $firstKey
* @param string $secondKey
* @param string $localKey
* @param string $secondLocalKey
*/
public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
{
$this->localKey = $localKey;
$this->firstKey = $firstKey;
$this->secondKey = $secondKey;
$this->farParent = $farParent;
$this->throughParent = $throughParent;
$this->secondLocalKey = $secondLocalKey;
parent::__construct($query, $throughParent);
}
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints()
{
$query = $this->getRelationQuery();
$this->performJoin($query);
if (static::$constraints) {
$localValue = $this->farParent[$this->localKey];
$query->where($this->getQualifiedFirstKeyName(), '=', $localValue);
}
}
/**
* Set the join clause on the query.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel>|null $query
* @return void
*/
protected function performJoin(?Builder $query = null)
{
$query ??= $this->query;
$farKey = $this->getQualifiedFarKeyName();
$query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey);
if ($this->throughParentSoftDeletes()) {
$query->withGlobalScope('SoftDeletableHasManyThrough', function ($query) {
$query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
});
}
}
/**
* Get the fully-qualified parent key name.
*
* @return string
*/
public function getQualifiedParentKeyName()
{
return $this->parent->qualifyColumn($this->secondLocalKey);
}
/**
* Determine whether "through" parent of the relation uses Soft Deletes.
*
* @return bool
*/
public function throughParentSoftDeletes()
{
return $this->throughParent::isSoftDeletable();
}
/**
* Indicate that trashed "through" parents should be included in the query.
*
* @return $this
*/
public function withTrashedParents()
{
$this->query->withoutGlobalScope('SoftDeletableHasManyThrough');
return $this;
}
/** @inheritDoc */
public function addEagerConstraints(array $models)
{
$whereIn = $this->whereInMethod($this->farParent, $this->localKey);
$this->whereInEager(
$whereIn,
$this->getQualifiedFirstKeyName(),
$this->getKeys($models, $this->localKey),
$this->getRelationQuery(),
);
}
/**
* Build model dictionary keyed by the relation's foreign key.
*
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @return array<array<array-key, TRelatedModel>>
*/
protected function buildDictionary(EloquentCollection $results)
{
$dictionary = [];
$isAssociative = Arr::isAssoc($results->all());
// First we will create a dictionary of models keyed by the foreign key of the
// relationship as this will allow us to quickly access all of the related
// models without having to do nested looping which will be quite slow.
foreach ($results as $key => $result) {
if ($isAssociative) {
$dictionary[$result->laravel_through_key][$key] = $result;
} else {
$dictionary[$result->laravel_through_key][] = $result;
}
}
return $dictionary;
}
/**
* Get the first related model record matching the attributes or instantiate it.
*
* @param array $attributes
* @param array $values
* @return TRelatedModel
*/
public function firstOrNew(array $attributes = [], array $values = [])
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
return $this->related->newInstance(array_merge($attributes, $values));
}
/**
* Get the first record matching the attributes. If the record is not found, create it.
*
* @param array $attributes
* @param (\Closure(): array)|array $values
* @return TRelatedModel
*/
public function firstOrCreate(array $attributes = [], Closure|array $values = [])
{
if (! is_null($instance = (clone $this)->where($attributes)->first())) {
return $instance;
}
return $this->createOrFirst(array_merge($attributes, value($values)));
}
/**
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
*
* @param array $attributes
* @param (\Closure(): array)|array $values
* @return TRelatedModel
*
* @throws \Illuminate\Database\UniqueConstraintViolationException
*/
public function createOrFirst(array $attributes = [], Closure|array $values = [])
{
try {
return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values))));
} catch (UniqueConstraintViolationException $exception) {
return $this->where($attributes)->first() ?? throw $exception;
}
}
/**
* Create or update a related record matching the attributes, and fill it with values.
*
* @param array $attributes
* @param array $values
* @return TRelatedModel
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) {
if (! $instance->wasRecentlyCreated) {
$instance->fill($values)->save();
}
});
}
/**
* Add a basic where clause to the query, and return the first result.
*
* @param \Closure|string|array $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
* @return TRelatedModel|null
*/
public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
{
return $this->where($column, $operator, $value, $boolean)->first();
}
/**
* Execute the query and get the first related model.
*
* @param array $columns
* @return TRelatedModel|null
*/
public function first($columns = ['*'])
{
$results = $this->limit(1)->get($columns);
return count($results) > 0 ? $results->first() : null;
}
/**
* Execute the query and get the first result or throw an exception.
*
* @param array $columns
* @return TRelatedModel
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
*/
public function firstOrFail($columns = ['*'])
{
if (! is_null($model = $this->first($columns))) {
return $model;
}
throw (new ModelNotFoundException)->setModel(get_class($this->related));
}
/**
* Execute the query and get the first result or call a callback.
*
* @template TValue
*
* @param (\Closure(): TValue)|list<string> $columns
* @param (\Closure(): TValue)|null $callback
* @return TRelatedModel|TValue
*/
public function firstOr($columns = ['*'], ?Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;
$columns = ['*'];
}
if (! is_null($model = $this->first($columns))) {
return $model;
}
return $callback();
}
/**
* Find a related model by its primary key.
*
* @param mixed $id
* @param array $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel|null)
*/
public function find($id, $columns = ['*'])
{
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
return $this->where(
$this->getRelated()->getQualifiedKeyName(), '=', $id
)->first($columns);
}
/**
* Find a sole related model by its primary key.
*
* @param mixed $id
* @param array $columns
* @return TRelatedModel
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
* @throws \Illuminate\Database\MultipleRecordsFoundException
*/
public function findSole($id, $columns = ['*'])
{
return $this->where(
$this->getRelated()->getQualifiedKeyName(), '=', $id
)->sole($columns);
}
/**
* Find multiple related models by their primary keys.
*
* @param \Illuminate\Contracts\Support\Arrayable|array $ids
* @param array $columns
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function findMany($ids, $columns = ['*'])
{
$ids = $ids instanceof Arrayable ? $ids->toArray() : $ids;
if (empty($ids)) {
return $this->getRelated()->newCollection();
}
return $this->whereIn(
$this->getRelated()->getQualifiedKeyName(), $ids
)->get($columns);
}
/**
* Find a related model by its primary key or throw an exception.
*
* @param mixed $id
* @param array $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
*/
public function findOrFail($id, $columns = ['*'])
{
$result = $this->find($id, $columns);
$id = $id instanceof Arrayable ? $id->toArray() : $id;
if (is_array($id)) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
}
/**
* Find a related model by its primary key or call a callback.
*
* @template TValue
*
* @param mixed $id
* @param (\Closure(): TValue)|list<string>|string $columns
* @param (\Closure(): TValue)|null $callback
* @return (
* $id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>)
* ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>|TValue
* : TRelatedModel|TValue
* )
*/
public function findOr($id, $columns = ['*'], ?Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;
$columns = ['*'];
}
$result = $this->find($id, $columns);
$id = $id instanceof Arrayable ? $id->toArray() : $id;
if (is_array($id)) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
return $callback();
}
/** @inheritDoc */
public function get($columns = ['*'])
{
$builder = $this->prepareQueryBuilder($columns);
$models = $builder->getModels();
// If we actually found models we will also eager load any relationships that
// have been specified as needing to be eager loaded. This will solve the
// n + 1 query problem for the developer and also increase performance.
if (count($models) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $this->query->applyAfterQueryCallbacks(
$this->related->newCollection($models)
);
}
/**
* Get a paginator for the "select" statement.
*
* @param int|null $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$this->query->addSelect($this->shouldSelect($columns));
return $this->query->paginate($perPage, $columns, $pageName, $page);
}
/**
* Paginate the given query into a simple paginator.
*
* @param int|null $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$this->query->addSelect($this->shouldSelect($columns));
return $this->query->simplePaginate($perPage, $columns, $pageName, $page);
}
/**
* Paginate the given query into a cursor paginator.
*
* @param int|null $perPage
* @param array $columns
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
$this->query->addSelect($this->shouldSelect($columns));
return $this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor);
}
/**
* Set the select clause for the relation query.
*
* @param array $columns
* @return array
*/
protected function shouldSelect(array $columns = ['*'])
{
if ($columns == ['*']) {
$columns = [$this->related->qualifyColumn('*')];
}
return array_merge($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']);
}
/**
* Chunk the results of the query.
*
* @param int $count
* @param callable $callback
* @return bool
*/
public function chunk($count, callable $callback)
{
return $this->prepareQueryBuilder()->chunk($count, $callback);
}
/**
* Chunk the results of a query by comparing numeric IDs.
*
* @param int $count
* @param callable $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkById($count, callable $callback, $column = null, $alias = null)
{
$column ??= $this->getRelated()->getQualifiedKeyName();
$alias ??= $this->getRelated()->getKeyName();
return $this->prepareQueryBuilder()->chunkById($count, $callback, $column, $alias);
}
/**
* Chunk the results of a query by comparing IDs in descending order.
*
* @param int $count
* @param callable $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
{
$column ??= $this->getRelated()->getQualifiedKeyName();
$alias ??= $this->getRelated()->getKeyName();
return $this->prepareQueryBuilder()->chunkByIdDesc($count, $callback, $column, $alias);
}
/**
* Execute a callback over each item while chunking by ID.
*
* @param callable $callback
* @param int $count
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
{
$column ??= $this->getRelated()->getQualifiedKeyName();
$alias ??= $this->getRelated()->getKeyName();
return $this->prepareQueryBuilder()->eachById($callback, $count, $column, $alias);
}
/**
* Get a generator for the given query.
*
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
*/
public function cursor()
{
return $this->prepareQueryBuilder()->cursor();
}
/**
* Execute a callback over each item while chunking.
*
* @param callable $callback
* @param int $count
* @return bool
*/
public function each(callable $callback, $count = 1000)
{
return $this->chunk($count, function ($results) use ($callback) {
foreach ($results as $key => $value) {
if ($callback($value, $key) === false) {
return false;
}
}
});
}
/**
* Query lazily, by chunks of the given size.
*
* @param int $chunkSize
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
*/
public function lazy($chunkSize = 1000)
{
return $this->prepareQueryBuilder()->lazy($chunkSize);
}
/**
* Query lazily, by chunking the results of a query by comparing IDs.
*
* @param int $chunkSize
* @param string|null $column
* @param string|null $alias
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
*/
public function lazyById($chunkSize = 1000, $column = null, $alias = null)
{
$column ??= $this->getRelated()->getQualifiedKeyName();
$alias ??= $this->getRelated()->getKeyName();
return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias);
}
/**
* Query lazily, by chunking the results of a query by comparing IDs in descending order.
*
* @param int $chunkSize
* @param string|null $column
* @param string|null $alias
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
*/
public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
{
$column ??= $this->getRelated()->getQualifiedKeyName();
$alias ??= $this->getRelated()->getKeyName();
return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias);
}
/**
* Prepare the query builder for query execution.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
protected function prepareQueryBuilder($columns = ['*'])
{
$builder = $this->query->applyScopes();
return $builder->addSelect(
$this->shouldSelect($builder->getQuery()->columns ? [] : $columns)
);
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($parentQuery->getQuery()->from === $query->getQuery()->from) {
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
}
if ($parentQuery->getQuery()->from === $this->throughParent->getTable()) {
return $this->getRelationExistenceQueryForThroughSelfRelation($query, $parentQuery, $columns);
}
$this->performJoin($query);
return $query->select($columns)->whereColumn(
$this->getQualifiedLocalKeyName(), '=', $this->getQualifiedFirstKeyName()
);
}
/**
* Add the constraints for a relationship query on the same table.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
* @param mixed $columns
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());
$query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->secondKey);
if ($this->throughParentSoftDeletes()) {
$query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
}
$query->getModel()->setTable($hash);
return $query->select($columns)->whereColumn(
$parentQuery->getQuery()->from.'.'.$this->localKey, '=', $this->getQualifiedFirstKeyName()
);
}
/**
* Add the constraints for a relationship query on the same table as the through parent.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
* @param mixed $columns
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$table = $this->throughParent->getTable().' as '.$hash = $this->getRelationCountHash();
$query->join($table, $hash.'.'.$this->secondLocalKey, '=', $this->getQualifiedFarKeyName());
if ($this->throughParentSoftDeletes()) {
$query->whereNull($hash.'.'.$this->throughParent->getDeletedAtColumn());
}
return $query->select($columns)->whereColumn(
$parentQuery->getQuery()->from.'.'.$this->localKey, '=', $hash.'.'.$this->firstKey
);
}
/**
* Alias to set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function take($value)
{
return $this->limit($value);
}
/**
* Set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function limit($value)
{
if ($this->farParent->exists) {
$this->query->limit($value);
} else {
$column = $this->getQualifiedFirstKeyName();
$grammar = $this->query->getQuery()->getGrammar();
if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) {
$column = 'laravel_through_key';
}
$this->query->groupLimit($value, $column);
}
return $this;
}
/**
* Get the qualified foreign key on the related model.
*
* @return string
*/
public function getQualifiedFarKeyName()
{
return $this->getQualifiedForeignKeyName();
}
/**
* Get the foreign key on the "through" model.
*
* @return string
*/
public function getFirstKeyName()
{
return $this->firstKey;
}
/**
* Get the qualified foreign key on the "through" model.
*
* @return string
*/
public function getQualifiedFirstKeyName()
{
return $this->throughParent->qualifyColumn($this->firstKey);
}
/**
* Get the foreign key on the related model.
*
* @return string
*/
public function getForeignKeyName()
{
return $this->secondKey;
}
/**
* Get the qualified foreign key on the related model.
*
* @return string
*/
public function getQualifiedForeignKeyName()
{
return $this->related->qualifyColumn($this->secondKey);
}
/**
* Get the local key on the far parent model.
*
* @return string
*/
public function getLocalKeyName()
{
return $this->localKey;
}
/**
* Get the qualified local key on the far parent model.
*
* @return string
*/
public function getQualifiedLocalKeyName()
{
return $this->farParent->qualifyColumn($this->localKey);
}
/**
* Get the local key on the intermediary model.
*
* @return string
*/
public function getSecondLocalKeyName()
{
return $this->secondLocalKey;
}
}
@@ -0,0 +1,124 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
use Illuminate\Database\Query\JoinClause;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel, ?TRelatedModel>
*/
class HasOneThrough extends HasOneOrManyThrough implements SupportsPartialRelations
{
use ComparesRelatedModels, CanBeOneOfMany, InteractsWithDictionary, SupportsDefaultModels;
/** @inheritDoc */
public function getResults()
{
if (is_null($this->getParentKey())) {
return $this->getDefaultFor($this->farParent);
}
return $this->first() ?: $this->getDefaultFor($this->farParent);
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->getDefaultFor($model));
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model) {
$key = $this->getDictionaryKey($model->getAttribute($this->localKey));
if ($key !== null && isset($dictionary[$key])) {
$value = $dictionary[$key];
$model->setRelation(
$relation, reset($value)
);
}
}
return $models;
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($this->isOneOfMany()) {
$this->mergeOneOfManyJoinsTo($query);
}
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}
/** @inheritDoc */
public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null)
{
$query->addSelect([$this->getQualifiedFirstKeyName()]);
// We need to join subqueries that aren't the inner-most subquery which is joined in the CanBeOneOfMany::ofMany method...
if ($this->getOneOfManySubQuery() !== null) {
$this->performJoin($query);
}
}
/** @inheritDoc */
public function getOneOfManySubQuerySelectColumns()
{
return [$this->getQualifiedFirstKeyName()];
}
/** @inheritDoc */
public function addOneOfManyJoinSubQueryConstraints(JoinClause $join)
{
$join->on($this->qualifySubSelectColumn($this->firstKey), '=', $this->getQualifiedFirstKeyName());
}
/**
* Make a new related instance for the given model.
*
* @param TDeclaringModel $parent
* @return TRelatedModel
*/
public function newRelatedInstanceFor(Model $parent)
{
return $this->related->newInstance();
}
/** @inheritDoc */
protected function getRelatedKeyFrom(Model $model)
{
return $model->getAttribute($this->getForeignKeyName());
}
/** @inheritDoc */
public function getParentKey()
{
return $this->farParent->getAttribute($this->localKey);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\MorphOneOrMany<TRelatedModel, TDeclaringModel, \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>>
*/
class MorphMany extends MorphOneOrMany
{
/**
* Convert the relationship to a "morph one" relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, TDeclaringModel>
*/
public function one()
{
return MorphOne::noConstraints(fn () => tap(
new MorphOne(
$this->getQuery(),
$this->getParent(),
$this->morphType,
$this->foreignKey,
$this->localKey
),
function ($morphOne) {
if ($inverse = $this->getInverseRelationship()) {
$morphOne->inverse($inverse);
}
}
));
}
/** @inheritDoc */
public function getResults()
{
return ! is_null($this->getParentKey())
? $this->query->get()
: $this->related->newCollection();
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->related->newCollection());
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
return $this->matchMany($models, $results, $relation);
}
/** @inheritDoc */
public function forceCreate(array $attributes = [])
{
$attributes[$this->getMorphType()] = $this->morphClass;
return parent::forceCreate($attributes);
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
use Illuminate\Database\Query\JoinClause;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\MorphOneOrMany<TRelatedModel, TDeclaringModel, ?TRelatedModel>
*/
class MorphOne extends MorphOneOrMany implements SupportsPartialRelations
{
use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels;
/** @inheritDoc */
public function getResults()
{
if (is_null($this->getParentKey())) {
return $this->getDefaultFor($this->parent);
}
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->getDefaultFor($model));
}
return $models;
}
/** @inheritDoc */
public function match(array $models, EloquentCollection $results, $relation)
{
return $this->matchOne($models, $results, $relation);
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($this->isOneOfMany()) {
$this->mergeOneOfManyJoinsTo($query);
}
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}
/**
* Add constraints for inner join subselect for one of many relationships.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param string|null $column
* @param string|null $aggregate
* @return void
*/
public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null)
{
$query->addSelect($this->foreignKey, $this->morphType);
}
/**
* Get the columns that should be selected by the one of many subquery.
*
* @return array|string
*/
public function getOneOfManySubQuerySelectColumns()
{
return [$this->foreignKey, $this->morphType];
}
/**
* Add join query constraints for one of many relationships.
*
* @param \Illuminate\Database\Query\JoinClause $join
* @return void
*/
public function addOneOfManyJoinSubQueryConstraints(JoinClause $join)
{
$join
->on($this->qualifySubSelectColumn($this->morphType), '=', $this->qualifyRelatedColumn($this->morphType))
->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey));
}
/**
* Make a new related instance for the given model.
*
* @param TDeclaringModel $parent
* @return TRelatedModel
*/
public function newRelatedInstanceFor(Model $parent)
{
return tap($this->related->newInstance(), function ($instance) use ($parent) {
$instance->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey})
->setAttribute($this->getMorphType(), $this->morphClass);
$this->applyInverseRelationToModel($instance, $parent);
});
}
/**
* Get the value of the model's foreign key.
*
* @param TRelatedModel $model
* @return int|string
*/
protected function getRelatedKeyFrom(Model $model)
{
return $model->getAttribute($this->getForeignKeyName());
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
* @template TResult
*
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TRelatedModel, TDeclaringModel, TResult>
*/
abstract class MorphOneOrMany extends HasOneOrMany
{
/**
* The foreign key type for the relationship.
*
* @var string
*/
protected $morphType;
/**
* The morph class of the parent model.
*
* @var class-string<TDeclaringModel>|string
*/
protected $morphClass;
/**
* Create a new morph one or many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $type
* @param string $id
* @param string $localKey
*/
public function __construct(Builder $query, Model $parent, $type, $id, $localKey)
{
$this->morphType = $type;
$this->morphClass = $parent->getMorphClass();
parent::__construct($query, $parent, $id, $localKey);
}
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints()
{
if (static::$constraints) {
$this->getRelationQuery()->where($this->morphType, $this->morphClass);
parent::addConstraints();
}
}
/** @inheritDoc */
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
$this->getRelationQuery()->where($this->morphType, $this->morphClass);
}
/**
* Create a new instance of the related model. Allow mass-assignment.
*
* @param array $attributes
* @return TRelatedModel
*/
public function forceCreate(array $attributes = [])
{
$attributes[$this->getForeignKeyName()] = $this->getParentKey();
$attributes[$this->getMorphType()] = $this->morphClass;
return $this->applyInverseRelationToModel($this->related->forceCreate($attributes));
}
/**
* Set the foreign ID and type for creating a related model.
*
* @param TRelatedModel $model
* @return void
*/
protected function setForeignAttributesForCreate(Model $model)
{
$model->{$this->getForeignKeyName()} = $this->getParentKey();
$model->{$this->getMorphType()} = $this->morphClass;
foreach ($this->getQuery()->pendingAttributes as $key => $value) {
$attributes ??= $model->getAttributes();
if (! array_key_exists($key, $attributes)) {
$model->setAttribute($key, $value);
}
}
$this->applyInverseRelationToModel($model);
}
/**
* Insert new records or update the existing ones.
*
* @param array $values
* @param array|string $uniqueBy
* @param array|null $update
* @return int
*/
public function upsert(array $values, $uniqueBy, $update = null)
{
if (! empty($values) && ! is_array(array_first($values))) {
$values = [$values];
}
foreach ($values as $key => $value) {
$values[$key][$this->getMorphType()] = $this->getMorphClass();
}
return parent::upsert($values, $uniqueBy, $update);
}
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where(
$query->qualifyColumn($this->getMorphType()), $this->morphClass
);
}
/**
* Get the foreign key "type" name.
*
* @return string
*/
public function getQualifiedMorphType()
{
return $this->morphType;
}
/**
* Get the plain morph type name without the table.
*
* @return string
*/
public function getMorphType()
{
return last(explode('.', $this->morphType));
}
/**
* Get the morph class of the parent model.
*
* @return class-string<TDeclaringModel>|string
*/
public function getMorphClass()
{
return $this->morphClass;
}
/**
* Get the possible inverse relations for the parent model.
*
* @return array<non-empty-string>
*/
protected function getPossibleInverseRelations(): array
{
return array_unique([
Str::beforeLast($this->getMorphType(), '_type'),
...parent::getPossibleInverseRelations(),
]);
}
}
@@ -0,0 +1,184 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
class MorphPivot extends Pivot
{
/**
* The type of the polymorphic relation.
*
* Explicitly define this so it's not included in saved attributes.
*
* @var string
*/
protected $morphType;
/**
* The value of the polymorphic relation.
*
* Explicitly define this so it's not included in saved attributes.
*
* @var class-string|string
*/
protected $morphClass;
/**
* Set the keys for a save update query.
*
* @param \Illuminate\Database\Eloquent\Builder<static> $query
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function setKeysForSaveQuery($query)
{
$query->where($this->morphType, $this->morphClass);
return parent::setKeysForSaveQuery($query);
}
/**
* Set the keys for a select query.
*
* @param \Illuminate\Database\Eloquent\Builder<static> $query
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function setKeysForSelectQuery($query)
{
$query->where($this->morphType, $this->morphClass);
return parent::setKeysForSelectQuery($query);
}
/**
* Delete the pivot model record from the database.
*
* @return int
*/
public function delete()
{
if (isset($this->attributes[$this->getKeyName()])) {
return (int) parent::delete();
}
if ($this->fireModelEvent('deleting') === false) {
return 0;
}
$query = $this->getDeleteQuery();
$query->where($this->morphType, $this->morphClass);
return tap($query->delete(), function () {
$this->exists = false;
$this->fireModelEvent('deleted', false);
});
}
/**
* Get the morph type for the pivot.
*
* @return string
*/
public function getMorphType()
{
return $this->morphType;
}
/**
* Set the morph type for the pivot.
*
* @param string $morphType
* @return $this
*/
public function setMorphType($morphType)
{
$this->morphType = $morphType;
return $this;
}
/**
* Set the morph class for the pivot.
*
* @param class-string|string $morphClass
* @return \Illuminate\Database\Eloquent\Relations\MorphPivot
*/
public function setMorphClass($morphClass)
{
$this->morphClass = $morphClass;
return $this;
}
/**
* Get the queueable identity for the entity.
*
* @return mixed
*/
public function getQueueableId()
{
if (isset($this->attributes[$this->getKeyName()])) {
return $this->getKey();
}
return sprintf(
'%s:%s:%s:%s:%s:%s',
$this->foreignKey, $this->getAttribute($this->foreignKey),
$this->relatedKey, $this->getAttribute($this->relatedKey),
$this->morphType, $this->morphClass
);
}
/**
* Get a new query to restore one or more models by their queueable IDs.
*
* @param array|int $ids
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
public function newQueryForRestoration($ids)
{
if (is_array($ids)) {
return $this->newQueryForCollectionRestoration($ids);
}
if (! str_contains($ids, ':')) {
return parent::newQueryForRestoration($ids);
}
$segments = explode(':', $ids);
return $this->newQueryWithoutScopes()
->where($segments[0], $segments[1])
->where($segments[2], $segments[3])
->where($segments[4], $segments[5]);
}
/**
* Get a new query to restore multiple models by their queueable IDs.
*
* @param array $ids
* @return \Illuminate\Database\Eloquent\Builder<static>
*/
protected function newQueryForCollectionRestoration(array $ids)
{
$ids = array_values($ids);
if (! str_contains($ids[0], ':')) {
return parent::newQueryForRestoration($ids);
}
$query = $this->newQueryWithoutScopes();
foreach ($ids as $id) {
$segments = explode(':', $id);
$query->orWhere(function ($query) use ($segments) {
return $query->where($segments[0], $segments[1])
->where($segments[2], $segments[3])
->where($segments[4], $segments[5]);
});
}
return $query;
}
}

Some files were not shown because too many files have changed in this diff Show More