init
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public string $collectionClass)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public array|string $classes)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public array|string $classes)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public string $factoryClass)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+2180
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Casts;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Support\Collection;
|
||||
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>
|
||||
*/
|
||||
public static function castUsing(array $arguments)
|
||||
{
|
||||
return new class($arguments) implements CastsAttributes
|
||||
{
|
||||
public function __construct(protected array $arguments)
|
||||
{
|
||||
}
|
||||
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
if (! isset($attributes[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = Json::decode($attributes[$key]);
|
||||
|
||||
$collectionClass = $this->arguments[0] ?? Collection::class;
|
||||
|
||||
if (! is_a($collectionClass, Collection::class, true)) {
|
||||
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
|
||||
}
|
||||
|
||||
return is_array($data) ? new $collectionClass($data) : null;
|
||||
}
|
||||
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
return [$key => Json::encode($value)];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the collection for the cast.
|
||||
*
|
||||
* @param class-string $class
|
||||
* @return string
|
||||
*/
|
||||
public static function using($class)
|
||||
{
|
||||
return static::class.':'.$class;
|
||||
}
|
||||
}
|
||||
@@ -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])));
|
||||
}
|
||||
|
||||
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,63 @@
|
||||
<?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 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>
|
||||
*/
|
||||
public static function castUsing(array $arguments)
|
||||
{
|
||||
return new class($arguments) implements CastsAttributes
|
||||
{
|
||||
public function __construct(protected array $arguments)
|
||||
{
|
||||
}
|
||||
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
$collectionClass = $this->arguments[0] ?? Collection::class;
|
||||
|
||||
if (! is_a($collectionClass, Collection::class, true)) {
|
||||
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
|
||||
}
|
||||
|
||||
if (isset($attributes[$key])) {
|
||||
return new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key])));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
if (! is_null($value)) {
|
||||
return [$key => Crypt::encryptString(Json::encode($value))];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the collection for the cast.
|
||||
*
|
||||
* @param class-string $class
|
||||
* @return string
|
||||
*/
|
||||
public static function using($class)
|
||||
{
|
||||
return static::class.':'.$class;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
* @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(function ($enum) {
|
||||
return $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|\BackedEnum
|
||||
*
|
||||
* @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(function ($enum) {
|
||||
return $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,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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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): mixed
|
||||
{
|
||||
return isset(static::$encoder) ? (static::$encoder)($value) : json_encode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
+820
@@ -0,0 +1,820 @@
|
||||
<?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();
|
||||
}
|
||||
|
||||
foreach ($relations as $key => $value) {
|
||||
if (is_numeric($key)) {
|
||||
$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][end($segments)] = $value;
|
||||
}
|
||||
|
||||
$this->loadMissingRelation($this, $path);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$dictionary[$this->getDictionaryKey($item->getKey())] = $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) {
|
||||
if (! isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
|
||||
$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) {
|
||||
if (isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visible attributes across the entire collection.
|
||||
*
|
||||
* @param array<int, string> $visible
|
||||
* @return $this
|
||||
*/
|
||||
public function setVisible($visible)
|
||||
{
|
||||
return $this->each->setVisible($visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hidden attributes across the entire collection.
|
||||
*
|
||||
* @param array<int, string> $hidden
|
||||
* @return $this
|
||||
*/
|
||||
public function setHidden($hidden)
|
||||
{
|
||||
return $this->each->setHidden($hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$dictionary[$this->getDictionaryKey($value->getKey())] = $value;
|
||||
}
|
||||
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following methods are intercepted to always return base collections.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Count the number of items in the collection by a field or using a callback.
|
||||
*
|
||||
* @param (callable(TModel, TKey): array-key)|string|null $countBy
|
||||
* @return \Illuminate\Support\Collection<array-key, int>
|
||||
*/
|
||||
public function countBy($countBy = null)
|
||||
{
|
||||
return $this->toBase()->countBy($countBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse the collection of items into a single array.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, mixed>
|
||||
*/
|
||||
public function collapse()
|
||||
{
|
||||
return $this->toBase()->collapse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a flattened array of the items in the collection.
|
||||
*
|
||||
* @param int $depth
|
||||
* @return \Illuminate\Support\Collection<int, mixed>
|
||||
*/
|
||||
public function flatten($depth = INF)
|
||||
{
|
||||
return $this->toBase()->flatten($depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip the items in the collection.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<TModel, TKey>
|
||||
*/
|
||||
public function flip()
|
||||
{
|
||||
return $this->toBase()->flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keys of the collection items.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, TKey>
|
||||
*/
|
||||
public function keys()
|
||||
{
|
||||
return $this->toBase()->keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad collection to the specified length with a value.
|
||||
*
|
||||
* @template TPadValue
|
||||
*
|
||||
* @param int $size
|
||||
* @param TPadValue $value
|
||||
* @return \Illuminate\Support\Collection<int, TModel|TPadValue>
|
||||
*/
|
||||
public function pad($size, $value)
|
||||
{
|
||||
return $this->toBase()->pad($size, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with the values of a given key.
|
||||
*
|
||||
* @param string|array<array-key, string>|null $value
|
||||
* @param string|null $key
|
||||
* @return \Illuminate\Support\Collection<array-key, mixed>
|
||||
*/
|
||||
public function pluck($value, $key = null)
|
||||
{
|
||||
return $this->toBase()->pluck($value, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the collection together with one or more arrays.
|
||||
*
|
||||
* @template TZipValue
|
||||
*
|
||||
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue> ...$items
|
||||
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, TModel|TZipValue>>
|
||||
*/
|
||||
public function zip($items)
|
||||
{
|
||||
return $this->toBase()->zip(...func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comparison function to detect duplicates.
|
||||
*
|
||||
* @param bool $strict
|
||||
* @return callable(TModel, TModel): bool
|
||||
*/
|
||||
protected function duplicateComparator($strict)
|
||||
{
|
||||
return fn ($a, $b) => $a->is($b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->filter(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,260 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Concerns;
|
||||
|
||||
trait GuardsAttributes
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [];
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array<string>|bool
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
protected static $guardableColumns = [];
|
||||
|
||||
/**
|
||||
* 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 $this->guarded === false
|
||||
? []
|
||||
: $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.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return mixed
|
||||
*/
|
||||
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)) {
|
||||
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 $attributes
|
||||
* @return array
|
||||
*/
|
||||
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
|
||||
*/
|
||||
protected $dispatchesEvents = [];
|
||||
|
||||
/**
|
||||
* User exposed observable events.
|
||||
*
|
||||
* These are extra user-defined events observers may subscribe to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $observables = [];
|
||||
|
||||
/**
|
||||
* Boot the has event trait for a model.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function bootHasEvents()
|
||||
{
|
||||
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|array|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 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 array
|
||||
*/
|
||||
public function getObservableEvents()
|
||||
{
|
||||
return array_merge(
|
||||
[
|
||||
'retrieved', 'creating', 'created', 'updating', 'updated',
|
||||
'saving', 'saved', 'restoring', 'restored', 'replicating',
|
||||
'deleting', 'deleted', 'forceDeleting', 'forceDeleted',
|
||||
],
|
||||
$this->observables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the observable event names.
|
||||
*
|
||||
* @param array $observables
|
||||
* @return $this
|
||||
*/
|
||||
public function setObservableEvents(array $observables)
|
||||
{
|
||||
$this->observables = $observables;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an observable event name.
|
||||
*
|
||||
* @param array|mixed $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 array|mixed $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 string $method
|
||||
* @return mixed|null
|
||||
*/
|
||||
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,139 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use InvalidArgumentException;
|
||||
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);
|
||||
|
||||
return (new Collection($reflectionClass->getAttributes(ScopedBy::class)))
|
||||
->map(fn ($attribute) => $attribute->getArguments())
|
||||
->flatten()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new global scope on the model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope
|
||||
* @param \Illuminate\Database\Eloquent\Scope|\Closure|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|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, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,997 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\ClassMorphViolationException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\PendingHasThroughRelationship;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait HasRelationships
|
||||
{
|
||||
/**
|
||||
* The loaded relationships for the model.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $relations = [];
|
||||
|
||||
/**
|
||||
* The relationships that should be touched on save.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $touches = [];
|
||||
|
||||
/**
|
||||
* The many to many relationship methods.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public static $manyMethods = [
|
||||
'belongsToMany', 'morphToMany', 'morphedByMany',
|
||||
];
|
||||
|
||||
/**
|
||||
* The relation resolver callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $relationResolvers = [];
|
||||
|
||||
/**
|
||||
* Get the dynamic relation resolver if defined or inherited, or return null.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $class
|
||||
* @param string $key
|
||||
* @return Closure|null
|
||||
*/
|
||||
public function relationResolver($class, $key)
|
||||
{
|
||||
if ($resolver = static::$relationResolvers[$class][$key] ?? null) {
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
if ($parent = get_parent_class($class)) {
|
||||
return $this->relationResolver($parent, $key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a dynamic relation resolver.
|
||||
*
|
||||
* @param string $name
|
||||
* @param \Closure $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function resolveRelationUsing($name, Closure $callback)
|
||||
{
|
||||
static::$relationResolvers = array_replace_recursive(
|
||||
static::$relationResolvers,
|
||||
[static::class => [$name => $callback]]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a one-to-one relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string|null $foreignKey
|
||||
* @param string|null $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, $this>
|
||||
*/
|
||||
public function hasOne($related, $foreignKey = null, $localKey = null)
|
||||
{
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
$foreignKey = $foreignKey ?: $this->getForeignKey();
|
||||
|
||||
$localKey = $localKey ?: $this->getKeyName();
|
||||
|
||||
return $this->newHasOne($instance->newQuery(), $this, $instance->qualifyColumn($foreignKey), $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new HasOne relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $foreignKey
|
||||
* @param string $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey)
|
||||
{
|
||||
return new HasOne($query, $parent, $foreignKey, $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a has-one-through relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param class-string<TIntermediateModel> $through
|
||||
* @param string|null $firstKey
|
||||
* @param string|null $secondKey
|
||||
* @param string|null $localKey
|
||||
* @param string|null $secondLocalKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, $this>
|
||||
*/
|
||||
public function hasOneThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
|
||||
{
|
||||
$through = $this->newRelatedThroughInstance($through);
|
||||
|
||||
$firstKey = $firstKey ?: $this->getForeignKey();
|
||||
|
||||
$secondKey = $secondKey ?: $through->getForeignKey();
|
||||
|
||||
return $this->newHasOneThrough(
|
||||
$this->newRelatedInstance($related)->newQuery(), $this, $through,
|
||||
$firstKey, $secondKey, $localKey ?: $this->getKeyName(),
|
||||
$secondLocalKey ?: $through->getKeyName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new HasOneThrough relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @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
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newHasOneThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
|
||||
{
|
||||
return new HasOneThrough($query, $farParent, $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic one-to-one relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string $name
|
||||
* @param string|null $type
|
||||
* @param string|null $id
|
||||
* @param string|null $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, $this>
|
||||
*/
|
||||
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
|
||||
{
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
[$type, $id] = $this->getMorphs($name, $type, $id);
|
||||
|
||||
$localKey = $localKey ?: $this->getKeyName();
|
||||
|
||||
return $this->newMorphOne($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new MorphOne relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @param string $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey)
|
||||
{
|
||||
return new MorphOne($query, $parent, $type, $id, $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define an inverse one-to-one or many relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string|null $foreignKey
|
||||
* @param string|null $ownerKey
|
||||
* @param string|null $relation
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, $this>
|
||||
*/
|
||||
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
|
||||
{
|
||||
// If no relation name was given, we will use this debug backtrace to extract
|
||||
// the calling method's name and use that as the relationship name as most
|
||||
// of the time this will be what we desire to use for the relationships.
|
||||
if (is_null($relation)) {
|
||||
$relation = $this->guessBelongsToRelation();
|
||||
}
|
||||
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
// If no foreign key was supplied, we can use a backtrace to guess the proper
|
||||
// foreign key name by using the name of the relationship function, which
|
||||
// when combined with an "_id" should conventionally match the columns.
|
||||
if (is_null($foreignKey)) {
|
||||
$foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
|
||||
}
|
||||
|
||||
// Once we have the foreign key names we'll just create a new Eloquent query
|
||||
// for the related models and return the relationship instance which will
|
||||
// actually be responsible for retrieving and hydrating every relation.
|
||||
$ownerKey = $ownerKey ?: $instance->getKeyName();
|
||||
|
||||
return $this->newBelongsTo(
|
||||
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new BelongsTo relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $child
|
||||
* @param string $foreignKey
|
||||
* @param string $ownerKey
|
||||
* @param string $relation
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
|
||||
{
|
||||
return new BelongsTo($query, $child, $foreignKey, $ownerKey, $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic, inverse one-to-one or many relationship.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param string|null $type
|
||||
* @param string|null $id
|
||||
* @param string|null $ownerKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
|
||||
*/
|
||||
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
|
||||
{
|
||||
// If no name is provided, we will use the backtrace to get the function name
|
||||
// since that is most likely the name of the polymorphic interface. We can
|
||||
// use that to get both the class and foreign key that will be utilized.
|
||||
$name = $name ?: $this->guessBelongsToRelation();
|
||||
|
||||
[$type, $id] = $this->getMorphs(
|
||||
Str::snake($name), $type, $id
|
||||
);
|
||||
|
||||
// If the type value is null it is probably safe to assume we're eager loading
|
||||
// the relationship. In this case we'll just pass in a dummy query where we
|
||||
// need to remove any eager loads that may already be defined on a model.
|
||||
return is_null($class = $this->getAttributeFromArray($type)) || $class === ''
|
||||
? $this->morphEagerTo($name, $type, $id, $ownerKey)
|
||||
: $this->morphInstanceTo($class, $name, $type, $id, $ownerKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic, inverse one-to-one or many relationship.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @param string|null $ownerKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
|
||||
*/
|
||||
protected function morphEagerTo($name, $type, $id, $ownerKey)
|
||||
{
|
||||
return $this->newMorphTo(
|
||||
$this->newQuery()->setEagerLoads([]), $this, $id, $ownerKey, $type, $name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic, inverse one-to-one or many relationship.
|
||||
*
|
||||
* @param string $target
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @param string|null $ownerKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
|
||||
*/
|
||||
protected function morphInstanceTo($target, $name, $type, $id, $ownerKey)
|
||||
{
|
||||
$instance = $this->newRelatedInstance(
|
||||
static::getActualClassNameForMorph($target)
|
||||
);
|
||||
|
||||
return $this->newMorphTo(
|
||||
$instance->newQuery(), $this, $id, $ownerKey ?? $instance->getKeyName(), $type, $name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new MorphTo relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $foreignKey
|
||||
* @param string|null $ownerKey
|
||||
* @param string $type
|
||||
* @param string $relation
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newMorphTo(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation)
|
||||
{
|
||||
return new MorphTo($query, $parent, $foreignKey, $ownerKey, $type, $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the actual class name for a given morph class.
|
||||
*
|
||||
* @param string $class
|
||||
* @return string
|
||||
*/
|
||||
public static function getActualClassNameForMorph($class)
|
||||
{
|
||||
return Arr::get(Relation::morphMap() ?: [], $class, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the "belongs to" relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function guessBelongsToRelation()
|
||||
{
|
||||
[$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
|
||||
|
||||
return $caller['function'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pending has-many-through or has-one-through relationship.
|
||||
*
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param string|\Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, covariant $this>|\Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, covariant $this> $relationship
|
||||
* @return (
|
||||
* $relationship is string
|
||||
* ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship<\Illuminate\Database\Eloquent\Model, $this>
|
||||
* : (
|
||||
* $relationship is \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, $this>
|
||||
* ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship<TIntermediateModel, $this, \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, $this>>
|
||||
* : \Illuminate\Database\Eloquent\PendingHasThroughRelationship<TIntermediateModel, $this, \Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, $this>>
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function through($relationship)
|
||||
{
|
||||
if (is_string($relationship)) {
|
||||
$relationship = $this->{$relationship}();
|
||||
}
|
||||
|
||||
return new PendingHasThroughRelationship($this, $relationship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a one-to-many relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string|null $foreignKey
|
||||
* @param string|null $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, $this>
|
||||
*/
|
||||
public function hasMany($related, $foreignKey = null, $localKey = null)
|
||||
{
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
$foreignKey = $foreignKey ?: $this->getForeignKey();
|
||||
|
||||
$localKey = $localKey ?: $this->getKeyName();
|
||||
|
||||
return $this->newHasMany(
|
||||
$instance->newQuery(), $this, $instance->qualifyColumn($foreignKey), $localKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new HasMany relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $foreignKey
|
||||
* @param string $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey)
|
||||
{
|
||||
return new HasMany($query, $parent, $foreignKey, $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a has-many-through relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param class-string<TIntermediateModel> $through
|
||||
* @param string|null $firstKey
|
||||
* @param string|null $secondKey
|
||||
* @param string|null $localKey
|
||||
* @param string|null $secondLocalKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, $this>
|
||||
*/
|
||||
public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
|
||||
{
|
||||
$through = $this->newRelatedThroughInstance($through);
|
||||
|
||||
$firstKey = $firstKey ?: $this->getForeignKey();
|
||||
|
||||
$secondKey = $secondKey ?: $through->getForeignKey();
|
||||
|
||||
return $this->newHasManyThrough(
|
||||
$this->newRelatedInstance($related)->newQuery(),
|
||||
$this,
|
||||
$through,
|
||||
$firstKey,
|
||||
$secondKey,
|
||||
$localKey ?: $this->getKeyName(),
|
||||
$secondLocalKey ?: $through->getKeyName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new HasManyThrough relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @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
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newHasManyThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
|
||||
{
|
||||
return new HasManyThrough($query, $farParent, $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic one-to-many relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string $name
|
||||
* @param string|null $type
|
||||
* @param string|null $id
|
||||
* @param string|null $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<TRelatedModel, $this>
|
||||
*/
|
||||
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
|
||||
{
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
// Here we will gather up the morph type and ID for the relationship so that we
|
||||
// can properly query the intermediate table of a relation. Finally, we will
|
||||
// get the table and create the relationship instances for the developers.
|
||||
[$type, $id] = $this->getMorphs($name, $type, $id);
|
||||
|
||||
$localKey = $localKey ?: $this->getKeyName();
|
||||
|
||||
return $this->newMorphMany($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new MorphMany relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @param string $localKey
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey)
|
||||
{
|
||||
return new MorphMany($query, $parent, $type, $id, $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a many-to-many relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string|class-string<\Illuminate\Database\Eloquent\Model>|null $table
|
||||
* @param string|null $foreignPivotKey
|
||||
* @param string|null $relatedPivotKey
|
||||
* @param string|null $parentKey
|
||||
* @param string|null $relatedKey
|
||||
* @param string|null $relation
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, $this>
|
||||
*/
|
||||
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
|
||||
$parentKey = null, $relatedKey = null, $relation = null)
|
||||
{
|
||||
// If no relationship name was passed, we will pull backtraces to get the
|
||||
// name of the calling function. We will use that function name as the
|
||||
// title of this relation since that is a great convention to apply.
|
||||
if (is_null($relation)) {
|
||||
$relation = $this->guessBelongsToManyRelation();
|
||||
}
|
||||
|
||||
// First, we'll need to determine the foreign key and "other key" for the
|
||||
// relationship. Once we have determined the keys we'll make the query
|
||||
// instances as well as the relationship instances we need for this.
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
|
||||
|
||||
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
|
||||
|
||||
// If no table name was provided, we can guess it by concatenating the two
|
||||
// models using underscores in alphabetical order. The two model names
|
||||
// are transformed to snake case from their default CamelCase also.
|
||||
if (is_null($table)) {
|
||||
$table = $this->joiningTable($related, $instance);
|
||||
}
|
||||
|
||||
return $this->newBelongsToMany(
|
||||
$instance->newQuery(), $this, $table, $foreignPivotKey,
|
||||
$relatedPivotKey, $parentKey ?: $this->getKeyName(),
|
||||
$relatedKey ?: $instance->getKeyName(), $relation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new BelongsToMany relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string|class-string<\Illuminate\Database\Eloquent\Model> $table
|
||||
* @param string $foreignPivotKey
|
||||
* @param string $relatedPivotKey
|
||||
* @param string $parentKey
|
||||
* @param string $relatedKey
|
||||
* @param string|null $relationName
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey,
|
||||
$parentKey, $relatedKey, $relationName = null)
|
||||
{
|
||||
return new BelongsToMany($query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic many-to-many relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string $name
|
||||
* @param string|null $table
|
||||
* @param string|null $foreignPivotKey
|
||||
* @param string|null $relatedPivotKey
|
||||
* @param string|null $parentKey
|
||||
* @param string|null $relatedKey
|
||||
* @param string|null $relation
|
||||
* @param bool $inverse
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<TRelatedModel, $this>
|
||||
*/
|
||||
public function morphToMany($related, $name, $table = null, $foreignPivotKey = null,
|
||||
$relatedPivotKey = null, $parentKey = null,
|
||||
$relatedKey = null, $relation = null, $inverse = false)
|
||||
{
|
||||
$relation = $relation ?: $this->guessBelongsToManyRelation();
|
||||
|
||||
// First, we will need to determine the foreign key and "other key" for the
|
||||
// relationship. Once we have determined the keys we will make the query
|
||||
// instances, as well as the relationship instances we need for these.
|
||||
$instance = $this->newRelatedInstance($related);
|
||||
|
||||
$foreignPivotKey = $foreignPivotKey ?: $name.'_id';
|
||||
|
||||
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
|
||||
|
||||
// Now we're ready to create a new query builder for the related model and
|
||||
// the relationship instances for this relation. This relation will set
|
||||
// appropriate query constraints then entirely manage the hydrations.
|
||||
if (! $table) {
|
||||
$words = preg_split('/(_)/u', $name, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
$lastWord = array_pop($words);
|
||||
|
||||
$table = implode('', $words).Str::plural($lastWord);
|
||||
}
|
||||
|
||||
return $this->newMorphToMany(
|
||||
$instance->newQuery(), $this, $name, $table,
|
||||
$foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(),
|
||||
$relatedKey ?: $instance->getKeyName(), $relation, $inverse
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new MorphToMany relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $name
|
||||
* @param string $table
|
||||
* @param string $foreignPivotKey
|
||||
* @param string $relatedPivotKey
|
||||
* @param string $parentKey
|
||||
* @param string $relatedKey
|
||||
* @param string|null $relationName
|
||||
* @param bool $inverse
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function newMorphToMany(Builder $query, Model $parent, $name, $table, $foreignPivotKey,
|
||||
$relatedPivotKey, $parentKey, $relatedKey,
|
||||
$relationName = null, $inverse = false)
|
||||
{
|
||||
return new MorphToMany($query, $parent, $name, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey,
|
||||
$relationName, $inverse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a polymorphic, inverse many-to-many relationship.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $related
|
||||
* @param string $name
|
||||
* @param string|null $table
|
||||
* @param string|null $foreignPivotKey
|
||||
* @param string|null $relatedPivotKey
|
||||
* @param string|null $parentKey
|
||||
* @param string|null $relatedKey
|
||||
* @param string|null $relation
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<TRelatedModel, $this>
|
||||
*/
|
||||
public function morphedByMany($related, $name, $table = null, $foreignPivotKey = null,
|
||||
$relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null)
|
||||
{
|
||||
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
|
||||
|
||||
// For the inverse of the polymorphic many-to-many relations, we will change
|
||||
// the way we determine the foreign and other keys, as it is the opposite
|
||||
// of the morph-to-many method since we're figuring out these inverses.
|
||||
$relatedPivotKey = $relatedPivotKey ?: $name.'_id';
|
||||
|
||||
return $this->morphToMany(
|
||||
$related, $name, $table, $foreignPivotKey,
|
||||
$relatedPivotKey, $parentKey, $relatedKey, $relation, true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name of the belongsToMany relationship.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function guessBelongsToManyRelation()
|
||||
{
|
||||
$caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) {
|
||||
return ! in_array(
|
||||
$trace['function'],
|
||||
array_merge(static::$manyMethods, ['guessBelongsToManyRelation'])
|
||||
);
|
||||
});
|
||||
|
||||
return ! is_null($caller) ? $caller['function'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the joining table name for a many-to-many relation.
|
||||
*
|
||||
* @param string $related
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $instance
|
||||
* @return string
|
||||
*/
|
||||
public function joiningTable($related, $instance = null)
|
||||
{
|
||||
// The joining table name, by convention, is simply the snake cased models
|
||||
// sorted alphabetically and concatenated with an underscore, so we can
|
||||
// just sort the models and join them together to get the table name.
|
||||
$segments = [
|
||||
$instance ? $instance->joiningTableSegment()
|
||||
: Str::snake(class_basename($related)),
|
||||
$this->joiningTableSegment(),
|
||||
];
|
||||
|
||||
// Now that we have the model names in an array we can just sort them and
|
||||
// use the implode function to join them together with an underscores,
|
||||
// which is typically used by convention within the database system.
|
||||
sort($segments);
|
||||
|
||||
return strtolower(implode('_', $segments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this model's half of the intermediate table name for belongsToMany relationships.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function joiningTableSegment()
|
||||
{
|
||||
return Str::snake(class_basename($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model touches a given relation.
|
||||
*
|
||||
* @param string $relation
|
||||
* @return bool
|
||||
*/
|
||||
public function touches($relation)
|
||||
{
|
||||
return in_array($relation, $this->getTouchedRelations());
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch the owning relations of the model.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function touchOwners()
|
||||
{
|
||||
$this->withoutRecursion(function () {
|
||||
foreach ($this->getTouchedRelations() as $relation) {
|
||||
$this->$relation()->touch();
|
||||
|
||||
if ($this->$relation instanceof self) {
|
||||
$this->$relation->fireModelEvent('saved', false);
|
||||
|
||||
$this->$relation->touchOwners();
|
||||
} elseif ($this->$relation instanceof EloquentCollection) {
|
||||
$this->$relation->each->touchOwners();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the polymorphic relationship columns.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
protected function getMorphs($name, $type, $id)
|
||||
{
|
||||
return [$type ?: $name.'_type', $id ?: $name.'_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name for polymorphic relations.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
$morphMap = Relation::morphMap();
|
||||
|
||||
if (! empty($morphMap) && in_array(static::class, $morphMap)) {
|
||||
return array_search(static::class, $morphMap, true);
|
||||
}
|
||||
|
||||
if (static::class === Pivot::class) {
|
||||
return static::class;
|
||||
}
|
||||
|
||||
if (Relation::requiresMorphMap()) {
|
||||
throw new ClassMorphViolationException($this);
|
||||
}
|
||||
|
||||
return static::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new model instance for a related model.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $class
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
protected function newRelatedInstance($class)
|
||||
{
|
||||
return tap(new $class, function ($instance) {
|
||||
if (! $instance->getConnectionName()) {
|
||||
$instance->setConnection($this->connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new model instance for a related "through" model.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TRelatedModel> $class
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
protected function newRelatedThroughInstance($class)
|
||||
{
|
||||
return new $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the loaded relations for the instance.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRelations()
|
||||
{
|
||||
return $this->relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specified relationship.
|
||||
*
|
||||
* @param string $relation
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRelation($relation)
|
||||
{
|
||||
return $this->relations[$relation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given relation is loaded.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function relationLoaded($key)
|
||||
{
|
||||
return array_key_exists($key, $this->relations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given relationship on the model.
|
||||
*
|
||||
* @param string $relation
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setRelation($relation, $value)
|
||||
{
|
||||
$this->relations[$relation] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a loaded relationship.
|
||||
*
|
||||
* @param string $relation
|
||||
* @return $this
|
||||
*/
|
||||
public function unsetRelation($relation)
|
||||
{
|
||||
unset($this->relations[$relation]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entire relations array on the model.
|
||||
*
|
||||
* @param array $relations
|
||||
* @return $this
|
||||
*/
|
||||
public function setRelations(array $relations)
|
||||
{
|
||||
$this->relations = $relations;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate the instance and unset all the loaded relations.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutRelations()
|
||||
{
|
||||
$model = clone $this;
|
||||
|
||||
return $model->unsetRelations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset all the loaded relations for the instance.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function unsetRelations()
|
||||
{
|
||||
$this->relations = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationships that are touched on save.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTouchedRelations()
|
||||
{
|
||||
return $this->touches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the relationships that are touched on save.
|
||||
*
|
||||
* @param array $touches
|
||||
* @return $this
|
||||
*/
|
||||
public function setTouchedRelations(array $touches)
|
||||
{
|
||||
$this->touches = $touches;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Concerns;
|
||||
|
||||
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 = [];
|
||||
|
||||
/**
|
||||
* Update the model's update timestamp.
|
||||
*
|
||||
* @param string|null $attribute
|
||||
* @return bool
|
||||
*/
|
||||
public function touch($attribute = null)
|
||||
{
|
||||
if ($attribute) {
|
||||
$this->$attribute = $this->freshTimestamp();
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
if (! $this->usesTimestamps()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->updateTimestamps();
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the model's update timestamp without raising any events.
|
||||
*
|
||||
* @param 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()
|
||||
{
|
||||
return $this->qualifyColumn($this->getCreatedAtColumn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully qualified "updated at" column.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getQualifiedUpdatedAtColumn()
|
||||
{
|
||||
return $this->qualifyColumn($this->getUpdatedAtColumn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable timestamps for the current class during the given callback scope.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @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::orderedUuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 HasVersion7Uuids
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
/**
|
||||
* Generate a new UUID (version 7) for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function newUniqueId()
|
||||
{
|
||||
return (string) Str::uuid7();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Concerns;
|
||||
|
||||
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 = [];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,975 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Concerns;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\RelationNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Support\Collection as BaseCollection;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function Illuminate\Support\enum_value;
|
||||
|
||||
/** @mixin \Illuminate\Database\Eloquent\Builder */
|
||||
trait QueriesRelationships
|
||||
{
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @param string $boolean
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
|
||||
{
|
||||
if (is_string($relation)) {
|
||||
if (str_contains($relation, '.')) {
|
||||
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
|
||||
}
|
||||
|
||||
$relation = $this->getRelationWithoutConstraints($relation);
|
||||
}
|
||||
|
||||
if ($relation instanceof MorphTo) {
|
||||
return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback);
|
||||
}
|
||||
|
||||
// If we only need to check for the existence of the relation, then we can optimize
|
||||
// the subquery to only run a "where exists" clause instead of this full "count"
|
||||
// clause. This will make these queries run much faster compared with a count.
|
||||
$method = $this->canUseExistsForExistenceCheck($operator, $count)
|
||||
? 'getRelationExistenceQuery'
|
||||
: 'getRelationExistenceCountQuery';
|
||||
|
||||
$hasQuery = $relation->{$method}(
|
||||
$relation->getRelated()->newQueryWithoutRelationships(), $this
|
||||
);
|
||||
|
||||
// Next we will call any given callback as an "anonymous" scope so they can get the
|
||||
// proper logical grouping of the where clauses if needed by this Eloquent query
|
||||
// builder. Then, we will be ready to finalize and return this query instance.
|
||||
if ($callback) {
|
||||
$hasQuery->callScope($callback);
|
||||
}
|
||||
|
||||
return $this->addHasWhere(
|
||||
$hasQuery, $relation, $operator, $count, $boolean
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nested relationship count / exists conditions to the query.
|
||||
*
|
||||
* Sets up recursive call to whereHas until we finish the nested relation.
|
||||
*
|
||||
* @param string $relations
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @param string $boolean
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
|
||||
{
|
||||
$relations = explode('.', $relations);
|
||||
|
||||
$doesntHave = $operator === '<' && $count === 1;
|
||||
|
||||
if ($doesntHave) {
|
||||
$operator = '>=';
|
||||
$count = 1;
|
||||
}
|
||||
|
||||
$closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback) {
|
||||
// In order to nest "has", we need to add count relation constraints on the
|
||||
// callback Closure. We'll do this by simply passing the Closure its own
|
||||
// reference to itself so it calls itself recursively on each segment.
|
||||
count($relations) > 1
|
||||
? $q->whereHas(array_shift($relations), $closure)
|
||||
: $q->has(array_shift($relations), $operator, $count, 'and', $callback);
|
||||
};
|
||||
|
||||
return $this->has(array_shift($relations), $doesntHave ? '<' : '>=', 1, $boolean, $closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function orHas($relation, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->has($relation, $operator, $count, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param string $boolean
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null)
|
||||
{
|
||||
return $this->has($relation, '<', 1, $boolean, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @return $this
|
||||
*/
|
||||
public function orDoesntHave($relation)
|
||||
{
|
||||
return $this->doesntHave($relation, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with where clauses.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|null $callback
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->has($relation, $operator, $count, 'and', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with where clauses.
|
||||
*
|
||||
* Also load the relationship with same condition.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|null $callback
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function withWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count)
|
||||
->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with where clauses and an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|null $callback
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->has($relation, $operator, $count, 'or', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with where clauses.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function whereDoesntHave($relation, ?Closure $callback = null)
|
||||
{
|
||||
return $this->doesntHave($relation, 'and', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship count / exists condition to the query with where clauses and an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereDoesntHave($relation, ?Closure $callback = null)
|
||||
{
|
||||
return $this->doesntHave($relation, 'or', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @param string $boolean
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
|
||||
{
|
||||
if (is_string($relation)) {
|
||||
$relation = $this->getRelationWithoutConstraints($relation);
|
||||
}
|
||||
|
||||
$types = (array) $types;
|
||||
|
||||
if ($types === ['*']) {
|
||||
$types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())
|
||||
->filter()
|
||||
->map(fn ($item) => enum_value($item))
|
||||
->all();
|
||||
}
|
||||
|
||||
if (empty($types)) {
|
||||
return $this->where(new Expression('0'), $operator, $count, $boolean);
|
||||
}
|
||||
|
||||
foreach ($types as &$type) {
|
||||
$type = Relation::getMorphedModel($type) ?? $type;
|
||||
}
|
||||
|
||||
return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types) {
|
||||
foreach ($types as $type) {
|
||||
$query->orWhere(function ($query) use ($relation, $callback, $operator, $count, $type) {
|
||||
$belongsTo = $this->getBelongsToRelation($relation, $type);
|
||||
|
||||
if ($callback) {
|
||||
$callback = function ($query) use ($callback, $type) {
|
||||
return $callback($query, $type);
|
||||
};
|
||||
}
|
||||
|
||||
$query->where($this->qualifyColumn($relation->getMorphType()), '=', (new $type)->getMorphClass())
|
||||
->whereHas($belongsTo, $callback, $operator, $count);
|
||||
});
|
||||
}
|
||||
}, null, null, $boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BelongsTo relationship for a single polymorphic type.
|
||||
*
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, TDeclaringModel> $relation
|
||||
* @param class-string<TRelatedModel> $type
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
protected function getBelongsToRelation(MorphTo $relation, $type)
|
||||
{
|
||||
$belongsTo = Relation::noConstraints(function () use ($relation, $type) {
|
||||
return $this->model->belongsTo(
|
||||
$type,
|
||||
$relation->getForeignKeyName(),
|
||||
$relation->getOwnerKeyName()
|
||||
);
|
||||
});
|
||||
|
||||
$belongsTo->getQuery()->mergeConstraintsFrom($relation->getQuery());
|
||||
|
||||
return $belongsTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query with an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function orHasMorph($relation, $types, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->hasMorph($relation, $types, $operator, $count, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param string $boolean
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function doesntHaveMorph($relation, $types, $boolean = 'and', ?Closure $callback = null)
|
||||
{
|
||||
return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query with an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @return $this
|
||||
*/
|
||||
public function orDoesntHaveMorph($relation, $types)
|
||||
{
|
||||
return $this->doesntHaveMorph($relation, $types, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query with where clauses.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|null $callback
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function whereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|null $callback
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query with where clauses.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function whereDoesntHaveMorph($relation, $types, ?Closure $callback = null)
|
||||
{
|
||||
return $this->doesntHaveMorph($relation, $types, 'and', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereDoesntHaveMorph($relation, $types, ?Closure $callback = null)
|
||||
{
|
||||
return $this->doesntHaveMorph($relation, $types, 'or', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic where clause to a relationship query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function whereRelation($relation, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->whereHas($relation, function ($query) use ($column, $operator, $value) {
|
||||
if ($column instanceof Closure) {
|
||||
$column($query);
|
||||
} else {
|
||||
$query->where($column, $operator, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an "or where" clause to a relationship query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereRelation($relation, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) {
|
||||
if ($column instanceof Closure) {
|
||||
$column($query);
|
||||
} else {
|
||||
$query->where($column, $operator, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic count / exists condition to a relationship query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function whereDoesntHaveRelation($relation, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->whereDoesntHave($relation, function ($query) use ($column, $operator, $value) {
|
||||
if ($column instanceof Closure) {
|
||||
$column($query);
|
||||
} else {
|
||||
$query->where($column, $operator, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an "or where" clause to a relationship query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereDoesntHaveRelation($relation, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->orWhereDoesntHave($relation, function ($query) use ($column, $operator, $value) {
|
||||
if ($column instanceof Closure) {
|
||||
$column($query);
|
||||
} else {
|
||||
$query->where($column, $operator, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship condition to the query with a where clause.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function whereMorphRelation($relation, $types, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->whereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) {
|
||||
$query->where($column, $operator, $value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship condition to the query with an "or where" clause.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereMorphRelation($relation, $types, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->orWhereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) {
|
||||
$query->where($column, $operator, $value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship condition to the query with a doesn't have clause.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function whereMorphDoesntHaveRelation($relation, $types, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->whereDoesntHaveMorph($relation, $types, function ($query) use ($column, $operator, $value) {
|
||||
$query->where($column, $operator, $value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a polymorphic relationship condition to the query with an "or doesn't have" clause.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param string|array $types
|
||||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereMorphDoesntHaveRelation($relation, $types, $column, $operator = null, $value = null)
|
||||
{
|
||||
return $this->orWhereDoesntHaveMorph($relation, $types, function ($query) use ($column, $operator, $value) {
|
||||
$query->where($column, $operator, $value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a morph-to relationship condition to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param \Illuminate\Database\Eloquent\Model|string|null $model
|
||||
* @return $this
|
||||
*/
|
||||
public function whereMorphedTo($relation, $model, $boolean = 'and')
|
||||
{
|
||||
if (is_string($relation)) {
|
||||
$relation = $this->getRelationWithoutConstraints($relation);
|
||||
}
|
||||
|
||||
if (is_null($model)) {
|
||||
return $this->whereNull($relation->qualifyColumn($relation->getMorphType()), $boolean);
|
||||
}
|
||||
|
||||
if (is_string($model)) {
|
||||
$morphMap = Relation::morphMap();
|
||||
|
||||
if (! empty($morphMap) && in_array($model, $morphMap)) {
|
||||
$model = array_search($model, $morphMap, true);
|
||||
}
|
||||
|
||||
return $this->where($relation->qualifyColumn($relation->getMorphType()), $model, null, $boolean);
|
||||
}
|
||||
|
||||
return $this->where(function ($query) use ($relation, $model) {
|
||||
$query->where($relation->qualifyColumn($relation->getMorphType()), $model->getMorphClass())
|
||||
->where($relation->qualifyColumn($relation->getForeignKeyName()), $model->getKey());
|
||||
}, null, null, $boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a not morph-to relationship condition to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param \Illuminate\Database\Eloquent\Model|string $model
|
||||
* @return $this
|
||||
*/
|
||||
public function whereNotMorphedTo($relation, $model, $boolean = 'and')
|
||||
{
|
||||
if (is_string($relation)) {
|
||||
$relation = $this->getRelationWithoutConstraints($relation);
|
||||
}
|
||||
|
||||
if (is_string($model)) {
|
||||
$morphMap = Relation::morphMap();
|
||||
|
||||
if (! empty($morphMap) && in_array($model, $morphMap)) {
|
||||
$model = array_search($model, $morphMap, true);
|
||||
}
|
||||
|
||||
return $this->whereNot($relation->qualifyColumn($relation->getMorphType()), '<=>', $model, $boolean);
|
||||
}
|
||||
|
||||
return $this->whereNot(function ($query) use ($relation, $model) {
|
||||
$query->where($relation->qualifyColumn($relation->getMorphType()), '<=>', $model->getMorphClass())
|
||||
->where($relation->qualifyColumn($relation->getForeignKeyName()), '<=>', $model->getKey());
|
||||
}, null, null, $boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a morph-to relationship condition to the query with an "or where" clause.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param \Illuminate\Database\Eloquent\Model|string|null $model
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereMorphedTo($relation, $model)
|
||||
{
|
||||
return $this->whereMorphedTo($relation, $model, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a not morph-to relationship condition to the query with an "or where" clause.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
|
||||
* @param \Illuminate\Database\Eloquent\Model|string $model
|
||||
* @return $this
|
||||
*/
|
||||
public function orWhereNotMorphedTo($relation, $model)
|
||||
{
|
||||
return $this->whereNotMorphedTo($relation, $model, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "belongs to" relationship where clause to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model> $related
|
||||
* @param string|null $relationshipName
|
||||
* @param string $boolean
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\RelationNotFoundException
|
||||
*/
|
||||
public function whereBelongsTo($related, $relationshipName = null, $boolean = 'and')
|
||||
{
|
||||
if (! $related instanceof EloquentCollection) {
|
||||
$relatedCollection = $related->newCollection([$related]);
|
||||
} else {
|
||||
$relatedCollection = $related;
|
||||
|
||||
$related = $relatedCollection->first();
|
||||
}
|
||||
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new InvalidArgumentException('Collection given to whereBelongsTo method may not be empty.');
|
||||
}
|
||||
|
||||
if ($relationshipName === null) {
|
||||
$relationshipName = Str::camel(class_basename($related));
|
||||
}
|
||||
|
||||
try {
|
||||
$relationship = $this->model->{$relationshipName}();
|
||||
} catch (BadMethodCallException) {
|
||||
throw RelationNotFoundException::make($this->model, $relationshipName);
|
||||
}
|
||||
|
||||
if (! $relationship instanceof BelongsTo) {
|
||||
throw RelationNotFoundException::make($this->model, $relationshipName, BelongsTo::class);
|
||||
}
|
||||
|
||||
$this->whereIn(
|
||||
$relationship->getQualifiedForeignKeyName(),
|
||||
$relatedCollection->pluck($relationship->getOwnerKeyName())->toArray(),
|
||||
$boolean,
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "BelongsTo" relationship with an "or where" clause to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $related
|
||||
* @param string|null $relationshipName
|
||||
* @return $this
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function orWhereBelongsTo($related, $relationshipName = null)
|
||||
{
|
||||
return $this->whereBelongsTo($related, $relationshipName, 'or');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to include an aggregate value for a relationship.
|
||||
*
|
||||
* @param mixed $relations
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @param string $function
|
||||
* @return $this
|
||||
*/
|
||||
public function withAggregate($relations, $column, $function = null)
|
||||
{
|
||||
if (empty($relations)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_null($this->query->columns)) {
|
||||
$this->query->select([$this->query->from.'.*']);
|
||||
}
|
||||
|
||||
$relations = is_array($relations) ? $relations : [$relations];
|
||||
|
||||
foreach ($this->parseWithRelations($relations) as $name => $constraints) {
|
||||
// First we will determine if the name has been aliased using an "as" clause on the name
|
||||
// and if it has we will extract the actual relationship name and the desired name of
|
||||
// the resulting column. This allows multiple aggregates on the same relationships.
|
||||
$segments = explode(' ', $name);
|
||||
|
||||
unset($alias);
|
||||
|
||||
if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
|
||||
[$name, $alias] = [$segments[0], $segments[2]];
|
||||
}
|
||||
|
||||
$relation = $this->getRelationWithoutConstraints($name);
|
||||
|
||||
if ($function) {
|
||||
if ($this->getQuery()->getGrammar()->isExpression($column)) {
|
||||
$aggregateColumn = $this->getQuery()->getGrammar()->getValue($column);
|
||||
} else {
|
||||
$hashedColumn = $this->getRelationHashedColumn($column, $relation);
|
||||
|
||||
$aggregateColumn = $this->getQuery()->getGrammar()->wrap(
|
||||
$column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn)
|
||||
);
|
||||
}
|
||||
|
||||
$expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn);
|
||||
} else {
|
||||
$expression = $this->getQuery()->getGrammar()->getValue($column);
|
||||
}
|
||||
|
||||
// Here, we will grab the relationship sub-query and prepare to add it to the main query
|
||||
// as a sub-select. First, we'll get the "has" query and use that to get the relation
|
||||
// sub-query. We'll format this relationship name and append this column if needed.
|
||||
$query = $relation->getRelationExistenceQuery(
|
||||
$relation->getRelated()->newQuery(), $this, new Expression($expression)
|
||||
)->setBindings([], 'select');
|
||||
|
||||
$query->callScope($constraints);
|
||||
|
||||
$query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
|
||||
|
||||
// If the query contains certain elements like orderings / more than one column selected
|
||||
// then we will remove those elements from the query so that it will execute properly
|
||||
// when given to the database. Otherwise, we may receive SQL errors or poor syntax.
|
||||
$query->orders = null;
|
||||
$query->setBindings([], 'order');
|
||||
|
||||
if (count($query->columns) > 1) {
|
||||
$query->columns = [$query->columns[0]];
|
||||
$query->bindings['select'] = [];
|
||||
}
|
||||
|
||||
// Finally, we will make the proper column alias to the query and run this sub-select on
|
||||
// the query builder. Then, we will return the builder instance back to the developer
|
||||
// for further constraint chaining that needs to take place on the query as needed.
|
||||
$alias ??= Str::snake(
|
||||
preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function {$this->getQuery()->getGrammar()->getValue($column)}")
|
||||
);
|
||||
|
||||
if ($function === 'exists') {
|
||||
$this->selectRaw(
|
||||
sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)),
|
||||
$query->getBindings()
|
||||
)->withCasts([$alias => 'bool']);
|
||||
} else {
|
||||
$this->selectSub(
|
||||
$function ? $query : $query->limit(1),
|
||||
$alias
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation hashed column name for the given column and relation.
|
||||
*
|
||||
* @param string $column
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relation
|
||||
* @return string
|
||||
*/
|
||||
protected function getRelationHashedColumn($column, $relation)
|
||||
{
|
||||
if (str_contains($column, '.')) {
|
||||
return $column;
|
||||
}
|
||||
|
||||
return $this->getQuery()->from === $relation->getQuery()->getQuery()->from
|
||||
? "{$relation->getRelationCountHash(false)}.$column"
|
||||
: $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to count the relations.
|
||||
*
|
||||
* @param mixed $relations
|
||||
* @return $this
|
||||
*/
|
||||
public function withCount($relations)
|
||||
{
|
||||
return $this->withAggregate(is_array($relations) ? $relations : func_get_args(), '*', 'count');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to include the max of the relation's column.
|
||||
*
|
||||
* @param string|array $relation
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return $this
|
||||
*/
|
||||
public function withMax($relation, $column)
|
||||
{
|
||||
return $this->withAggregate($relation, $column, 'max');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to include the min of the relation's column.
|
||||
*
|
||||
* @param string|array $relation
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return $this
|
||||
*/
|
||||
public function withMin($relation, $column)
|
||||
{
|
||||
return $this->withAggregate($relation, $column, 'min');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to include the sum of the relation's column.
|
||||
*
|
||||
* @param string|array $relation
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return $this
|
||||
*/
|
||||
public function withSum($relation, $column)
|
||||
{
|
||||
return $this->withAggregate($relation, $column, 'sum');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to include the average of the relation's column.
|
||||
*
|
||||
* @param string|array $relation
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return $this
|
||||
*/
|
||||
public function withAvg($relation, $column)
|
||||
{
|
||||
return $this->withAggregate($relation, $column, 'avg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subselect queries to include the existence of related models.
|
||||
*
|
||||
* @param string|array $relation
|
||||
* @return $this
|
||||
*/
|
||||
public function withExists($relation)
|
||||
{
|
||||
return $this->withAggregate($relation, '*', 'exists');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the "has" condition where clause to the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $hasQuery
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relation
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @param string $boolean
|
||||
* @return $this
|
||||
*/
|
||||
protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
|
||||
{
|
||||
$hasQuery->mergeConstraintsFrom($relation->getQuery());
|
||||
|
||||
return $this->canUseExistsForExistenceCheck($operator, $count)
|
||||
? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1)
|
||||
: $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the where constraints from another query to the current query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $from
|
||||
* @return $this
|
||||
*/
|
||||
public function mergeConstraintsFrom(Builder $from)
|
||||
{
|
||||
$whereBindings = $from->getQuery()->getRawBindings()['where'] ?? [];
|
||||
|
||||
$wheres = $from->getQuery()->from !== $this->getQuery()->from
|
||||
? $this->requalifyWhereTables(
|
||||
$from->getQuery()->wheres,
|
||||
$from->getQuery()->grammar->getValue($from->getQuery()->from),
|
||||
$this->getModel()->getTable()
|
||||
) : $from->getQuery()->wheres;
|
||||
|
||||
// Here we have some other query that we want to merge the where constraints from. We will
|
||||
// copy over any where constraints on the query as well as remove any global scopes the
|
||||
// query might have removed. Then we will return ourselves with the finished merging.
|
||||
return $this->withoutGlobalScopes(
|
||||
$from->removedScopes()
|
||||
)->mergeWheres(
|
||||
$wheres, $whereBindings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the table name for any columns with a new qualified name.
|
||||
*
|
||||
* @param array $wheres
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @return array
|
||||
*/
|
||||
protected function requalifyWhereTables(array $wheres, string $from, string $to): array
|
||||
{
|
||||
return (new BaseCollection($wheres))->map(function ($where) use ($from, $to) {
|
||||
return (new BaseCollection($where))->map(function ($value) use ($from, $to) {
|
||||
return is_string($value) && str_starts_with($value, $from.'.')
|
||||
? $to.'.'.Str::afterLast($value, '.')
|
||||
: $value;
|
||||
});
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sub-query count clause to this query.
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder $query
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @param string $boolean
|
||||
* @return $this
|
||||
*/
|
||||
protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and')
|
||||
{
|
||||
$this->query->addBinding($query->getBindings(), 'where');
|
||||
|
||||
return $this->where(
|
||||
new Expression('('.$query->toSql().')'),
|
||||
$operator,
|
||||
is_numeric($count) ? new Expression($count) : $count,
|
||||
$boolean
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "has relation" base query instance.
|
||||
*
|
||||
* @param string $relation
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>
|
||||
*/
|
||||
protected function getRelationWithoutConstraints($relation)
|
||||
{
|
||||
return Relation::noConstraints(function () use ($relation) {
|
||||
return $this->getModel()->{$relation}();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can run an "exists" query to optimize performance.
|
||||
*
|
||||
* @param string $operator
|
||||
* @param int $count
|
||||
* @return bool
|
||||
*/
|
||||
protected function canUseExistsForExistenceCheck($operator, $count)
|
||||
{
|
||||
return ($operator === '>=' || $operator === '<') && $count === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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)
|
||||
{
|
||||
Collection::wrap($this->factory instanceof Factory ? $this->factory->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,97 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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,26 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Factories;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CrossJoinSequence extends Sequence
|
||||
{
|
||||
/**
|
||||
* Create a new cross join sequence instance.
|
||||
*
|
||||
* @param array ...$sequences
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(...$sequences)
|
||||
{
|
||||
$crossJoined = array_map(
|
||||
function ($a) {
|
||||
return array_merge(...$a);
|
||||
},
|
||||
Arr::crossJoin(...$sequences),
|
||||
);
|
||||
|
||||
parent::__construct(...$crossJoined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,971 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Factories;
|
||||
|
||||
use Closure;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
use Illuminate\Support\Traits\ForwardsCalls;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @method $this trashed()
|
||||
*/
|
||||
abstract class Factory
|
||||
{
|
||||
use Conditionable, ForwardsCalls, Macroable {
|
||||
__call as macroCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<TModel>
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* The number of models that should be generated.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $count;
|
||||
|
||||
/**
|
||||
* The state transformations that will be applied to the model.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $states;
|
||||
|
||||
/**
|
||||
* The parent relationships that will be applied to the model.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $has;
|
||||
|
||||
/**
|
||||
* The child relationships that will be applied to the model.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $for;
|
||||
|
||||
/**
|
||||
* The model instances to always use when creating relationships.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $recycle;
|
||||
|
||||
/**
|
||||
* The "after making" callbacks that will be applied to the model.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $afterMaking;
|
||||
|
||||
/**
|
||||
* The "after creating" callbacks that will be applied to the model.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $afterCreating;
|
||||
|
||||
/**
|
||||
* Whether relationships should not be automatically created.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $expandRelationships = true;
|
||||
|
||||
/**
|
||||
* The name of the database connection that will be used to create the models.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* The current Faker instance.
|
||||
*
|
||||
* @var \Faker\Generator
|
||||
*/
|
||||
protected $faker;
|
||||
|
||||
/**
|
||||
* The default namespace where factories reside.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $namespace = 'Database\\Factories\\';
|
||||
|
||||
/**
|
||||
* The default model name resolver.
|
||||
*
|
||||
* @var callable(self): class-string<TModel>
|
||||
*/
|
||||
protected static $modelNameResolver;
|
||||
|
||||
/**
|
||||
* The factory name resolver.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected static $factoryNameResolver;
|
||||
|
||||
/**
|
||||
* Create a new factory instance.
|
||||
*
|
||||
* @param int|null $count
|
||||
* @param \Illuminate\Support\Collection|null $states
|
||||
* @param \Illuminate\Support\Collection|null $has
|
||||
* @param \Illuminate\Support\Collection|null $for
|
||||
* @param \Illuminate\Support\Collection|null $afterMaking
|
||||
* @param \Illuminate\Support\Collection|null $afterCreating
|
||||
* @param string|null $connection
|
||||
* @param \Illuminate\Support\Collection|null $recycle
|
||||
* @param bool $expandRelationships
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
$count = null,
|
||||
?Collection $states = null,
|
||||
?Collection $has = null,
|
||||
?Collection $for = null,
|
||||
?Collection $afterMaking = null,
|
||||
?Collection $afterCreating = null,
|
||||
$connection = null,
|
||||
?Collection $recycle = null,
|
||||
bool $expandRelationships = true
|
||||
) {
|
||||
$this->count = $count;
|
||||
$this->states = $states ?? new Collection;
|
||||
$this->has = $has ?? new Collection;
|
||||
$this->for = $for ?? new Collection;
|
||||
$this->afterMaking = $afterMaking ?? new Collection;
|
||||
$this->afterCreating = $afterCreating ?? new Collection;
|
||||
$this->connection = $connection;
|
||||
$this->recycle = $recycle ?? new Collection;
|
||||
$this->faker = $this->withFaker();
|
||||
$this->expandRelationships = $expandRelationships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract public function definition();
|
||||
|
||||
/**
|
||||
* Get a new factory instance for the given attributes.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @return static
|
||||
*/
|
||||
public static function new($attributes = [])
|
||||
{
|
||||
return (new static)->state($attributes)->configure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new factory instance for the given number of models.
|
||||
*
|
||||
* @param int $count
|
||||
* @return static
|
||||
*/
|
||||
public static function times(int $count)
|
||||
{
|
||||
return static::new()->count($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the factory.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function configure()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw attributes generated by the factory.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function raw($attributes = [], ?Model $parent = null)
|
||||
{
|
||||
if ($this->count === null) {
|
||||
return $this->state($attributes)->getExpandedAttributes($parent);
|
||||
}
|
||||
|
||||
return array_map(function () use ($attributes, $parent) {
|
||||
return $this->state($attributes)->getExpandedAttributes($parent);
|
||||
}, range(1, $this->count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single model and persist it to the database.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @return TModel
|
||||
*/
|
||||
public function createOne($attributes = [])
|
||||
{
|
||||
return $this->count(null)->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single model and persist it to the database without dispatching any model events.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @return TModel
|
||||
*/
|
||||
public function createOneQuietly($attributes = [])
|
||||
{
|
||||
return $this->count(null)->createQuietly($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection of models and persist them to the database.
|
||||
*
|
||||
* @param int|null|iterable<int, array<string, mixed>> $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>
|
||||
*/
|
||||
public function createMany(int|iterable|null $records = null)
|
||||
{
|
||||
$records ??= ($this->count ?? 1);
|
||||
|
||||
$this->count = null;
|
||||
|
||||
if (is_numeric($records)) {
|
||||
$records = array_fill(0, $records, []);
|
||||
}
|
||||
|
||||
return new EloquentCollection(
|
||||
(new Collection($records))->map(function ($record) {
|
||||
return $this->state($record)->create();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection of models and persist them to the database without dispatching any model events.
|
||||
*
|
||||
* @param int|null|iterable<int, array<string, mixed>> $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>
|
||||
*/
|
||||
public function createManyQuietly(int|iterable|null $records = null)
|
||||
{
|
||||
return Model::withoutEvents(fn () => $this->createMany($records));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection of models and persist them to the database.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>|TModel
|
||||
*/
|
||||
public function create($attributes = [], ?Model $parent = null)
|
||||
{
|
||||
if (! empty($attributes)) {
|
||||
return $this->state($attributes)->create([], $parent);
|
||||
}
|
||||
|
||||
$results = $this->make($attributes, $parent);
|
||||
|
||||
if ($results instanceof Model) {
|
||||
$this->store(new Collection([$results]));
|
||||
|
||||
$this->callAfterCreating(new Collection([$results]), $parent);
|
||||
} else {
|
||||
$this->store($results);
|
||||
|
||||
$this->callAfterCreating($results, $parent);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection of models and persist them to the database without dispatching any model events.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>|TModel
|
||||
*/
|
||||
public function createQuietly($attributes = [], ?Model $parent = null)
|
||||
{
|
||||
return Model::withoutEvents(fn () => $this->create($attributes, $parent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a callback that persists a model in the database when invoked.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return \Closure(): (\Illuminate\Database\Eloquent\Collection<int, TModel>|TModel)
|
||||
*/
|
||||
public function lazy(array $attributes = [], ?Model $parent = null)
|
||||
{
|
||||
return fn () => $this->create($attributes, $parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection name on the results and store them.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, \Illuminate\Database\Eloquent\Model> $results
|
||||
* @return void
|
||||
*/
|
||||
protected function store(Collection $results)
|
||||
{
|
||||
$results->each(function ($model) {
|
||||
if (! isset($this->connection)) {
|
||||
$model->setConnection($model->newQueryWithoutScopes()->getConnection()->getName());
|
||||
}
|
||||
|
||||
$model->save();
|
||||
|
||||
foreach ($model->getRelations() as $name => $items) {
|
||||
if ($items instanceof Enumerable && $items->isEmpty()) {
|
||||
$model->unsetRelation($name);
|
||||
}
|
||||
}
|
||||
|
||||
$this->createChildren($model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the children for the given model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return void
|
||||
*/
|
||||
protected function createChildren(Model $model)
|
||||
{
|
||||
Model::unguarded(function () use ($model) {
|
||||
$this->has->each(function ($has) use ($model) {
|
||||
$has->recycle($this->recycle)->createFor($model);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a single instance of the model.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @return TModel
|
||||
*/
|
||||
public function makeOne($attributes = [])
|
||||
{
|
||||
return $this->count(null)->make($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection of models.
|
||||
*
|
||||
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>|TModel
|
||||
*/
|
||||
public function make($attributes = [], ?Model $parent = null)
|
||||
{
|
||||
if (! empty($attributes)) {
|
||||
return $this->state($attributes)->make([], $parent);
|
||||
}
|
||||
|
||||
if ($this->count === null) {
|
||||
return tap($this->makeInstance($parent), function ($instance) {
|
||||
$this->callAfterMaking(new Collection([$instance]));
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->count < 1) {
|
||||
return $this->newModel()->newCollection();
|
||||
}
|
||||
|
||||
$instances = $this->newModel()->newCollection(array_map(function () use ($parent) {
|
||||
return $this->makeInstance($parent);
|
||||
}, range(1, $this->count)));
|
||||
|
||||
$this->callAfterMaking($instances);
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an instance of the model with the given attributes.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
protected function makeInstance(?Model $parent)
|
||||
{
|
||||
return Model::unguarded(function () use ($parent) {
|
||||
return tap($this->newModel($this->getExpandedAttributes($parent)), function ($instance) {
|
||||
if (isset($this->connection)) {
|
||||
$instance->setConnection($this->connection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a raw attributes array for the model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getExpandedAttributes(?Model $parent)
|
||||
{
|
||||
return $this->expandAttributes($this->getRawAttributes($parent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw attributes for the model as an array.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return array
|
||||
*/
|
||||
protected function getRawAttributes(?Model $parent)
|
||||
{
|
||||
return $this->states->pipe(function ($states) {
|
||||
return $this->for->isEmpty() ? $states : new Collection(array_merge([function () {
|
||||
return $this->parentResolvers();
|
||||
}], $states->all()));
|
||||
})->reduce(function ($carry, $state) use ($parent) {
|
||||
if ($state instanceof Closure) {
|
||||
$state = $state->bindTo($this);
|
||||
}
|
||||
|
||||
return array_merge($carry, $state($carry, $parent));
|
||||
}, $this->definition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the parent relationship resolvers (as deferred Closures).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parentResolvers()
|
||||
{
|
||||
return $this->for
|
||||
->map(fn (BelongsToRelationship $for) => $for->recycle($this->recycle)->attributesFor($this->newModel()))
|
||||
->collapse()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all attributes to their underlying values.
|
||||
*
|
||||
* @param array $definition
|
||||
* @return array
|
||||
*/
|
||||
protected function expandAttributes(array $definition)
|
||||
{
|
||||
return (new Collection($definition))
|
||||
->map($evaluateRelations = function ($attribute) {
|
||||
if (! $this->expandRelationships && $attribute instanceof self) {
|
||||
$attribute = null;
|
||||
} elseif ($attribute instanceof self) {
|
||||
$attribute = $this->getRandomRecycledModel($attribute->modelName())?->getKey()
|
||||
?? $attribute->recycle($this->recycle)->create()->getKey();
|
||||
} elseif ($attribute instanceof Model) {
|
||||
$attribute = $attribute->getKey();
|
||||
}
|
||||
|
||||
return $attribute;
|
||||
})
|
||||
->map(function ($attribute, $key) use (&$definition, $evaluateRelations) {
|
||||
if (is_callable($attribute) && ! is_string($attribute) && ! is_array($attribute)) {
|
||||
$attribute = $attribute($definition);
|
||||
}
|
||||
|
||||
$attribute = $evaluateRelations($attribute);
|
||||
|
||||
$definition[$key] = $attribute;
|
||||
|
||||
return $attribute;
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new state transformation to the model definition.
|
||||
*
|
||||
* @param (callable(array<string, mixed>, TModel|null): array<string, mixed>)|array<string, mixed> $state
|
||||
* @return static
|
||||
*/
|
||||
public function state($state)
|
||||
{
|
||||
return $this->newInstance([
|
||||
'states' => $this->states->concat([
|
||||
is_callable($state) ? $state : fn () => $state,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single model attribute.
|
||||
*
|
||||
* @param string|int $key
|
||||
* @param mixed $value
|
||||
* @return static
|
||||
*/
|
||||
public function set($key, $value)
|
||||
{
|
||||
return $this->state([$key => $value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new sequenced state transformation to the model definition.
|
||||
*
|
||||
* @param mixed ...$sequence
|
||||
* @return static
|
||||
*/
|
||||
public function sequence(...$sequence)
|
||||
{
|
||||
return $this->state(new Sequence(...$sequence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new sequenced state transformation to the model definition and update the pending creation count to the size of the sequence.
|
||||
*
|
||||
* @param array ...$sequence
|
||||
* @return static
|
||||
*/
|
||||
public function forEachSequence(...$sequence)
|
||||
{
|
||||
return $this->state(new Sequence(...$sequence))->count(count($sequence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new cross joined sequenced state transformation to the model definition.
|
||||
*
|
||||
* @param array ...$sequence
|
||||
* @return static
|
||||
*/
|
||||
public function crossJoinSequence(...$sequence)
|
||||
{
|
||||
return $this->state(new CrossJoinSequence(...$sequence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a child relationship for the model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Factories\Factory $factory
|
||||
* @param string|null $relationship
|
||||
* @return static
|
||||
*/
|
||||
public function has(self $factory, $relationship = null)
|
||||
{
|
||||
return $this->newInstance([
|
||||
'has' => $this->has->concat([new Relationship(
|
||||
$factory, $relationship ?? $this->guessRelationship($factory->modelName())
|
||||
)]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to guess the relationship name for a "has" relationship.
|
||||
*
|
||||
* @param string $related
|
||||
* @return string
|
||||
*/
|
||||
protected function guessRelationship(string $related)
|
||||
{
|
||||
$guess = Str::camel(Str::plural(class_basename($related)));
|
||||
|
||||
return method_exists($this->modelName(), $guess) ? $guess : Str::singular($guess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define an attached relationship for the model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $factory
|
||||
* @param (callable(): array<string, mixed>)|array<string, mixed> $pivot
|
||||
* @param string|null $relationship
|
||||
* @return static
|
||||
*/
|
||||
public function hasAttached($factory, $pivot = [], $relationship = null)
|
||||
{
|
||||
return $this->newInstance([
|
||||
'has' => $this->has->concat([new BelongsToManyRelationship(
|
||||
$factory,
|
||||
$pivot,
|
||||
$relationship ?? Str::camel(Str::plural(class_basename(
|
||||
$factory instanceof Factory
|
||||
? $factory->modelName()
|
||||
: Collection::wrap($factory)->first()
|
||||
)))
|
||||
)]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a parent relationship for the model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model $factory
|
||||
* @param string|null $relationship
|
||||
* @return static
|
||||
*/
|
||||
public function for($factory, $relationship = null)
|
||||
{
|
||||
return $this->newInstance(['for' => $this->for->concat([new BelongsToRelationship(
|
||||
$factory,
|
||||
$relationship ?? Str::camel(class_basename(
|
||||
$factory instanceof Factory ? $factory->modelName() : $factory
|
||||
))
|
||||
)])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide model instances to use instead of any nested factory calls when creating relationships.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Support\Collection|array $model
|
||||
* @return static
|
||||
*/
|
||||
public function recycle($model)
|
||||
{
|
||||
// Group provided models by the type and merge them into existing recycle collection
|
||||
return $this->newInstance([
|
||||
'recycle' => $this->recycle
|
||||
->flatten()
|
||||
->merge(
|
||||
Collection::wrap($model instanceof Model ? func_get_args() : $model)
|
||||
->flatten()
|
||||
)->groupBy(fn ($model) => get_class($model)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a random model of a given type from previously provided models to recycle.
|
||||
*
|
||||
* @template TClass of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TClass> $modelClassName
|
||||
* @return TClass|null
|
||||
*/
|
||||
public function getRandomRecycledModel($modelClassName)
|
||||
{
|
||||
return $this->recycle->get($modelClassName)?->random();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new "after making" callback to the model definition.
|
||||
*
|
||||
* @param \Closure(TModel): mixed $callback
|
||||
* @return static
|
||||
*/
|
||||
public function afterMaking(Closure $callback)
|
||||
{
|
||||
return $this->newInstance(['afterMaking' => $this->afterMaking->concat([$callback])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new "after creating" callback to the model definition.
|
||||
*
|
||||
* @param \Closure(TModel, \Illuminate\Database\Eloquent\Model|null): mixed $callback
|
||||
* @return static
|
||||
*/
|
||||
public function afterCreating(Closure $callback)
|
||||
{
|
||||
return $this->newInstance(['afterCreating' => $this->afterCreating->concat([$callback])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the "after making" callbacks for the given model instances.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $instances
|
||||
* @return void
|
||||
*/
|
||||
protected function callAfterMaking(Collection $instances)
|
||||
{
|
||||
$instances->each(function ($model) {
|
||||
$this->afterMaking->each(function ($callback) use ($model) {
|
||||
$callback($model);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the "after creating" callbacks for the given model instances.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $instances
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $parent
|
||||
* @return void
|
||||
*/
|
||||
protected function callAfterCreating(Collection $instances, ?Model $parent = null)
|
||||
{
|
||||
$instances->each(function ($model) use ($parent) {
|
||||
$this->afterCreating->each(function ($callback) use ($model, $parent) {
|
||||
$callback($model, $parent);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify how many models should be generated.
|
||||
*
|
||||
* @param int|null $count
|
||||
* @return static
|
||||
*/
|
||||
public function count(?int $count)
|
||||
{
|
||||
return $this->newInstance(['count' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that related parent models should not be created.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withoutParents()
|
||||
{
|
||||
return $this->newInstance(['expandRelationships' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the database connection that is used to generate models.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getConnectionName()
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the database connection that should be used to generate models.
|
||||
*
|
||||
* @param string $connection
|
||||
* @return static
|
||||
*/
|
||||
public function connection(string $connection)
|
||||
{
|
||||
return $this->newInstance(['connection' => $connection]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the factory builder with the given mutated properties.
|
||||
*
|
||||
* @param array $arguments
|
||||
* @return static
|
||||
*/
|
||||
protected function newInstance(array $arguments = [])
|
||||
{
|
||||
return new static(...array_values(array_merge([
|
||||
'count' => $this->count,
|
||||
'states' => $this->states,
|
||||
'has' => $this->has,
|
||||
'for' => $this->for,
|
||||
'afterMaking' => $this->afterMaking,
|
||||
'afterCreating' => $this->afterCreating,
|
||||
'connection' => $this->connection,
|
||||
'recycle' => $this->recycle,
|
||||
'expandRelationships' => $this->expandRelationships,
|
||||
], $arguments)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new model instance.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return TModel
|
||||
*/
|
||||
public function newModel(array $attributes = [])
|
||||
{
|
||||
$model = $this->modelName();
|
||||
|
||||
return new $model($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the model that is generated by the factory.
|
||||
*
|
||||
* @return class-string<TModel>
|
||||
*/
|
||||
public function modelName()
|
||||
{
|
||||
if ($this->model !== null) {
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
$resolver = static::$modelNameResolver ?? function (self $factory) {
|
||||
$namespacedFactoryBasename = Str::replaceLast(
|
||||
'Factory', '', Str::replaceFirst(static::$namespace, '', get_class($factory))
|
||||
);
|
||||
|
||||
$factoryBasename = Str::replaceLast('Factory', '', class_basename($factory));
|
||||
|
||||
$appNamespace = static::appNamespace();
|
||||
|
||||
return class_exists($appNamespace.'Models\\'.$namespacedFactoryBasename)
|
||||
? $appNamespace.'Models\\'.$namespacedFactoryBasename
|
||||
: $appNamespace.$factoryBasename;
|
||||
};
|
||||
|
||||
return $resolver($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be invoked to guess model names based on factory names.
|
||||
*
|
||||
* @param callable(self): class-string<TModel> $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function guessModelNamesUsing(callable $callback)
|
||||
{
|
||||
static::$modelNameResolver = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the default namespace that contains the application's model factories.
|
||||
*
|
||||
* @param string $namespace
|
||||
* @return void
|
||||
*/
|
||||
public static function useNamespace(string $namespace)
|
||||
{
|
||||
static::$namespace = $namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new factory instance for the given model name.
|
||||
*
|
||||
* @template TClass of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TClass> $modelName
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<TClass>
|
||||
*/
|
||||
public static function factoryForModel(string $modelName)
|
||||
{
|
||||
$factory = static::resolveFactoryName($modelName);
|
||||
|
||||
return $factory::new();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be invoked to guess factory names based on dynamic relationship names.
|
||||
*
|
||||
* @param callable(class-string<\Illuminate\Database\Eloquent\Model>): class-string<\Illuminate\Database\Eloquent\Factories\Factory> $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function guessFactoryNamesUsing(callable $callback)
|
||||
{
|
||||
static::$factoryNameResolver = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new Faker instance.
|
||||
*
|
||||
* @return \Faker\Generator
|
||||
*/
|
||||
protected function withFaker()
|
||||
{
|
||||
return Container::getInstance()->make(Generator::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the factory name for the given model name.
|
||||
*
|
||||
* @template TClass of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param class-string<TClass> $modelName
|
||||
* @return class-string<\Illuminate\Database\Eloquent\Factories\Factory<TClass>>
|
||||
*/
|
||||
public static function resolveFactoryName(string $modelName)
|
||||
{
|
||||
$resolver = static::$factoryNameResolver ?? function (string $modelName) {
|
||||
$appNamespace = static::appNamespace();
|
||||
|
||||
$modelName = Str::startsWith($modelName, $appNamespace.'Models\\')
|
||||
? Str::after($modelName, $appNamespace.'Models\\')
|
||||
: Str::after($modelName, $appNamespace);
|
||||
|
||||
return static::$namespace.$modelName.'Factory';
|
||||
};
|
||||
|
||||
return $resolver($modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application namespace for the application.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function appNamespace()
|
||||
{
|
||||
try {
|
||||
return Container::getInstance()
|
||||
->make(Application::class)
|
||||
->getNamespace();
|
||||
} catch (Throwable) {
|
||||
return 'App\\';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy dynamic factory methods onto their proper methods.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (static::hasMacro($method)) {
|
||||
return $this->macroCall($method, $parameters);
|
||||
}
|
||||
|
||||
if ($method === 'trashed' && in_array(SoftDeletes::class, class_uses_recursive($this->modelName()))) {
|
||||
return $this->state([
|
||||
$this->newModel()->getDeletedAtColumn() => $parameters[0] ?? Carbon::now()->subDay(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! Str::startsWith($method, ['for', 'has'])) {
|
||||
static::throwBadMethodCallException($method);
|
||||
}
|
||||
|
||||
$relationship = Str::camel(Str::substr($method, 3));
|
||||
|
||||
$relatedModel = get_class($this->newModel()->{$relationship}()->getRelated());
|
||||
|
||||
if (method_exists($relatedModel, 'newFactory')) {
|
||||
$factory = $relatedModel::newFactory() ?? static::factoryForModel($relatedModel);
|
||||
} else {
|
||||
$factory = static::factoryForModel($relatedModel);
|
||||
}
|
||||
|
||||
if (str_starts_with($method, 'for')) {
|
||||
return $this->for($factory->state($parameters[0] ?? []), $relationship);
|
||||
} elseif (str_starts_with($method, 'has')) {
|
||||
return $this->has(
|
||||
$factory
|
||||
->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : 1)
|
||||
->state((is_callable($parameters[0] ?? null) || is_array($parameters[0] ?? null)) ? $parameters[0] : ($parameters[1] ?? [])),
|
||||
$relationship
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = new $useFactory->factoryClass;
|
||||
|
||||
$factory->guessModelNamesUsing(fn () => static::class);
|
||||
|
||||
return $factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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(),
|
||||
])->create([], $parent);
|
||||
} elseif ($relationship instanceof HasOneOrMany) {
|
||||
$this->factory->state([
|
||||
$relationship->getForeignKeyName() => $relationship->getParentKey(),
|
||||
])->create([], $parent);
|
||||
} elseif ($relationship instanceof BelongsToMany) {
|
||||
$relationship->attach($this->factory->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,63 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
return tap(value($this->sequence[$this->index % $this->count], $this), function () {
|
||||
$this->index = $this->index + 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
+124
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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);
|
||||
|
||||
return new static::$resolvedCollectionClasses[static::class]($models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the collection class name from the CollectedBy attribute.
|
||||
*
|
||||
* @return class-string<TCollection>|null
|
||||
*/
|
||||
public function resolveCollectionFromAttribute()
|
||||
{
|
||||
$reflectionClass = new ReflectionClass(static::class);
|
||||
|
||||
$attributes = $reflectionClass->getAttributes(CollectedBy::class);
|
||||
|
||||
if (! isset($attributes[0]) || ! isset($attributes[0]->getArguments()[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $attributes[0]->getArguments()[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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,48 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class MassAssignmentException extends RuntimeException
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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;
|
||||
|
||||
do {
|
||||
$total += $count = in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))
|
||||
? $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>
|
||||
*/
|
||||
public function prunable()
|
||||
{
|
||||
throw new LogicException('Please implement the prunable method on your model.');
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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)
|
||||
));
|
||||
}
|
||||
}
|
||||
+2434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection as BaseCollection;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
use SplFileObject;
|
||||
|
||||
use function Illuminate\Support\enum_value;
|
||||
|
||||
class ModelInspector
|
||||
{
|
||||
/**
|
||||
* The Laravel application instance.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Foundation\Application
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* The methods that can be called in a model to indicate a relation.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $relationMethods = [
|
||||
'hasMany',
|
||||
'hasManyThrough',
|
||||
'hasOneThrough',
|
||||
'belongsToMany',
|
||||
'hasOne',
|
||||
'belongsTo',
|
||||
'morphOne',
|
||||
'morphTo',
|
||||
'morphMany',
|
||||
'morphToMany',
|
||||
'morphedByMany',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new model inspector instance.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Foundation\Application $app
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract model details for the given model.
|
||||
*
|
||||
* @param class-string<\Illuminate\Database\Eloquent\Model>|string $model
|
||||
* @param string|null $connection
|
||||
* @return array{"class": class-string<\Illuminate\Database\Eloquent\Model>, database: string, table: string, policy: class-string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>}
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function inspect($model, $connection = null)
|
||||
{
|
||||
$class = $this->qualifyModel($model);
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Model $model */
|
||||
$model = $this->app->make($class);
|
||||
|
||||
if ($connection !== null) {
|
||||
$model->setConnection($connection);
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => get_class($model),
|
||||
'database' => $model->getConnection()->getName(),
|
||||
'table' => $model->getConnection()->getTablePrefix().$model->getTable(),
|
||||
'policy' => $this->getPolicy($model),
|
||||
'attributes' => $this->getAttributes($model),
|
||||
'relations' => $this->getRelations($model),
|
||||
'events' => $this->getEvents($model),
|
||||
'observers' => $this->getObservers($model),
|
||||
'collection' => $this->getCollectedBy($model),
|
||||
'builder' => $this->getBuilder($model),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column attributes for the given model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
|
||||
*/
|
||||
protected function getAttributes($model)
|
||||
{
|
||||
$connection = $model->getConnection();
|
||||
$schema = $connection->getSchemaBuilder();
|
||||
$table = $model->getTable();
|
||||
$columns = $schema->getColumns($table);
|
||||
$indexes = $schema->getIndexes($table);
|
||||
|
||||
return (new BaseCollection($columns))
|
||||
->map(fn ($column) => [
|
||||
'name' => $column['name'],
|
||||
'type' => $column['type'],
|
||||
'increments' => $column['auto_increment'],
|
||||
'nullable' => $column['nullable'],
|
||||
'default' => $this->getColumnDefault($column, $model),
|
||||
'unique' => $this->columnIsUnique($column['name'], $indexes),
|
||||
'fillable' => $model->isFillable($column['name']),
|
||||
'hidden' => $this->attributeIsHidden($column['name'], $model),
|
||||
'appended' => null,
|
||||
'cast' => $this->getCastType($column['name'], $model),
|
||||
])
|
||||
->merge($this->getVirtualAttributes($model, $columns));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the virtual (non-column) attributes for the given model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param array $columns
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getVirtualAttributes($model, $columns)
|
||||
{
|
||||
$class = new ReflectionClass($model);
|
||||
|
||||
return (new BaseCollection($class->getMethods()))
|
||||
->reject(
|
||||
fn (ReflectionMethod $method) => $method->isStatic()
|
||||
|| $method->isAbstract()
|
||||
|| $method->getDeclaringClass()->getName() === Model::class
|
||||
)
|
||||
->mapWithKeys(function (ReflectionMethod $method) use ($model) {
|
||||
if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) {
|
||||
return [Str::snake($matches[1]) => 'accessor'];
|
||||
} elseif ($model->hasAttributeMutator($method->getName())) {
|
||||
return [Str::snake($method->getName()) => 'attribute'];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
->reject(fn ($cast, $name) => (new BaseCollection($columns))->contains('name', $name))
|
||||
->map(fn ($cast, $name) => [
|
||||
'name' => $name,
|
||||
'type' => null,
|
||||
'increments' => false,
|
||||
'nullable' => null,
|
||||
'default' => null,
|
||||
'unique' => null,
|
||||
'fillable' => $model->isFillable($name),
|
||||
'hidden' => $this->attributeIsHidden($name, $model),
|
||||
'appended' => $model->hasAppended($name),
|
||||
'cast' => $cast,
|
||||
])
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relations from the given model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getRelations($model)
|
||||
{
|
||||
return (new BaseCollection(get_class_methods($model)))
|
||||
->map(fn ($method) => new ReflectionMethod($model, $method))
|
||||
->reject(
|
||||
fn (ReflectionMethod $method) => $method->isStatic()
|
||||
|| $method->isAbstract()
|
||||
|| $method->getDeclaringClass()->getName() === Model::class
|
||||
|| $method->getNumberOfParameters() > 0
|
||||
)
|
||||
->filter(function (ReflectionMethod $method) {
|
||||
if ($method->getReturnType() instanceof ReflectionNamedType
|
||||
&& is_subclass_of($method->getReturnType()->getName(), Relation::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$file = new SplFileObject($method->getFileName());
|
||||
$file->seek($method->getStartLine() - 1);
|
||||
$code = '';
|
||||
while ($file->key() < $method->getEndLine()) {
|
||||
$code .= trim($file->current());
|
||||
$file->next();
|
||||
}
|
||||
|
||||
return (new BaseCollection($this->relationMethods))
|
||||
->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'('));
|
||||
})
|
||||
->map(function (ReflectionMethod $method) use ($model) {
|
||||
$relation = $method->invoke($model);
|
||||
|
||||
if (! $relation instanceof Relation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $method->getName(),
|
||||
'type' => Str::afterLast(get_class($relation), '\\'),
|
||||
'related' => get_class($relation->getRelated()),
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first policy associated with this model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getPolicy($model)
|
||||
{
|
||||
$policy = Gate::getPolicyFor($model::class);
|
||||
|
||||
return $policy ? $policy::class : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the events that the model dispatches.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getEvents($model)
|
||||
{
|
||||
return (new BaseCollection($model->dispatchesEvents()))
|
||||
->map(fn (string $class, string $event) => [
|
||||
'event' => $event,
|
||||
'class' => $class,
|
||||
])->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the observers watching this model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return \Illuminate\Support\Collection
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
protected function getObservers($model)
|
||||
{
|
||||
$listeners = $this->app->make('events')->getRawListeners();
|
||||
|
||||
// Get the Eloquent observers for this model...
|
||||
$listeners = array_filter($listeners, function ($v, $key) use ($model) {
|
||||
return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
// Format listeners Eloquent verb => Observer methods...
|
||||
$extractVerb = function ($key) {
|
||||
preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches);
|
||||
|
||||
return $matches[1] ?? '?';
|
||||
};
|
||||
|
||||
$formatted = [];
|
||||
|
||||
foreach ($listeners as $key => $observerMethods) {
|
||||
$formatted[] = [
|
||||
'event' => $extractVerb($key),
|
||||
'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods),
|
||||
];
|
||||
}
|
||||
|
||||
return new BaseCollection($formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection class being used by the model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return class-string<\Illuminate\Database\Eloquent\Collection>
|
||||
*/
|
||||
protected function getCollectedBy($model)
|
||||
{
|
||||
return $model->newCollection()::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the builder class being used by the model.
|
||||
*
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param TModel $model
|
||||
* @return class-string<\Illuminate\Database\Eloquent\Builder<TModel>>
|
||||
*/
|
||||
protected function getBuilder($model)
|
||||
{
|
||||
return $model->newQuery()::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Qualify the given model class base name.
|
||||
*
|
||||
* @param string $model
|
||||
* @return class-string<\Illuminate\Database\Eloquent\Model>
|
||||
*
|
||||
* @see \Illuminate\Console\GeneratorCommand
|
||||
*/
|
||||
protected function qualifyModel(string $model)
|
||||
{
|
||||
if (str_contains($model, '\\') && class_exists($model)) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
$model = ltrim($model, '\\/');
|
||||
|
||||
$model = str_replace('/', '\\', $model);
|
||||
|
||||
$rootNamespace = $this->app->getNamespace();
|
||||
|
||||
if (Str::startsWith($model, $rootNamespace)) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
return is_dir(app_path('Models'))
|
||||
? $rootNamespace.'Models\\'.$model
|
||||
: $rootNamespace.$model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cast type for the given column.
|
||||
*
|
||||
* @param string $column
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getCastType($column, $model)
|
||||
{
|
||||
if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) {
|
||||
return 'accessor';
|
||||
}
|
||||
|
||||
if ($model->hasAttributeMutator($column)) {
|
||||
return 'attribute';
|
||||
}
|
||||
|
||||
return $this->getCastsWithDates($model)->get($column) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model casts, including any date casts.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getCastsWithDates($model)
|
||||
{
|
||||
return (new BaseCollection($model->getDates()))
|
||||
->filter()
|
||||
->flip()
|
||||
->map(fn () => 'datetime')
|
||||
->merge($model->getCasts());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given attribute is hidden.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return bool
|
||||
*/
|
||||
protected function attributeIsHidden($attribute, $model)
|
||||
{
|
||||
if (count($model->getHidden()) > 0) {
|
||||
return in_array($attribute, $model->getHidden());
|
||||
}
|
||||
|
||||
if (count($model->getVisible()) > 0) {
|
||||
return ! in_array($attribute, $model->getVisible());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value for the given column.
|
||||
*
|
||||
* @param array<string, mixed> $column
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function getColumnDefault($column, $model)
|
||||
{
|
||||
$attributeDefault = $model->getAttributes()[$column['name']] ?? null;
|
||||
|
||||
return enum_value($attributeDefault) ?? $column['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given attribute is unique.
|
||||
*
|
||||
* @param string $column
|
||||
* @param array $indexes
|
||||
* @return bool
|
||||
*/
|
||||
protected function columnIsUnique($column, $indexes)
|
||||
{
|
||||
return (new BaseCollection($indexes))->contains(
|
||||
fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique']
|
||||
);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
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 = Arr::wrap($ids);
|
||||
|
||||
$this->message = "No query results for model [{$model}]";
|
||||
|
||||
if (count($this->ids) > 0) {
|
||||
$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,117 @@
|
||||
<?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
|
||||
*/
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
use Illuminate\Database\Events\ModelsPruned;
|
||||
use LogicException;
|
||||
|
||||
trait Prunable
|
||||
{
|
||||
/**
|
||||
* Prune all prunable models in the database.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @return int
|
||||
*/
|
||||
public function pruneAll(int $chunkSize = 1000)
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
$this->prunable()
|
||||
->when(in_array(SoftDeletes::class, class_uses_recursive(static::class)), function ($query) {
|
||||
$query->withTrashed();
|
||||
})->chunkById($chunkSize, function ($models) use (&$total) {
|
||||
$models->each->prune();
|
||||
|
||||
$total += $models->count();
|
||||
|
||||
event(new ModelsPruned(static::class, $total));
|
||||
});
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prunable model query.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
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 in_array(SoftDeletes::class, class_uses_recursive(static::class))
|
||||
? $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
@@ -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;
|
||||
}
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
<?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
|
||||
* @return void
|
||||
*/
|
||||
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));
|
||||
|
||||
$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 (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 array|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;
|
||||
}
|
||||
}
|
||||
+1640
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
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->setTable(str_replace(
|
||||
'\\', '', Str::snake(Str::singular(class_basename($this)))
|
||||
));
|
||||
}
|
||||
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 array_key_exists($this->getCreatedAtColumn(), $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;
|
||||
}
|
||||
}
|
||||
+77
@@ -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;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?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 mixed
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function getDictionaryKey($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 $attribute;
|
||||
}
|
||||
}
|
||||
+694
@@ -0,0 +1,694 @@
|
||||
<?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 (count($detach) > 0) {
|
||||
$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 (count($attach) > 0) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs without detaching.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $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 $ids
|
||||
* @param bool $detaching
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*/
|
||||
public function sync($ids, $detaching = true)
|
||||
{
|
||||
$changes = [
|
||||
'attached' => [], 'detached' => [], 'updated' => [],
|
||||
];
|
||||
|
||||
// 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();
|
||||
|
||||
$records = $this->formatRecordsList($this->parseIds($ids));
|
||||
|
||||
// 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 (count($detach) > 0) {
|
||||
$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 with the given pivot values.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 &&
|
||||
empty($this->pivotWheres) &&
|
||||
empty($this->pivotWhereIns) &&
|
||||
empty($this->pivotWhereNulls)) {
|
||||
return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch);
|
||||
}
|
||||
|
||||
if ($this->hasPivotColumn($this->updatedAt())) {
|
||||
$attributes = $this->addTimestampsToAttachment($attributes, true);
|
||||
}
|
||||
|
||||
$updated = $this->newPivotStatementForId($this->parseId($id))->update(
|
||||
$this->castAttributes($attributes)
|
||||
);
|
||||
|
||||
if ($touch) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->getCurrentlyAttachedPivots()
|
||||
->where($this->foreignPivotKey, $this->parent->{$this->parentKey})
|
||||
->where($this->relatedPivotKey, $this->parseId($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 $id
|
||||
* @param array $attributes
|
||||
* @param bool $touch
|
||||
* @return void
|
||||
*/
|
||||
public function attach($id, array $attributes = [], $touch = true)
|
||||
{
|
||||
if ($this->using) {
|
||||
$this->attachUsingCustomClass($id, $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($id), $attributes
|
||||
));
|
||||
}
|
||||
|
||||
if ($touch) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a model to the parent using a custom class.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $attributes
|
||||
* @return void
|
||||
*/
|
||||
protected function attachUsingCustomClass($id, array $attributes)
|
||||
{
|
||||
$records = $this->formatAttachRecords(
|
||||
$this->parseIds($id), $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 &&
|
||||
! empty($ids) &&
|
||||
empty($this->pivotWheres) &&
|
||||
empty($this->pivotWhereIns) &&
|
||||
empty($this->pivotWhereNulls)) {
|
||||
$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 using a custom class.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @return int
|
||||
*/
|
||||
protected function detachUsingCustomClass($ids)
|
||||
{
|
||||
$results = 0;
|
||||
|
||||
foreach ($this->parseIds($ids) as $id) {
|
||||
$results += $this->newPivot([
|
||||
$this->foreignPivotKey => $this->parent->{$this->parentKey},
|
||||
$this->relatedPivotKey => $id,
|
||||
], true)->delete();
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pivot models that are currently attached.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getCurrentlyAttachedPivots()
|
||||
{
|
||||
return $this->newPivotQuery()->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->relatedPivotKey, $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 array|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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+63
@@ -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;
|
||||
}
|
||||
}
|
||||
+157
@@ -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
@@ -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,74 @@
|
||||
<?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->secondKey,
|
||||
$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) {
|
||||
if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) {
|
||||
$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
@@ -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());
|
||||
}
|
||||
}
|
||||
+577
@@ -0,0 +1,577 @@
|
||||
<?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\InteractsWithDictionary;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @return void
|
||||
*/
|
||||
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) {
|
||||
if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) {
|
||||
$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<int, TRelatedModel>>
|
||||
*/
|
||||
protected function buildDictionary(EloquentCollection $results)
|
||||
{
|
||||
$foreign = $this->getForeignKeyName();
|
||||
|
||||
return $results->mapToDictionary(function ($result) use ($foreign) {
|
||||
return [$this->getDictionaryKey($result->{$foreign}) => $result];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrNew(array $attributes = [], array $values = [])
|
||||
{
|
||||
if (is_null($instance = $this->where($attributes)->first())) {
|
||||
$instance = $this->related->newInstance(array_merge($attributes, $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 array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrCreate(array $attributes = [], 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 array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function createOrFirst(array $attributes = [], array $values = [])
|
||||
{
|
||||
try {
|
||||
return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, $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 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(reset($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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (! $model->hasAttribute($key)) {
|
||||
$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 array|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 end($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,845 @@
|
||||
<?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\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Query\Grammars\MySqlGrammar;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @return void
|
||||
*/
|
||||
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()
|
||||
{
|
||||
$localValue = $this->farParent[$this->localKey];
|
||||
|
||||
$this->performJoin();
|
||||
|
||||
if (static::$constraints) {
|
||||
$this->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 = $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 in_array(SoftDeletes::class, class_uses_recursive($this->throughParent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build model dictionary keyed by the relation's foreign key.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @return array<array<TRelatedModel>>
|
||||
*/
|
||||
protected function buildDictionary(EloquentCollection $results)
|
||||
{
|
||||
$dictionary = [];
|
||||
|
||||
// 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 $result) {
|
||||
$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 array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrCreate(array $attributes = [], array $values = [])
|
||||
{
|
||||
if (! is_null($instance = (clone $this)->where($attributes)->first())) {
|
||||
return $instance;
|
||||
}
|
||||
|
||||
return $this->createOrFirst(array_merge($attributes, $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function createOrFirst(array $attributes = [], array $values = [])
|
||||
{
|
||||
try {
|
||||
return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, $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->take(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 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 $page
|
||||
* @return \Illuminate\Contracts\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 = $column ?? $this->getRelated()->getQualifiedKeyName();
|
||||
|
||||
$alias = $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 array|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 array|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,67 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
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\SupportsDefaultModels;
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
use InteractsWithDictionary, SupportsDefaultModels;
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getResults()
|
||||
{
|
||||
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) {
|
||||
if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) {
|
||||
$value = $dictionary[$key];
|
||||
$model->setRelation(
|
||||
$relation, reset($value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new related instance for the given model.
|
||||
*
|
||||
* @param TDeclaringModel $parent
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function newRelatedInstanceFor(Model $parent)
|
||||
{
|
||||
return $this->related->newInstance();
|
||||
}
|
||||
}
|
||||
+69
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
<?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 class name of the parent model.
|
||||
*
|
||||
* @var 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
|
||||
* @return void
|
||||
*/
|
||||
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) {
|
||||
if (! $model->hasAttribute($key)) {
|
||||
$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(reset($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 class name of the parent model.
|
||||
*
|
||||
* @return 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 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 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
class MorphTo extends BelongsTo
|
||||
{
|
||||
use InteractsWithDictionary;
|
||||
|
||||
/**
|
||||
* The type of the polymorphic relation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* The associated key on the parent model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $ownerKey;
|
||||
|
||||
/**
|
||||
* The models whose relations are being eager loaded.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Collection<int, TDeclaringModel>
|
||||
*/
|
||||
protected $models;
|
||||
|
||||
/**
|
||||
* All of the models keyed by ID.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dictionary = [];
|
||||
|
||||
/**
|
||||
* A buffer of dynamic calls to query macros.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $macroBuffer = [];
|
||||
|
||||
/**
|
||||
* A map of relations to load for each individual morph type.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $morphableEagerLoads = [];
|
||||
|
||||
/**
|
||||
* A map of relationship counts to load for each individual morph type.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $morphableEagerLoadCounts = [];
|
||||
|
||||
/**
|
||||
* A map of constraints to apply for each individual morph type.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $morphableConstraints = [];
|
||||
|
||||
/**
|
||||
* Create a new morph to relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $foreignKey
|
||||
* @param string|null $ownerKey
|
||||
* @param string $type
|
||||
* @param string $relation
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation)
|
||||
{
|
||||
$this->morphType = $type;
|
||||
|
||||
parent::__construct($query, $parent, $foreignKey, $ownerKey, $relation);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
#[\Override]
|
||||
public function addEagerConstraints(array $models)
|
||||
{
|
||||
$this->buildDictionary($this->models = new EloquentCollection($models));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a dictionary with the models.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $models
|
||||
* @return void
|
||||
*/
|
||||
protected function buildDictionary(EloquentCollection $models)
|
||||
{
|
||||
foreach ($models as $model) {
|
||||
if ($model->{$this->morphType}) {
|
||||
$morphTypeKey = $this->getDictionaryKey($model->{$this->morphType});
|
||||
$foreignKeyKey = $this->getDictionaryKey($model->{$this->foreignKey});
|
||||
|
||||
$this->dictionary[$morphTypeKey][$foreignKeyKey][] = $model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results of the relationship.
|
||||
*
|
||||
* Called via eager load method of Eloquent query builder.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TDeclaringModel>
|
||||
*/
|
||||
public function getEager()
|
||||
{
|
||||
foreach (array_keys($this->dictionary) as $type) {
|
||||
$this->matchToMorphParents($type, $this->getResultsByType($type));
|
||||
}
|
||||
|
||||
return $this->models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the relation results for a type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
protected function getResultsByType($type)
|
||||
{
|
||||
$instance = $this->createModelByType($type);
|
||||
|
||||
$ownerKey = $this->ownerKey ?? $instance->getKeyName();
|
||||
|
||||
$query = $this->replayMacros($instance->newQuery())
|
||||
->mergeConstraintsFrom($this->getQuery())
|
||||
->with(array_merge(
|
||||
$this->getQuery()->getEagerLoads(),
|
||||
(array) ($this->morphableEagerLoads[get_class($instance)] ?? [])
|
||||
))
|
||||
->withCount(
|
||||
(array) ($this->morphableEagerLoadCounts[get_class($instance)] ?? [])
|
||||
);
|
||||
|
||||
if ($callback = ($this->morphableConstraints[get_class($instance)] ?? null)) {
|
||||
$callback($query);
|
||||
}
|
||||
|
||||
$whereIn = $this->whereInMethod($instance, $ownerKey);
|
||||
|
||||
return $query->{$whereIn}(
|
||||
$instance->qualifyColumn($ownerKey), $this->gatherKeysByType($type, $instance->getKeyType())
|
||||
)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather all of the foreign keys for a given type.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $keyType
|
||||
* @return array
|
||||
*/
|
||||
protected function gatherKeysByType($type, $keyType)
|
||||
{
|
||||
return $keyType !== 'string'
|
||||
? array_keys($this->dictionary[$type])
|
||||
: array_map(function ($modelId) {
|
||||
return (string) $modelId;
|
||||
}, array_filter(array_keys($this->dictionary[$type])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new model instance by type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function createModelByType($type)
|
||||
{
|
||||
$class = Model::getActualClassNameForMorph($type);
|
||||
|
||||
return tap(new $class, function ($instance) {
|
||||
if (! $instance->getConnectionName()) {
|
||||
$instance->setConnection($this->getConnection()->getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
#[\Override]
|
||||
public function match(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the results for a given type to their parents.
|
||||
*
|
||||
* @param string $type
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @return void
|
||||
*/
|
||||
protected function matchToMorphParents($type, EloquentCollection $results)
|
||||
{
|
||||
foreach ($results as $result) {
|
||||
$ownerKey = ! is_null($this->ownerKey) ? $this->getDictionaryKey($result->{$this->ownerKey}) : $result->getKey();
|
||||
|
||||
if (isset($this->dictionary[$type][$ownerKey])) {
|
||||
foreach ($this->dictionary[$type][$ownerKey] as $model) {
|
||||
$model->setRelation($this->relationName, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the model instance to the given parent.
|
||||
*
|
||||
* @param TRelatedModel|null $model
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
#[\Override]
|
||||
public function associate($model)
|
||||
{
|
||||
if ($model instanceof Model) {
|
||||
$foreignKey = $this->ownerKey && $model->{$this->ownerKey}
|
||||
? $this->ownerKey
|
||||
: $model->getKeyName();
|
||||
}
|
||||
|
||||
$this->parent->setAttribute(
|
||||
$this->foreignKey, $model instanceof Model ? $model->{$foreignKey} : null
|
||||
);
|
||||
|
||||
$this->parent->setAttribute(
|
||||
$this->morphType, $model instanceof Model ? $model->getMorphClass() : null
|
||||
);
|
||||
|
||||
return $this->parent->setRelation($this->relationName, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dissociate previously associated model from the given parent.
|
||||
*
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
#[\Override]
|
||||
public function dissociate()
|
||||
{
|
||||
$this->parent->setAttribute($this->foreignKey, null);
|
||||
|
||||
$this->parent->setAttribute($this->morphType, null);
|
||||
|
||||
return $this->parent->setRelation($this->relationName, null);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
#[\Override]
|
||||
public function touch()
|
||||
{
|
||||
if (! is_null($this->getParentKey())) {
|
||||
parent::touch();
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
#[\Override]
|
||||
protected function newRelatedInstanceFor(Model $parent)
|
||||
{
|
||||
return $parent->{$this->getRelationName()}()->getRelated()->newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key "type" name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphType()
|
||||
{
|
||||
return $this->morphType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dictionary used by the relationship.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDictionary()
|
||||
{
|
||||
return $this->dictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify which relations to load for a given morph type.
|
||||
*
|
||||
* @param array $with
|
||||
* @return $this
|
||||
*/
|
||||
public function morphWith(array $with)
|
||||
{
|
||||
$this->morphableEagerLoads = array_merge(
|
||||
$this->morphableEagerLoads, $with
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify which relationship counts to load for a given morph type.
|
||||
*
|
||||
* @param array $withCount
|
||||
* @return $this
|
||||
*/
|
||||
public function morphWithCount(array $withCount)
|
||||
{
|
||||
$this->morphableEagerLoadCounts = array_merge(
|
||||
$this->morphableEagerLoadCounts, $withCount
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify constraints on the query for a given morph type.
|
||||
*
|
||||
* @param array $callbacks
|
||||
* @return $this
|
||||
*/
|
||||
public function constrain(array $callbacks)
|
||||
{
|
||||
$this->morphableConstraints = array_merge(
|
||||
$this->morphableConstraints, $callbacks
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that soft deleted models should be included in the results.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withTrashed()
|
||||
{
|
||||
$callback = fn ($query) => $query->hasMacro('withTrashed') ? $query->withTrashed() : $query;
|
||||
|
||||
$this->macroBuffer[] = [
|
||||
'method' => 'when',
|
||||
'parameters' => [true, $callback],
|
||||
];
|
||||
|
||||
return $this->when(true, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that soft deleted models should not be included in the results.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutTrashed()
|
||||
{
|
||||
$callback = fn ($query) => $query->hasMacro('withoutTrashed') ? $query->withoutTrashed() : $query;
|
||||
|
||||
$this->macroBuffer[] = [
|
||||
'method' => 'when',
|
||||
'parameters' => [true, $callback],
|
||||
];
|
||||
|
||||
return $this->when(true, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that only soft deleted models should be included in the results.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyTrashed()
|
||||
{
|
||||
$callback = fn ($query) => $query->hasMacro('onlyTrashed') ? $query->onlyTrashed() : $query;
|
||||
|
||||
$this->macroBuffer[] = [
|
||||
'method' => 'when',
|
||||
'parameters' => [true, $callback],
|
||||
];
|
||||
|
||||
return $this->when(true, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay stored macro calls on the actual related instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
protected function replayMacros(Builder $query)
|
||||
{
|
||||
foreach ($this->macroBuffer as $macro) {
|
||||
$query->{$macro['method']}(...$macro['parameters']);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
#[\Override]
|
||||
public function getQualifiedOwnerKeyName()
|
||||
{
|
||||
if (is_null($this->ownerKey)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return parent::getQualifiedOwnerKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic method calls to the relationship.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
try {
|
||||
$result = parent::__call($method, $parameters);
|
||||
|
||||
if (in_array($method, ['select', 'selectRaw', 'selectSub', 'addSelect', 'withoutGlobalScopes'])) {
|
||||
$this->macroBuffer[] = compact('method', 'parameters');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If we tried to call a method that does not exist on the parent Builder instance,
|
||||
// we'll assume that we want to call a query macro (e.g. withTrashed) that only
|
||||
// exists on related models. We will just store the call and replay it later.
|
||||
catch (BadMethodCallException) {
|
||||
$this->macroBuffer[] = compact('method', 'parameters');
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, TDeclaringModel>
|
||||
*/
|
||||
class MorphToMany extends BelongsToMany
|
||||
{
|
||||
/**
|
||||
* The type of the polymorphic relation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* The class name of the morph type constraint.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphClass;
|
||||
|
||||
/**
|
||||
* Indicates if we are connecting the inverse of the relation.
|
||||
*
|
||||
* This primarily affects the morphClass constraint.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $inverse;
|
||||
|
||||
/**
|
||||
* Create a new morph to many relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $name
|
||||
* @param string $table
|
||||
* @param string $foreignPivotKey
|
||||
* @param string $relatedPivotKey
|
||||
* @param string $parentKey
|
||||
* @param string $relatedKey
|
||||
* @param string|null $relationName
|
||||
* @param bool $inverse
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
Builder $query,
|
||||
Model $parent,
|
||||
$name,
|
||||
$table,
|
||||
$foreignPivotKey,
|
||||
$relatedPivotKey,
|
||||
$parentKey,
|
||||
$relatedKey,
|
||||
$relationName = null,
|
||||
$inverse = false,
|
||||
) {
|
||||
$this->inverse = $inverse;
|
||||
$this->morphType = $name.'_type';
|
||||
$this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass();
|
||||
|
||||
parent::__construct(
|
||||
$query, $parent, $table, $foreignPivotKey,
|
||||
$relatedPivotKey, $parentKey, $relatedKey, $relationName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the where clause for the relation query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function addWhereConstraints()
|
||||
{
|
||||
parent::addWhereConstraints();
|
||||
|
||||
$this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addEagerConstraints(array $models)
|
||||
{
|
||||
parent::addEagerConstraints($models);
|
||||
|
||||
$this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new pivot attachment record.
|
||||
*
|
||||
* @param int $id
|
||||
* @param bool $timed
|
||||
* @return array
|
||||
*/
|
||||
protected function baseAttachRecord($id, $timed)
|
||||
{
|
||||
return Arr::add(
|
||||
parent::baseAttachRecord($id, $timed), $this->morphType, $this->morphClass
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where(
|
||||
$this->qualifyPivotColumn($this->morphType), $this->morphClass
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pivot models that are currently attached.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, \Illuminate\Database\Eloquent\Relations\Pivot|\Illuminate\Database\Eloquent\Relations\MorphPivot>
|
||||
*/
|
||||
protected function getCurrentlyAttachedPivots()
|
||||
{
|
||||
return parent::getCurrentlyAttachedPivots()->map(function ($record) {
|
||||
return $record instanceof MorphPivot
|
||||
? $record->setMorphType($this->morphType)
|
||||
->setMorphClass($this->morphClass)
|
||||
: $record;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query builder for the pivot table.
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function newPivotQuery()
|
||||
{
|
||||
return parent::newPivotQuery()->where($this->morphType, $this->morphClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$using = $this->using;
|
||||
|
||||
$attributes = array_merge([$this->morphType => $this->morphClass], $attributes);
|
||||
|
||||
$pivot = $using ? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists)
|
||||
: MorphPivot::fromAttributes($this->parent, $attributes, $this->table, $exists);
|
||||
|
||||
$pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)
|
||||
->setRelatedModel($this->related)
|
||||
->setMorphType($this->morphType)
|
||||
->setMorphClass($this->morphClass);
|
||||
|
||||
return $pivot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pivot columns for the relation.
|
||||
*
|
||||
* "pivot_" is prefixed at each column for easy removal later.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function aliasedPivotColumns()
|
||||
{
|
||||
$defaults = [$this->foreignPivotKey, $this->relatedPivotKey, $this->morphType];
|
||||
|
||||
return (new Collection(array_merge($defaults, $this->pivotColumns)))->map(function ($column) {
|
||||
return $this->qualifyPivotColumn($column).' as pivot_'.$column;
|
||||
})->unique()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key "type" name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphType()
|
||||
{
|
||||
return $this->morphType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully qualified morph type for the relation.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedMorphTypeName()
|
||||
{
|
||||
return $this->qualifyPivotColumn($this->morphType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name of the parent model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return $this->morphClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indicator for a reverse relationship.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getInverse()
|
||||
{
|
||||
return $this->inverse;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
|
||||
|
||||
class Pivot extends Model
|
||||
{
|
||||
use AsPivot;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array<string>|bool
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
+548
@@ -0,0 +1,548 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract;
|
||||
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\MultipleRecordsFoundException;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Support\Collection as BaseCollection;
|
||||
use Illuminate\Support\Traits\ForwardsCalls;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TResult
|
||||
*
|
||||
* @mixin \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
abstract class Relation implements BuilderContract
|
||||
{
|
||||
use ForwardsCalls, Macroable {
|
||||
Macroable::__call as macroCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Eloquent query builder instance.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* The parent model instance.
|
||||
*
|
||||
* @var TDeclaringModel
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* The related model instance.
|
||||
*
|
||||
* @var TRelatedModel
|
||||
*/
|
||||
protected $related;
|
||||
|
||||
/**
|
||||
* Indicates whether the eagerly loaded relation should implicitly return an empty collection.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $eagerKeysWereEmpty = false;
|
||||
|
||||
/**
|
||||
* Indicates if the relation is adding constraints.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $constraints = true;
|
||||
|
||||
/**
|
||||
* An array to map class names to their morph names in the database.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $morphMap = [];
|
||||
|
||||
/**
|
||||
* Prevents morph relationships without a morph map.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $requireMorphMap = false;
|
||||
|
||||
/**
|
||||
* The count of self joins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected static $selfJoinCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new relation instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Builder $query, Model $parent)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->parent = $parent;
|
||||
$this->related = $query->getModel();
|
||||
|
||||
$this->addConstraints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback with constraints disabled on the relation.
|
||||
*
|
||||
* @param \Closure $callback
|
||||
* @return mixed
|
||||
*/
|
||||
public static function noConstraints(Closure $callback)
|
||||
{
|
||||
$previous = static::$constraints;
|
||||
|
||||
static::$constraints = false;
|
||||
|
||||
// When resetting the relation where clause, we want to shift the first element
|
||||
// off of the bindings, leaving only the constraints that the developers put
|
||||
// as "extra" on the relationships, and not original relation constraints.
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
static::$constraints = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base constraints on the relation query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function addConstraints();
|
||||
|
||||
/**
|
||||
* Set the constraints for an eager load of the relation.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @return void
|
||||
*/
|
||||
abstract public function addEagerConstraints(array $models);
|
||||
|
||||
/**
|
||||
* Initialize the relation on a set of models.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @param string $relation
|
||||
* @return array<int, TDeclaringModel>
|
||||
*/
|
||||
abstract public function initRelation(array $models, $relation);
|
||||
|
||||
/**
|
||||
* Match the eagerly loaded results to their parents.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @param string $relation
|
||||
* @return array<int, TDeclaringModel>
|
||||
*/
|
||||
abstract public function match(array $models, EloquentCollection $results, $relation);
|
||||
|
||||
/**
|
||||
* Get the results of the relationship.
|
||||
*
|
||||
* @return TResult
|
||||
*/
|
||||
abstract public function getResults();
|
||||
|
||||
/**
|
||||
* Get the relationship for eager loading.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function getEager()
|
||||
{
|
||||
return $this->eagerKeysWereEmpty
|
||||
? $this->query->getModel()->newCollection()
|
||||
: $this->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get the first result if it's the sole matching record.
|
||||
*
|
||||
* @param array|string $columns
|
||||
* @return TRelatedModel
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
|
||||
* @throws \Illuminate\Database\MultipleRecordsFoundException
|
||||
*/
|
||||
public function sole($columns = ['*'])
|
||||
{
|
||||
$result = $this->take(2)->get($columns);
|
||||
|
||||
$count = $result->count();
|
||||
|
||||
if ($count === 0) {
|
||||
throw (new ModelNotFoundException)->setModel(get_class($this->related));
|
||||
}
|
||||
|
||||
if ($count > 1) {
|
||||
throw new MultipleRecordsFoundException($count);
|
||||
}
|
||||
|
||||
return $result->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query as a "select" statement.
|
||||
*
|
||||
* @param array $columns
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function get($columns = ['*'])
|
||||
{
|
||||
return $this->query->get($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch all of the related models for the relationship.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function touch()
|
||||
{
|
||||
$model = $this->getRelated();
|
||||
|
||||
if (! $model::isIgnoringTouch()) {
|
||||
$this->rawUpdate([
|
||||
$model->getUpdatedAtColumn() => $model->freshTimestampString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a raw update against the base query.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return int
|
||||
*/
|
||||
public function rawUpdate(array $attributes = [])
|
||||
{
|
||||
return $this->query->withoutGlobalScopes()->update($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the constraints for a relationship count query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getRelationExistenceCountQuery(Builder $query, Builder $parentQuery)
|
||||
{
|
||||
return $this->getRelationExistenceQuery(
|
||||
$query, $parentQuery, new Expression('count(*)')
|
||||
)->setBindings([], 'select');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the constraints for an internal relationship existence query.
|
||||
*
|
||||
* Essentially, these queries compare on column names like whereColumn.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
|
||||
* @param array|mixed $columns
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
return $query->select($columns)->whereColumn(
|
||||
$this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a relationship join table hash.
|
||||
*
|
||||
* @param bool $incrementJoinCount
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountHash($incrementJoinCount = true)
|
||||
{
|
||||
return 'laravel_reserved_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the primary keys for an array of models.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @param string|null $key
|
||||
* @return array<int, int|string|null>
|
||||
*/
|
||||
protected function getKeys(array $models, $key = null)
|
||||
{
|
||||
return (new BaseCollection($models))->map(function ($value) use ($key) {
|
||||
return $key ? $value->getAttribute($key) : $value->getKey();
|
||||
})->values()->unique(null, true)->sort()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query builder that will contain the relationship constraints.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
protected function getRelationQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying query for the relation.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base query builder driving the Eloquent builder.
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function getBaseQuery()
|
||||
{
|
||||
return $this->query->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a base query builder instance.
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function toBase()
|
||||
{
|
||||
return $this->query->toBase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent model of the relation.
|
||||
*
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully qualified parent key name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedParentKeyName()
|
||||
{
|
||||
return $this->parent->getQualifiedKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related model of the relation.
|
||||
*
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function getRelated()
|
||||
{
|
||||
return $this->related;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "created at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function createdAt()
|
||||
{
|
||||
return $this->parent->getCreatedAtColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "updated at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function updatedAt()
|
||||
{
|
||||
return $this->parent->getUpdatedAtColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the related model's "updated at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relatedUpdatedAt()
|
||||
{
|
||||
return $this->related->getUpdatedAtColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a whereIn eager constraint for the given set of model keys to be loaded.
|
||||
*
|
||||
* @param string $whereIn
|
||||
* @param string $key
|
||||
* @param array $modelKeys
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel>|null $query
|
||||
* @return void
|
||||
*/
|
||||
protected function whereInEager(string $whereIn, string $key, array $modelKeys, ?Builder $query = null)
|
||||
{
|
||||
($query ?? $this->query)->{$whereIn}($key, $modelKeys);
|
||||
|
||||
if ($modelKeys === []) {
|
||||
$this->eagerKeysWereEmpty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "where in" method for eager loading.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function whereInMethod(Model $model, $key)
|
||||
{
|
||||
return $model->getKeyName() === last(explode('.', $key))
|
||||
&& in_array($model->getKeyType(), ['int', 'integer'])
|
||||
? 'whereIntegerInRaw'
|
||||
: 'whereIn';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent polymorphic relationships from being used without model mappings.
|
||||
*
|
||||
* @param bool $requireMorphMap
|
||||
* @return void
|
||||
*/
|
||||
public static function requireMorphMap($requireMorphMap = true)
|
||||
{
|
||||
static::$requireMorphMap = $requireMorphMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if polymorphic relationships require explicit model mapping.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function requiresMorphMap()
|
||||
{
|
||||
return static::$requireMorphMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the morph map for polymorphic relations and require all morphed models to be explicitly mapped.
|
||||
*
|
||||
* @param array $map
|
||||
* @param bool $merge
|
||||
* @return array
|
||||
*/
|
||||
public static function enforceMorphMap(array $map, $merge = true)
|
||||
{
|
||||
static::requireMorphMap();
|
||||
|
||||
return static::morphMap($map, $merge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or get the morph map for polymorphic relations.
|
||||
*
|
||||
* @param array|null $map
|
||||
* @param bool $merge
|
||||
* @return array
|
||||
*/
|
||||
public static function morphMap(?array $map = null, $merge = true)
|
||||
{
|
||||
$map = static::buildMorphMapFromModels($map);
|
||||
|
||||
if (is_array($map)) {
|
||||
static::$morphMap = $merge && static::$morphMap
|
||||
? $map + static::$morphMap : $map;
|
||||
}
|
||||
|
||||
return static::$morphMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a table-keyed array from model class names.
|
||||
*
|
||||
* @param string[]|null $models
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function buildMorphMapFromModels(?array $models = null)
|
||||
{
|
||||
if (is_null($models) || ! array_is_list($models)) {
|
||||
return $models;
|
||||
}
|
||||
|
||||
return array_combine(array_map(function ($model) {
|
||||
return (new $model)->getTable();
|
||||
}, $models), $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model associated with a custom polymorphic type.
|
||||
*
|
||||
* @param string $alias
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getMorphedModel($alias)
|
||||
{
|
||||
return static::$morphMap[$alias] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the alias associated with a custom polymorphic class.
|
||||
*
|
||||
* @param string $className
|
||||
* @return int|string
|
||||
*/
|
||||
public static function getMorphAlias(string $className)
|
||||
{
|
||||
return array_search($className, static::$morphMap, strict: true) ?: $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic method calls to the relationship.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (static::hasMacro($method)) {
|
||||
return $this->macroCall($method, $parameters);
|
||||
}
|
||||
|
||||
return $this->forwardDecoratedCallTo($this->query, $method, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a clone of the underlying query builder when cloning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->query = clone $this->query;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
interface Scope
|
||||
{
|
||||
/**
|
||||
* Apply the scope to a given Eloquent query builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $builder
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return void
|
||||
*/
|
||||
public function apply(Builder $builder, Model $model);
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection as BaseCollection;
|
||||
|
||||
/**
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static> withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static> onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static> withoutTrashed()
|
||||
* @method static static restoreOrCreate(array<string, mixed> $attributes = [], array<string, mixed> $values = [])
|
||||
* @method static static createOrRestore(array<string, mixed> $attributes = [], array<string, mixed> $values = [])
|
||||
*/
|
||||
trait SoftDeletes
|
||||
{
|
||||
/**
|
||||
* Indicates if the model is currently force deleting.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $forceDeleting = false;
|
||||
|
||||
/**
|
||||
* Boot the soft deleting trait for a model.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function bootSoftDeletes()
|
||||
{
|
||||
static::addGlobalScope(new SoftDeletingScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the soft deleting trait for an instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initializeSoftDeletes()
|
||||
{
|
||||
if (! isset($this->casts[$this->getDeletedAtColumn()])) {
|
||||
$this->casts[$this->getDeletedAtColumn()] = 'datetime';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a hard delete on a soft deleted model.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function forceDelete()
|
||||
{
|
||||
if ($this->fireModelEvent('forceDeleting') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->forceDeleting = true;
|
||||
|
||||
return tap($this->delete(), function ($deleted) {
|
||||
$this->forceDeleting = false;
|
||||
|
||||
if ($deleted) {
|
||||
$this->fireModelEvent('forceDeleted', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a hard delete on a soft deleted model without raising any events.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function forceDeleteQuietly()
|
||||
{
|
||||
return static::withoutEvents(fn () => $this->forceDelete());
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the models for the given IDs.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|array|int|string $ids
|
||||
* @return int
|
||||
*/
|
||||
public static function forceDestroy($ids)
|
||||
{
|
||||
if ($ids instanceof EloquentCollection) {
|
||||
$ids = $ids->modelKeys();
|
||||
}
|
||||
|
||||
if ($ids instanceof BaseCollection) {
|
||||
$ids = $ids->all();
|
||||
}
|
||||
|
||||
$ids = is_array($ids) ? $ids : func_get_args();
|
||||
|
||||
if (count($ids) === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We will actually pull the models from the database table and call delete on
|
||||
// each of them individually so that their events get fired properly with a
|
||||
// correct set of attributes in case the developers wants to check these.
|
||||
$key = ($instance = new static)->getKeyName();
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($instance->withTrashed()->whereIn($key, $ids)->get() as $model) {
|
||||
if ($model->forceDelete()) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual delete query on this model instance.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function performDeleteOnModel()
|
||||
{
|
||||
if ($this->forceDeleting) {
|
||||
return tap($this->setKeysForSaveQuery($this->newModelQuery())->forceDelete(), function () {
|
||||
$this->exists = false;
|
||||
});
|
||||
}
|
||||
|
||||
return $this->runSoftDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual delete query on this model instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function runSoftDelete()
|
||||
{
|
||||
$query = $this->setKeysForSaveQuery($this->newModelQuery());
|
||||
|
||||
$time = $this->freshTimestamp();
|
||||
|
||||
$columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];
|
||||
|
||||
$this->{$this->getDeletedAtColumn()} = $time;
|
||||
|
||||
if ($this->usesTimestamps() && ! is_null($this->getUpdatedAtColumn())) {
|
||||
$this->{$this->getUpdatedAtColumn()} = $time;
|
||||
|
||||
$columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
|
||||
}
|
||||
|
||||
$query->update($columns);
|
||||
|
||||
$this->syncOriginalAttributes(array_keys($columns));
|
||||
|
||||
$this->fireModelEvent('trashed', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a soft-deleted model instance.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function restore()
|
||||
{
|
||||
// If the restoring event does not return false, we will proceed with this
|
||||
// restore operation. Otherwise, we bail out so the developer will stop
|
||||
// the restore totally. We will clear the deleted timestamp and save.
|
||||
if ($this->fireModelEvent('restoring') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->{$this->getDeletedAtColumn()} = null;
|
||||
|
||||
// Once we have saved the model, we will fire the "restored" event so this
|
||||
// developer will do anything they need to after a restore operation is
|
||||
// totally finished. Then we will return the result of the save call.
|
||||
$this->exists = true;
|
||||
|
||||
$result = $this->save();
|
||||
|
||||
$this->fireModelEvent('restored', false);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a soft-deleted model instance without raising any events.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function restoreQuietly()
|
||||
{
|
||||
return static::withoutEvents(fn () => $this->restore());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model instance has been soft-deleted.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function trashed()
|
||||
{
|
||||
return ! is_null($this->{$this->getDeletedAtColumn()});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "softDeleted" model event callback with the dispatcher.
|
||||
*
|
||||
* @param \Illuminate\Events\QueuedClosure|callable|class-string $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function softDeleted($callback)
|
||||
{
|
||||
static::registerModelEvent('trashed', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "restoring" model event callback with the dispatcher.
|
||||
*
|
||||
* @param \Illuminate\Events\QueuedClosure|callable|class-string $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function restoring($callback)
|
||||
{
|
||||
static::registerModelEvent('restoring', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "restored" model event callback with the dispatcher.
|
||||
*
|
||||
* @param \Illuminate\Events\QueuedClosure|callable|class-string $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function restored($callback)
|
||||
{
|
||||
static::registerModelEvent('restored', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "forceDeleting" model event callback with the dispatcher.
|
||||
*
|
||||
* @param \Illuminate\Events\QueuedClosure|callable|class-string $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function forceDeleting($callback)
|
||||
{
|
||||
static::registerModelEvent('forceDeleting', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "forceDeleted" model event callback with the dispatcher.
|
||||
*
|
||||
* @param \Illuminate\Events\QueuedClosure|callable|class-string $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function forceDeleted($callback)
|
||||
{
|
||||
static::registerModelEvent('forceDeleted', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model is currently force deleting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isForceDeleting()
|
||||
{
|
||||
return $this->forceDeleting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "deleted at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDeletedAtColumn()
|
||||
{
|
||||
return defined(static::class.'::DELETED_AT') ? static::DELETED_AT : 'deleted_at';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully qualified "deleted at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedDeletedAtColumn()
|
||||
{
|
||||
return $this->qualifyColumn($this->getDeletedAtColumn());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent;
|
||||
|
||||
class SoftDeletingScope implements Scope
|
||||
{
|
||||
/**
|
||||
* All of the extensions to be added to the builder.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $extensions = ['Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed'];
|
||||
|
||||
/**
|
||||
* Apply the scope to a given Eloquent query builder.
|
||||
*
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TModel> $builder
|
||||
* @param TModel $model
|
||||
* @return void
|
||||
*/
|
||||
public function apply(Builder $builder, Model $model)
|
||||
{
|
||||
$builder->whereNull($model->getQualifiedDeletedAtColumn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the query builder with the needed functions.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
public function extend(Builder $builder)
|
||||
{
|
||||
foreach ($this->extensions as $extension) {
|
||||
$this->{"add{$extension}"}($builder);
|
||||
}
|
||||
|
||||
$builder->onDelete(function (Builder $builder) {
|
||||
$column = $this->getDeletedAtColumn($builder);
|
||||
|
||||
return $builder->update([
|
||||
$column => $builder->getModel()->freshTimestampString(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "deleted at" column for the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return string
|
||||
*/
|
||||
protected function getDeletedAtColumn(Builder $builder)
|
||||
{
|
||||
if (count((array) $builder->getQuery()->joins) > 0) {
|
||||
return $builder->getModel()->getQualifiedDeletedAtColumn();
|
||||
}
|
||||
|
||||
return $builder->getModel()->getDeletedAtColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the restore extension to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function addRestore(Builder $builder)
|
||||
{
|
||||
$builder->macro('restore', function (Builder $builder) {
|
||||
$builder->withTrashed();
|
||||
|
||||
return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the restore-or-create extension to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function addRestoreOrCreate(Builder $builder)
|
||||
{
|
||||
$builder->macro('restoreOrCreate', function (Builder $builder, array $attributes = [], array $values = []) {
|
||||
$builder->withTrashed();
|
||||
|
||||
return tap($builder->firstOrCreate($attributes, $values), function ($instance) {
|
||||
$instance->restore();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the create-or-restore extension to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function addCreateOrRestore(Builder $builder)
|
||||
{
|
||||
$builder->macro('createOrRestore', function (Builder $builder, array $attributes = [], array $values = []) {
|
||||
$builder->withTrashed();
|
||||
|
||||
return tap($builder->createOrFirst($attributes, $values), function ($instance) {
|
||||
$instance->restore();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the with-trashed extension to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function addWithTrashed(Builder $builder)
|
||||
{
|
||||
$builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) {
|
||||
if (! $withTrashed) {
|
||||
return $builder->withoutTrashed();
|
||||
}
|
||||
|
||||
return $builder->withoutGlobalScope($this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the without-trashed extension to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function addWithoutTrashed(Builder $builder)
|
||||
{
|
||||
$builder->macro('withoutTrashed', function (Builder $builder) {
|
||||
$model = $builder->getModel();
|
||||
|
||||
$builder->withoutGlobalScope($this)->whereNull(
|
||||
$model->getQualifiedDeletedAtColumn()
|
||||
);
|
||||
|
||||
return $builder;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the only-trashed extension to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function addOnlyTrashed(Builder $builder)
|
||||
{
|
||||
$builder->macro('onlyTrashed', function (Builder $builder) {
|
||||
$model = $builder->getModel();
|
||||
|
||||
$builder->withoutGlobalScope($this)->whereNotNull(
|
||||
$model->getQualifiedDeletedAtColumn()
|
||||
);
|
||||
|
||||
return $builder;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user