暂存
This commit is contained in:
+384
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
|
||||
|
||||
use function Illuminate\Support\enum_value;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TDeclaringModel, ?TRelatedModel>
|
||||
*/
|
||||
class BelongsTo extends Relation
|
||||
{
|
||||
use ComparesRelatedModels,
|
||||
InteractsWithDictionary,
|
||||
SupportsDefaultModels;
|
||||
|
||||
/**
|
||||
* The child model instance of the relation.
|
||||
*
|
||||
* @var TDeclaringModel
|
||||
*/
|
||||
protected $child;
|
||||
|
||||
/**
|
||||
* The foreign key of the parent model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $foreignKey;
|
||||
|
||||
/**
|
||||
* The associated key on the parent model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $ownerKey;
|
||||
|
||||
/**
|
||||
* The name of the relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relationName;
|
||||
|
||||
/**
|
||||
* Create a new belongs to relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $child
|
||||
* @param string $foreignKey
|
||||
* @param string $ownerKey
|
||||
* @param string $relationName
|
||||
*/
|
||||
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)
|
||||
{
|
||||
$this->ownerKey = $ownerKey;
|
||||
$this->relationName = $relationName;
|
||||
$this->foreignKey = $foreignKey;
|
||||
|
||||
// In the underlying base relationship class, this variable is referred to as
|
||||
// the "parent" since most relationships are not inversed. But, since this
|
||||
// one is we will create a "child" variable for much better readability.
|
||||
$this->child = $child;
|
||||
|
||||
parent::__construct($query, $child);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getResults()
|
||||
{
|
||||
if (is_null($this->getForeignKeyFrom($this->child))) {
|
||||
return $this->getDefaultFor($this->parent);
|
||||
}
|
||||
|
||||
return $this->query->first() ?: $this->getDefaultFor($this->parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base constraints on the relation query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addConstraints()
|
||||
{
|
||||
if (static::$constraints) {
|
||||
// For belongs to relationships, which are essentially the inverse of has one
|
||||
// or has many relationships, we need to actually query on the primary key
|
||||
// of the related models matching on the foreign key that's on a parent.
|
||||
$key = $this->getQualifiedOwnerKeyName();
|
||||
|
||||
$this->query->where($key, '=', $this->getForeignKeyFrom($this->child));
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addEagerConstraints(array $models)
|
||||
{
|
||||
// We'll grab the primary key name of the related models since it could be set to
|
||||
// a non-standard name and not "id". We will then construct the constraint for
|
||||
// our eagerly loading query so it returns the proper models from execution.
|
||||
$key = $this->getQualifiedOwnerKeyName();
|
||||
|
||||
$whereIn = $this->whereInMethod($this->related, $this->ownerKey);
|
||||
|
||||
$this->whereInEager($whereIn, $key, $this->getEagerModelKeys($models));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather the keys from an array of related models.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @return array
|
||||
*/
|
||||
protected function getEagerModelKeys(array $models)
|
||||
{
|
||||
$keys = [];
|
||||
|
||||
// First we need to gather all of the keys from the parent models so we know what
|
||||
// to query for via the eager loading query. We will add them to an array then
|
||||
// execute a "where in" statement to gather up all of those related records.
|
||||
foreach ($models as $model) {
|
||||
if (! is_null($value = $this->getForeignKeyFrom($model))) {
|
||||
$keys[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
sort($keys);
|
||||
|
||||
return array_values(array_unique($keys));
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function initRelation(array $models, $relation)
|
||||
{
|
||||
foreach ($models as $model) {
|
||||
$model->setRelation($relation, $this->getDefaultFor($model));
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function match(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
// First we will get to build a dictionary of the child models by their primary
|
||||
// key of the relationship, then we can easily match the children back onto
|
||||
// the parents using that dictionary and the primary key of the children.
|
||||
$dictionary = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
$attribute = $this->getDictionaryKey($this->getRelatedKeyFrom($result));
|
||||
|
||||
if ($attribute !== null) {
|
||||
$dictionary[$attribute] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Once we have the dictionary constructed, we can loop through all the parents
|
||||
// and match back onto their children using these keys of the dictionary and
|
||||
// the primary key of the children to map them onto the correct instances.
|
||||
foreach ($models as $model) {
|
||||
$attribute = $this->getDictionaryKey($this->getForeignKeyFrom($model));
|
||||
|
||||
if ($attribute !== null && isset($dictionary[$attribute])) {
|
||||
$model->setRelation($relation, $dictionary[$attribute]);
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the model instance to the given parent.
|
||||
*
|
||||
* @param TRelatedModel|int|string|null $model
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
public function associate($model)
|
||||
{
|
||||
$ownerKey = $model instanceof Model ? $model->getAttribute($this->ownerKey) : $model;
|
||||
|
||||
$this->child->setAttribute($this->foreignKey, $ownerKey);
|
||||
|
||||
if ($model instanceof Model) {
|
||||
$this->child->setRelation($this->relationName, $model);
|
||||
} else {
|
||||
$this->child->unsetRelation($this->relationName);
|
||||
}
|
||||
|
||||
return $this->child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dissociate previously associated model from the given parent.
|
||||
*
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
public function dissociate()
|
||||
{
|
||||
$this->child->setAttribute($this->foreignKey, null);
|
||||
|
||||
return $this->child->setRelation($this->relationName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of "dissociate" method.
|
||||
*
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
public function disassociate()
|
||||
{
|
||||
return $this->dissociate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch all of the related models for the relationship.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function touch()
|
||||
{
|
||||
if (! is_null($this->getParentKey())) {
|
||||
parent::touch();
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
if ($parentQuery->getQuery()->from == $query->getQuery()->from) {
|
||||
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
return $query->select($columns)->whereColumn(
|
||||
$this->getQualifiedForeignKeyName(), '=', $query->qualifyColumn($this->ownerKey)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the constraints for a relationship query on the same table.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
|
||||
* @param mixed $columns
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
$query->select($columns)->from(
|
||||
$query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()
|
||||
);
|
||||
|
||||
$query->getModel()->setTable($hash);
|
||||
|
||||
return $query->whereColumn(
|
||||
$hash.'.'.$this->ownerKey, '=', $this->getQualifiedForeignKeyName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the related model has an auto-incrementing ID.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function relationHasIncrementingId()
|
||||
{
|
||||
return $this->related->getIncrementing() &&
|
||||
in_array($this->related->getKeyType(), ['int', 'integer']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new related instance for the given model.
|
||||
*
|
||||
* @param TDeclaringModel $parent
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
protected function newRelatedInstanceFor(Model $parent)
|
||||
{
|
||||
return $this->related->newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child of the relationship.
|
||||
*
|
||||
* @return TDeclaringModel
|
||||
*/
|
||||
public function getChild()
|
||||
{
|
||||
return $this->child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key of the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKeyName()
|
||||
{
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified foreign key of the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedForeignKeyName()
|
||||
{
|
||||
return $this->child->qualifyColumn($this->foreignKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key value of the child's foreign key.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getParentKey()
|
||||
{
|
||||
return $this->getForeignKeyFrom($this->child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated key of the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOwnerKeyName()
|
||||
{
|
||||
return $this->ownerKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified associated key of the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedOwnerKeyName()
|
||||
{
|
||||
return $this->related->qualifyColumn($this->ownerKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the model's foreign key.
|
||||
*
|
||||
* @param TRelatedModel $model
|
||||
* @return int|string
|
||||
*/
|
||||
protected function getRelatedKeyFrom(Model $model)
|
||||
{
|
||||
return $model->{$this->ownerKey};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the model's foreign key.
|
||||
*
|
||||
* @param TDeclaringModel $model
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getForeignKeyFrom(Model $model)
|
||||
{
|
||||
$foreignKey = $model->{$this->foreignKey};
|
||||
|
||||
return enum_value($foreignKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationName()
|
||||
{
|
||||
return $this->relationName;
|
||||
}
|
||||
}
|
||||
+1712
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait AsPivot
|
||||
{
|
||||
/**
|
||||
* The parent model of the relationship.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public $pivotParent;
|
||||
|
||||
/**
|
||||
* The related model of the relationship.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public $pivotRelated;
|
||||
|
||||
/**
|
||||
* The name of the foreign key column.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $foreignKey;
|
||||
|
||||
/**
|
||||
* The name of the "other key" column.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relatedKey;
|
||||
|
||||
/**
|
||||
* Create a new pivot model instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param array $attributes
|
||||
* @param string $table
|
||||
* @param bool $exists
|
||||
* @return static
|
||||
*/
|
||||
public static function fromAttributes(Model $parent, $attributes, $table, $exists = false)
|
||||
{
|
||||
$instance = new static;
|
||||
|
||||
$instance->timestamps = $instance->hasTimestampAttributes($attributes);
|
||||
|
||||
// The pivot model is a "dynamic" model since we will set the tables dynamically
|
||||
// for the instance. This allows it work for any intermediate tables for the
|
||||
// many to many relationship that are defined by this developer's classes.
|
||||
$instance->setConnection($parent->getConnectionName())
|
||||
->setTable($table)
|
||||
->forceFill($attributes)
|
||||
->syncOriginal();
|
||||
|
||||
// We store off the parent instance so we will access the timestamp column names
|
||||
// for the model, since the pivot model timestamps aren't easily configurable
|
||||
// from the developer's point of view. We can use the parents to get these.
|
||||
$instance->pivotParent = $parent;
|
||||
|
||||
$instance->exists = $exists;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new pivot model from raw values returned from a query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param array $attributes
|
||||
* @param string $table
|
||||
* @param bool $exists
|
||||
* @return static
|
||||
*/
|
||||
public static function fromRawAttributes(Model $parent, $attributes, $table, $exists = false)
|
||||
{
|
||||
$instance = static::fromAttributes($parent, [], $table, $exists);
|
||||
|
||||
$instance->timestamps = $instance->hasTimestampAttributes($attributes);
|
||||
|
||||
$instance->setRawAttributes(
|
||||
array_merge($instance->getRawOriginal(), $attributes), $exists
|
||||
);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the keys for a select query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<static> $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function setKeysForSelectQuery($query)
|
||||
{
|
||||
if (isset($this->attributes[$this->getKeyName()])) {
|
||||
return parent::setKeysForSelectQuery($query);
|
||||
}
|
||||
|
||||
$query->where($this->foreignKey, $this->getOriginal(
|
||||
$this->foreignKey, $this->getAttribute($this->foreignKey)
|
||||
));
|
||||
|
||||
return $query->where($this->relatedKey, $this->getOriginal(
|
||||
$this->relatedKey, $this->getAttribute($this->relatedKey)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the keys for a save update query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<static> $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function setKeysForSaveQuery($query)
|
||||
{
|
||||
return $this->setKeysForSelectQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the pivot model record from the database.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if (isset($this->attributes[$this->getKeyName()])) {
|
||||
return (int) parent::delete();
|
||||
}
|
||||
|
||||
if ($this->fireModelEvent('deleting') === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->touchOwners();
|
||||
|
||||
return tap($this->getDeleteQuery()->delete(), function () {
|
||||
$this->exists = false;
|
||||
|
||||
$this->fireModelEvent('deleted', false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query builder for a delete operation on the pivot.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function getDeleteQuery()
|
||||
{
|
||||
return $this->newQueryWithoutRelationships()->where([
|
||||
$this->foreignKey => $this->getOriginal($this->foreignKey, $this->getAttribute($this->foreignKey)),
|
||||
$this->relatedKey => $this->getOriginal($this->relatedKey, $this->getAttribute($this->relatedKey)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table associated with the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTable()
|
||||
{
|
||||
if (! isset($this->table) && (! $this instanceof MorphPivot)) {
|
||||
$this->setTable(str_replace(
|
||||
'\\', '', Str::snake(Str::singular(class_basename($this)))
|
||||
));
|
||||
}
|
||||
|
||||
return parent::getTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key column name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKey()
|
||||
{
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "related key" column name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelatedKey()
|
||||
{
|
||||
return $this->relatedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "related key" column name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOtherKey()
|
||||
{
|
||||
return $this->getRelatedKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key names for the pivot model instance.
|
||||
*
|
||||
* @param string $foreignKey
|
||||
* @param string $relatedKey
|
||||
* @return $this
|
||||
*/
|
||||
public function setPivotKeys($foreignKey, $relatedKey)
|
||||
{
|
||||
$this->foreignKey = $foreignKey;
|
||||
|
||||
$this->relatedKey = $relatedKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the related model of the relationship.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|null $related
|
||||
* @return $this
|
||||
*/
|
||||
public function setRelatedModel(?Model $related = null)
|
||||
{
|
||||
$this->pivotRelated = $related;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the pivot model or given attributes has timestamp attributes.
|
||||
*
|
||||
* @param array|null $attributes
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTimestampAttributes($attributes = null)
|
||||
{
|
||||
return ($createdAt = $this->getCreatedAtColumn()) !== null
|
||||
&& array_key_exists($createdAt, $attributes ?? $this->attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "created at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCreatedAtColumn()
|
||||
{
|
||||
return $this->pivotParent
|
||||
? $this->pivotParent->getCreatedAtColumn()
|
||||
: parent::getCreatedAtColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "updated at" column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdatedAtColumn()
|
||||
{
|
||||
return $this->pivotParent
|
||||
? $this->pivotParent->getUpdatedAtColumn()
|
||||
: parent::getUpdatedAtColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the queueable identity for the entity.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getQueueableId()
|
||||
{
|
||||
if (isset($this->attributes[$this->getKeyName()])) {
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s:%s:%s:%s',
|
||||
$this->foreignKey, $this->getAttribute($this->foreignKey),
|
||||
$this->relatedKey, $this->getAttribute($this->relatedKey)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query to restore one or more models by their queueable IDs.
|
||||
*
|
||||
* @param int[]|string[]|string $ids
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
public function newQueryForRestoration($ids)
|
||||
{
|
||||
if (is_array($ids)) {
|
||||
return $this->newQueryForCollectionRestoration($ids);
|
||||
}
|
||||
|
||||
if (! str_contains($ids, ':')) {
|
||||
return parent::newQueryForRestoration($ids);
|
||||
}
|
||||
|
||||
$segments = explode(':', $ids);
|
||||
|
||||
return $this->newQueryWithoutScopes()
|
||||
->where($segments[0], $segments[1])
|
||||
->where($segments[2], $segments[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query to restore multiple models by their queueable IDs.
|
||||
*
|
||||
* @param int[]|string[] $ids
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function newQueryForCollectionRestoration(array $ids)
|
||||
{
|
||||
$ids = array_values($ids);
|
||||
|
||||
if (! str_contains($ids[0], ':')) {
|
||||
return parent::newQueryForRestoration($ids);
|
||||
}
|
||||
|
||||
$query = $this->newQueryWithoutScopes();
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$segments = explode(':', $id);
|
||||
|
||||
$query->orWhere(function ($query) use ($segments) {
|
||||
return $query->where($segments[0], $segments[1])
|
||||
->where($segments[2], $segments[3]);
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset all the loaded relations for the instance.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function unsetRelations()
|
||||
{
|
||||
$this->pivotParent = null;
|
||||
$this->pivotRelated = null;
|
||||
$this->relations = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use InvalidArgumentException;
|
||||
|
||||
trait CanBeOneOfMany
|
||||
{
|
||||
/**
|
||||
* Determines whether the relationship is one-of-many.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isOneOfMany = false;
|
||||
|
||||
/**
|
||||
* The name of the relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relationName;
|
||||
|
||||
/**
|
||||
* The one of many inner join subselect query builder instance.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Builder<*>|null
|
||||
*/
|
||||
protected $oneOfManySubQuery;
|
||||
|
||||
/**
|
||||
* Add constraints for inner join subselect for one of many relationships.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $query
|
||||
* @param string|null $column
|
||||
* @param string|null $aggregate
|
||||
* @return void
|
||||
*/
|
||||
abstract public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null);
|
||||
|
||||
/**
|
||||
* Get the columns the determine the relationship groups.
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
abstract public function getOneOfManySubQuerySelectColumns();
|
||||
|
||||
/**
|
||||
* Add join query constraints for one of many relationships.
|
||||
*
|
||||
* @param \Illuminate\Database\Query\JoinClause $join
|
||||
* @return void
|
||||
*/
|
||||
abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join);
|
||||
|
||||
/**
|
||||
* Indicate that the relation is a single result of a larger one-to-many relationship.
|
||||
*
|
||||
* @param string|array|null $column
|
||||
* @param string|\Closure|null $aggregate
|
||||
* @param string|null $relation
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null)
|
||||
{
|
||||
$this->isOneOfMany = true;
|
||||
|
||||
$this->relationName = $relation ?: $this->getDefaultOneOfManyJoinAlias(
|
||||
$this->guessRelationship()
|
||||
);
|
||||
|
||||
$keyName = $this->query->getModel()->getKeyName();
|
||||
|
||||
$columns = is_string($columns = $column) ? [
|
||||
$column => $aggregate,
|
||||
$keyName => $aggregate,
|
||||
] : $column;
|
||||
|
||||
if (! array_key_exists($keyName, $columns)) {
|
||||
$columns[$keyName] = 'MAX';
|
||||
}
|
||||
|
||||
if ($aggregate instanceof Closure) {
|
||||
$closure = $aggregate;
|
||||
}
|
||||
|
||||
foreach ($columns as $column => $aggregate) {
|
||||
if (! in_array(strtolower($aggregate), ['min', 'max'])) {
|
||||
throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX");
|
||||
}
|
||||
|
||||
$subQuery = $this->newOneOfManySubQuery(
|
||||
$this->getOneOfManySubQuerySelectColumns(),
|
||||
array_merge([$column], $previous['columns'] ?? []),
|
||||
$aggregate,
|
||||
);
|
||||
|
||||
if (isset($previous)) {
|
||||
$this->addOneOfManyJoinSubQuery(
|
||||
$subQuery,
|
||||
$previous['subQuery'],
|
||||
$previous['columns'],
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($closure)) {
|
||||
$closure($subQuery);
|
||||
}
|
||||
|
||||
if (! isset($previous)) {
|
||||
$this->oneOfManySubQuery = $subQuery;
|
||||
}
|
||||
|
||||
if (array_key_last($columns) == $column) {
|
||||
$this->addOneOfManyJoinSubQuery(
|
||||
$this->query,
|
||||
$subQuery,
|
||||
array_merge([$column], $previous['columns'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
$previous = [
|
||||
'subQuery' => $subQuery,
|
||||
'columns' => array_merge([$column], $previous['columns'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
$this->addConstraints();
|
||||
|
||||
$columns = $this->query->getQuery()->columns;
|
||||
|
||||
if (is_null($columns) || $columns === ['*']) {
|
||||
$this->select([$this->qualifyColumn('*')]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the relation is the latest single result of a larger one-to-many relationship.
|
||||
*
|
||||
* @param string|array|null $column
|
||||
* @param string|null $relation
|
||||
* @return $this
|
||||
*/
|
||||
public function latestOfMany($column = 'id', $relation = null)
|
||||
{
|
||||
return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) {
|
||||
return [$column => 'MAX'];
|
||||
})->all(), 'MAX', $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the relation is the oldest single result of a larger one-to-many relationship.
|
||||
*
|
||||
* @param string|array|null $column
|
||||
* @param string|null $relation
|
||||
* @return $this
|
||||
*/
|
||||
public function oldestOfMany($column = 'id', $relation = null)
|
||||
{
|
||||
return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) {
|
||||
return [$column => 'MIN'];
|
||||
})->all(), 'MIN', $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default alias for the one of many inner join clause.
|
||||
*
|
||||
* @param string $relation
|
||||
* @return string
|
||||
*/
|
||||
protected function getDefaultOneOfManyJoinAlias($relation)
|
||||
{
|
||||
return $relation == $this->query->getModel()->getTable()
|
||||
? $relation.'_of_many'
|
||||
: $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship.
|
||||
*
|
||||
* @param string|array $groupBy
|
||||
* @param array<string>|null $columns
|
||||
* @param string|null $aggregate
|
||||
* @return \Illuminate\Database\Eloquent\Builder<*>
|
||||
*/
|
||||
protected function newOneOfManySubQuery($groupBy, $columns = null, $aggregate = null)
|
||||
{
|
||||
$subQuery = $this->query->getModel()
|
||||
->newQuery()
|
||||
->withoutGlobalScopes($this->removedScopes());
|
||||
|
||||
foreach (Arr::wrap($groupBy) as $group) {
|
||||
$subQuery->groupBy($this->qualifyRelatedColumn($group));
|
||||
}
|
||||
|
||||
if (! is_null($columns)) {
|
||||
foreach ($columns as $key => $column) {
|
||||
$aggregatedColumn = $subQuery->getQuery()->grammar->wrap($subQuery->qualifyColumn($column));
|
||||
|
||||
if ($key === 0) {
|
||||
$aggregatedColumn = "{$aggregate}({$aggregatedColumn})";
|
||||
} else {
|
||||
$aggregatedColumn = "min({$aggregatedColumn})";
|
||||
}
|
||||
|
||||
$subQuery->selectRaw($aggregatedColumn.' as '.$subQuery->getQuery()->grammar->wrap($column.'_aggregate'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->addOneOfManySubQueryConstraints($subQuery, column: null, aggregate: $aggregate);
|
||||
|
||||
return $subQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the join subquery to the given query on the given column and the relationship's foreign key.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $parent
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $subQuery
|
||||
* @param array<string> $on
|
||||
* @return void
|
||||
*/
|
||||
protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on)
|
||||
{
|
||||
$parent->beforeQuery(function ($parent) use ($subQuery, $on) {
|
||||
$subQuery->applyBeforeQueryCallbacks();
|
||||
|
||||
$parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) {
|
||||
foreach ($on as $onColumn) {
|
||||
$join->on($this->qualifySubSelectColumn($onColumn.'_aggregate'), '=', $this->qualifyRelatedColumn($onColumn));
|
||||
}
|
||||
|
||||
$this->addOneOfManyJoinSubQueryConstraints($join);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the relationship query joins to the given query builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*> $query
|
||||
* @return void
|
||||
*/
|
||||
protected function mergeOneOfManyJoinsTo(Builder $query)
|
||||
{
|
||||
$query->getQuery()->beforeQueryCallbacks = $this->query->getQuery()->beforeQueryCallbacks;
|
||||
|
||||
$query->applyBeforeQueryCallbacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query builder that will contain the relationship constraints.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder<*>
|
||||
*/
|
||||
protected function getRelationQuery()
|
||||
{
|
||||
return $this->isOneOfMany()
|
||||
? $this->oneOfManySubQuery
|
||||
: $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the one of many inner join subselect builder instance.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder<*>|void
|
||||
*/
|
||||
public function getOneOfManySubQuery()
|
||||
{
|
||||
return $this->oneOfManySubQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the qualified column name for the one-of-many relationship using the subselect join query's alias.
|
||||
*
|
||||
* @param string $column
|
||||
* @return string
|
||||
*/
|
||||
public function qualifySubSelectColumn($column)
|
||||
{
|
||||
return $this->getRelationName().'.'.last(explode('.', $column));
|
||||
}
|
||||
|
||||
/**
|
||||
* Qualify related column using the related table name if it is not already qualified.
|
||||
*
|
||||
* @param string $column
|
||||
* @return string
|
||||
*/
|
||||
protected function qualifyRelatedColumn($column)
|
||||
{
|
||||
return $this->query->getModel()->qualifyColumn($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the "hasOne" relationship's name via backtrace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function guessRelationship()
|
||||
{
|
||||
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the relationship is a one-of-many relationship.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isOneOfMany()
|
||||
{
|
||||
return $this->isOneOfMany;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationName()
|
||||
{
|
||||
return $this->relationName;
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations\Concerns;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use UnitEnum;
|
||||
|
||||
use function Illuminate\Support\enum_value;
|
||||
|
||||
trait InteractsWithDictionary
|
||||
{
|
||||
/**
|
||||
* Get a dictionary key attribute - casting it to a string if necessary.
|
||||
*
|
||||
* @param mixed $attribute
|
||||
* @return string|int|null
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function getDictionaryKey($attribute)
|
||||
{
|
||||
if (is_null($attribute) || is_string($attribute) || is_int($attribute)) {
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
if (is_object($attribute)) {
|
||||
if (method_exists($attribute, '__toString')) {
|
||||
return $attribute->__toString();
|
||||
}
|
||||
|
||||
if ($attribute instanceof UnitEnum) {
|
||||
return enum_value($attribute);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Model attribute value is an object but does not have a __toString method.');
|
||||
}
|
||||
|
||||
return (string) $attribute;
|
||||
}
|
||||
}
|
||||
+803
@@ -0,0 +1,803 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations\Concerns;
|
||||
|
||||
use BackedEnum;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Support\Collection as BaseCollection;
|
||||
|
||||
trait InteractsWithPivotTable
|
||||
{
|
||||
/**
|
||||
* Toggles a model (or models) from the parent.
|
||||
*
|
||||
* Each existing model is detached, and non existing ones are attached.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param bool $touch
|
||||
* @return array
|
||||
*/
|
||||
public function toggle($ids, $touch = true)
|
||||
{
|
||||
$changes = [
|
||||
'attached' => [], 'detached' => [],
|
||||
];
|
||||
|
||||
$records = $this->formatRecordsList($this->parseIds($ids));
|
||||
|
||||
// Next, we will determine which IDs should get removed from the join table by
|
||||
// checking which of the given ID/records is in the list of current records
|
||||
// and removing all of those rows from this "intermediate" joining table.
|
||||
$detach = array_values(array_intersect(
|
||||
$this->newPivotQuery()->pluck($this->relatedPivotKey)->all(),
|
||||
array_keys($records)
|
||||
));
|
||||
|
||||
if ($detach !== []) {
|
||||
$this->detach($detach, false);
|
||||
|
||||
$changes['detached'] = $this->castKeys($detach);
|
||||
}
|
||||
|
||||
// Finally, for all of the records which were not "detached", we'll attach the
|
||||
// records into the intermediate table. Then, we will add those attaches to
|
||||
// this change list and get ready to return these results to the callers.
|
||||
$attach = array_diff_key($records, array_flip($detach));
|
||||
|
||||
if ($attach !== []) {
|
||||
$this->attach($attach, [], false);
|
||||
|
||||
$changes['attached'] = array_keys($attach);
|
||||
}
|
||||
|
||||
// Once we have finished attaching or detaching the records, we will see if we
|
||||
// have done any attaching or detaching, and if we have we will touch these
|
||||
// relationships if they are configured to touch on any database updates.
|
||||
if ($touch && (count($changes['attached']) ||
|
||||
count($changes['detached']))) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a model (or models) from the parent within a transaction.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param bool $touch
|
||||
* @return array
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function toggleOrFail($ids, $touch = true)
|
||||
{
|
||||
return $this->parent->getConnection()->transaction(fn () => $this->toggle($ids, $touch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs without detaching.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*/
|
||||
public function syncWithoutDetaching($ids)
|
||||
{
|
||||
return $this->sync($ids, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs or collection of models.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
|
||||
* @param bool $detaching
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*/
|
||||
public function sync($ids, $detaching = true)
|
||||
{
|
||||
$changes = [
|
||||
'attached' => [], 'detached' => [], 'updated' => [],
|
||||
];
|
||||
|
||||
$records = $this->formatRecordsList($this->parseIds($ids));
|
||||
|
||||
if (empty($records) && ! $detaching) {
|
||||
return $changes;
|
||||
}
|
||||
|
||||
// First we need to attach any of the associated models that are not currently
|
||||
// in this joining table. We'll spin through the given IDs, checking to see
|
||||
// if they exist in the array of current ones, and if not we will insert.
|
||||
$current = $this->getCurrentlyAttachedPivots()
|
||||
->pluck($this->relatedPivotKey)->all();
|
||||
|
||||
// Next, we will take the differences of the currents and given IDs and detach
|
||||
// all of the entities that exist in the "current" array but are not in the
|
||||
// array of the new IDs given to the method which will complete the sync.
|
||||
if ($detaching) {
|
||||
$detach = array_diff($current, array_keys($records));
|
||||
|
||||
if ($detach !== []) {
|
||||
$this->detach($detach, false);
|
||||
|
||||
$changes['detached'] = $this->castKeys($detach);
|
||||
}
|
||||
}
|
||||
|
||||
// Now we are finally ready to attach the new records. Note that we'll disable
|
||||
// touching until after the entire operation is complete so we don't fire a
|
||||
// ton of touch operations until we are totally done syncing the records.
|
||||
$changes = array_merge(
|
||||
$changes, $this->attachNew($records, $current, false)
|
||||
);
|
||||
|
||||
// Once we have finished attaching or detaching the records, we will see if we
|
||||
// have done any attaching or detaching, and if we have we will touch these
|
||||
// relationships if they are configured to touch on any database updates.
|
||||
if (count($changes['attached']) ||
|
||||
count($changes['updated']) ||
|
||||
count($changes['detached'])) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs or collection of models within a transaction.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
|
||||
* @param bool $detaching
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function syncOrFail($ids, $detaching = true)
|
||||
{
|
||||
return $this->parent->getConnection()->transaction(fn () => $this->sync($ids, $detaching));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs without detaching within a transaction.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function syncWithoutDetachingOrFail($ids)
|
||||
{
|
||||
return $this->syncOrFail($ids, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs or collection of models with the given pivot values.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
|
||||
* @param array $values
|
||||
* @param bool $detaching
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*/
|
||||
public function syncWithPivotValues($ids, array $values, bool $detaching = true)
|
||||
{
|
||||
return $this->sync((new BaseCollection($this->parseIds($ids)))->mapWithKeys(function ($id) use ($values) {
|
||||
return [$id => $values];
|
||||
}), $detaching);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the intermediate tables with a list of IDs with the given pivot values within a transaction.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids
|
||||
* @param array $values
|
||||
* @param bool $detaching
|
||||
* @return array{attached: array, detached: array, updated: array}
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function syncWithPivotValuesOrFail($ids, array $values, bool $detaching = true)
|
||||
{
|
||||
return $this->parent->getConnection()->transaction(fn () => $this->syncWithPivotValues($ids, $values, $detaching));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the sync / toggle record list so that it is keyed by ID.
|
||||
*
|
||||
* @param array $records
|
||||
* @return array
|
||||
*/
|
||||
protected function formatRecordsList(array $records)
|
||||
{
|
||||
return (new BaseCollection($records))->mapWithKeys(function ($attributes, $id) {
|
||||
if (! is_array($attributes)) {
|
||||
[$id, $attributes] = [$attributes, []];
|
||||
}
|
||||
|
||||
if ($id instanceof BackedEnum) {
|
||||
$id = $id->value;
|
||||
}
|
||||
|
||||
return [$id => $attributes];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach all of the records that aren't in the given current records.
|
||||
*
|
||||
* @param array $records
|
||||
* @param array $current
|
||||
* @param bool $touch
|
||||
* @return array
|
||||
*/
|
||||
protected function attachNew(array $records, array $current, $touch = true)
|
||||
{
|
||||
$changes = ['attached' => [], 'updated' => []];
|
||||
|
||||
foreach ($records as $id => $attributes) {
|
||||
// If the ID is not in the list of existing pivot IDs, we will insert a new pivot
|
||||
// record, otherwise, we will just update this existing record on this joining
|
||||
// table, so that the developers will easily update these records pain free.
|
||||
if (! in_array($id, $current)) {
|
||||
$this->attach($id, $attributes, $touch);
|
||||
|
||||
$changes['attached'][] = $this->castKey($id);
|
||||
}
|
||||
|
||||
// Now we'll try to update an existing pivot record with the attributes that were
|
||||
// given to the method. If the model is actually updated we will add it to the
|
||||
// list of updated pivot records so we return them back out to the consumer.
|
||||
elseif (count($attributes) > 0 &&
|
||||
$this->updateExistingPivot($id, $attributes, $touch)) {
|
||||
$changes['updated'][] = $this->castKey($id);
|
||||
}
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing pivot record on the table.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $attributes
|
||||
* @param bool $touch
|
||||
* @return int
|
||||
*/
|
||||
public function updateExistingPivot($id, array $attributes, $touch = true)
|
||||
{
|
||||
if ($this->using) {
|
||||
return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch);
|
||||
}
|
||||
|
||||
if ($this->hasPivotColumn($this->updatedAt())) {
|
||||
$attributes = $this->addTimestampsToAttachment($attributes, true);
|
||||
}
|
||||
|
||||
$updated = $this->newPivotStatementForId($id)->update(
|
||||
$this->castAttributes($attributes)
|
||||
);
|
||||
|
||||
if ($touch) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing pivot record on the table within a transaction.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $attributes
|
||||
* @param bool $touch
|
||||
* @return int
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function updateExistingPivotOrFail($id, array $attributes, $touch = true)
|
||||
{
|
||||
return $this->parent->getConnection()->transaction(fn () => $this->updateExistingPivot($id, $attributes, $touch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing pivot record on the table via a custom class.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $attributes
|
||||
* @param bool $touch
|
||||
* @return int
|
||||
*/
|
||||
protected function updateExistingPivotUsingCustomClass($id, array $attributes, $touch)
|
||||
{
|
||||
$pivot = $this->getCurrentlyAttachedPivotsForIds($id)->first();
|
||||
|
||||
$updated = $pivot ? $pivot->fill($attributes)->isDirty() : false;
|
||||
|
||||
if ($updated) {
|
||||
$pivot->save();
|
||||
}
|
||||
|
||||
if ($touch) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
|
||||
return (int) $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a model to the parent.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param array $attributes
|
||||
* @param bool $touch
|
||||
* @return void
|
||||
*/
|
||||
public function attach($ids, array $attributes = [], $touch = true)
|
||||
{
|
||||
if ($this->using) {
|
||||
$this->attachUsingCustomClass($ids, $attributes);
|
||||
} else {
|
||||
// Here we will insert the attachment records into the pivot table. Once we have
|
||||
// inserted the records, we will touch the relationships if necessary and the
|
||||
// function will return. We can parse the IDs before inserting the records.
|
||||
$this->newPivotStatement()->insert($this->formatAttachRecords(
|
||||
$this->parseIds($ids), $attributes
|
||||
));
|
||||
}
|
||||
|
||||
if ($touch) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a model to the parent within a transaction.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param array $attributes
|
||||
* @param bool $touch
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function attachOrFail($ids, array $attributes = [], $touch = true)
|
||||
{
|
||||
$this->parent->getConnection()->transaction(fn () => $this->attach($ids, $attributes, $touch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a model to the parent using a custom class.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param array $attributes
|
||||
* @return void
|
||||
*/
|
||||
protected function attachUsingCustomClass($ids, array $attributes)
|
||||
{
|
||||
$records = $this->formatAttachRecords(
|
||||
$this->parseIds($ids), $attributes
|
||||
);
|
||||
|
||||
foreach ($records as $record) {
|
||||
$this->newPivot($record, false)->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of records to insert into the pivot table.
|
||||
*
|
||||
* @param array $ids
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
protected function formatAttachRecords($ids, array $attributes)
|
||||
{
|
||||
$records = [];
|
||||
|
||||
$hasTimestamps = ($this->hasPivotColumn($this->createdAt()) ||
|
||||
$this->hasPivotColumn($this->updatedAt()));
|
||||
|
||||
// To create the attachment records, we will simply spin through the IDs given
|
||||
// and create a new record to insert for each ID. Each ID may actually be a
|
||||
// key in the array, with extra attributes to be placed in other columns.
|
||||
foreach ($ids as $key => $value) {
|
||||
$records[] = $this->formatAttachRecord(
|
||||
$key, $value, $attributes, $hasTimestamps
|
||||
);
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a full attachment record payload.
|
||||
*
|
||||
* @param int $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @param bool $hasTimestamps
|
||||
* @return array
|
||||
*/
|
||||
protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps)
|
||||
{
|
||||
[$id, $attributes] = $this->extractAttachIdAndAttributes($key, $value, $attributes);
|
||||
|
||||
return array_merge(
|
||||
$this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attach record ID and extra attributes.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
protected function extractAttachIdAndAttributes($key, $value, array $attributes)
|
||||
{
|
||||
return is_array($value)
|
||||
? [$key, array_merge($value, $attributes)]
|
||||
: [$value, $attributes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new pivot attachment record.
|
||||
*
|
||||
* @param int $id
|
||||
* @param bool $timed
|
||||
* @return array
|
||||
*/
|
||||
protected function baseAttachRecord($id, $timed)
|
||||
{
|
||||
$record[$this->relatedPivotKey] = $id;
|
||||
|
||||
$record[$this->foreignPivotKey] = $this->parent->{$this->parentKey};
|
||||
|
||||
// If the record needs to have creation and update timestamps, we will make
|
||||
// them by calling the parent model's "freshTimestamp" method which will
|
||||
// provide us with a fresh timestamp in this model's preferred format.
|
||||
if ($timed) {
|
||||
$record = $this->addTimestampsToAttachment($record);
|
||||
}
|
||||
|
||||
foreach ($this->pivotValues as $value) {
|
||||
$record[$value['column']] = $value['value'];
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation and update timestamps on an attach record.
|
||||
*
|
||||
* @param array $record
|
||||
* @param bool $exists
|
||||
* @return array
|
||||
*/
|
||||
protected function addTimestampsToAttachment(array $record, $exists = false)
|
||||
{
|
||||
$fresh = $this->parent->freshTimestamp();
|
||||
|
||||
if ($this->using) {
|
||||
$pivotModel = new $this->using;
|
||||
|
||||
$fresh = $pivotModel->fromDateTime($fresh);
|
||||
}
|
||||
|
||||
if (! $exists && $this->hasPivotColumn($this->createdAt())) {
|
||||
$record[$this->createdAt()] = $fresh;
|
||||
}
|
||||
|
||||
if ($this->hasPivotColumn($this->updatedAt())) {
|
||||
$record[$this->updatedAt()] = $fresh;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given column is defined as a pivot column.
|
||||
*
|
||||
* @param string $column
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPivotColumn($column)
|
||||
{
|
||||
return in_array($column, $this->pivotColumns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach models from the relationship.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param bool $touch
|
||||
* @return int
|
||||
*/
|
||||
public function detach($ids = null, $touch = true)
|
||||
{
|
||||
if ($this->using) {
|
||||
$results = $this->detachUsingCustomClass($ids);
|
||||
} else {
|
||||
$query = $this->newPivotQuery();
|
||||
|
||||
// If associated IDs were passed to the method we will only delete those
|
||||
// associations, otherwise all of the association ties will be broken.
|
||||
// We'll return the numbers of affected rows when we do the deletes.
|
||||
if (! is_null($ids)) {
|
||||
$ids = $this->parseIds($ids);
|
||||
|
||||
if (empty($ids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query->whereIn($this->getQualifiedRelatedPivotKeyName(), (array) $ids);
|
||||
}
|
||||
|
||||
// Once we have all of the conditions set on the statement, we are ready
|
||||
// to run the delete on the pivot table. Then, if the touch parameter
|
||||
// is true, we will go ahead and touch all related models to sync.
|
||||
$results = $query->delete();
|
||||
}
|
||||
|
||||
if ($touch) {
|
||||
$this->touchIfTouching();
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach models from the relationship within a transaction.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @param bool $touch
|
||||
* @return int
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function detachOrFail($ids = null, $touch = true)
|
||||
{
|
||||
return $this->parent->getConnection()->transaction(fn () => $this->detach($ids, $touch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach models from the relationship using a custom class.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @return int
|
||||
*/
|
||||
protected function detachUsingCustomClass($ids)
|
||||
{
|
||||
$results = 0;
|
||||
|
||||
$records = $this->getCurrentlyAttachedPivotsForIds($ids);
|
||||
|
||||
foreach ($records as $record) {
|
||||
$results += $record->delete();
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pivot models that are currently attached.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getCurrentlyAttachedPivots()
|
||||
{
|
||||
return $this->getCurrentlyAttachedPivotsForIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pivot models that are currently attached, filtered by related model keys.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function getCurrentlyAttachedPivotsForIds($ids = null)
|
||||
{
|
||||
return $this->newPivotQuery()
|
||||
->when(! is_null($ids), fn ($query) => $query->whereIn(
|
||||
$this->getQualifiedRelatedPivotKeyName(), $this->parseIds($ids)
|
||||
))
|
||||
->get()
|
||||
->map(function ($record) {
|
||||
$class = $this->using ?: Pivot::class;
|
||||
|
||||
$pivot = $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true);
|
||||
|
||||
return $pivot
|
||||
->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)
|
||||
->setRelatedModel($this->related);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new pivot model instance.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param bool $exists
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Pivot
|
||||
*/
|
||||
public function newPivot(array $attributes = [], $exists = false)
|
||||
{
|
||||
$attributes = array_merge(array_column($this->pivotValues, 'value', 'column'), $attributes);
|
||||
|
||||
$pivot = $this->related->newPivot(
|
||||
$this->parent, $attributes, $this->table, $exists, $this->using
|
||||
);
|
||||
|
||||
return $pivot
|
||||
->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)
|
||||
->setRelatedModel($this->related);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new existing pivot model instance.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Pivot
|
||||
*/
|
||||
public function newExistingPivot(array $attributes = [])
|
||||
{
|
||||
return $this->newPivot($attributes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new plain query builder for the pivot table.
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function newPivotStatement()
|
||||
{
|
||||
return $this->query->getQuery()->newQuery()->from($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new pivot statement for a given "other" ID.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function newPivotStatementForId($id)
|
||||
{
|
||||
return $this->newPivotQuery()->whereIn($this->getQualifiedRelatedPivotKeyName(), $this->parseIds($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query builder for the pivot table.
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function newPivotQuery()
|
||||
{
|
||||
$query = $this->newPivotStatement();
|
||||
|
||||
foreach ($this->pivotWheres as $arguments) {
|
||||
$query->where(...$arguments);
|
||||
}
|
||||
|
||||
foreach ($this->pivotWhereIns as $arguments) {
|
||||
$query->whereIn(...$arguments);
|
||||
}
|
||||
|
||||
foreach ($this->pivotWhereNulls as $arguments) {
|
||||
$query->whereNull(...$arguments);
|
||||
}
|
||||
|
||||
return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the columns on the pivot table to retrieve.
|
||||
*
|
||||
* @param mixed $columns
|
||||
* @return $this
|
||||
*/
|
||||
public function withPivot($columns)
|
||||
{
|
||||
$this->pivotColumns = array_merge(
|
||||
$this->pivotColumns, is_array($columns) ? $columns : func_get_args()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the IDs from the given mixed value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected function parseIds($value)
|
||||
{
|
||||
if ($value instanceof Model) {
|
||||
return [$value->{$this->relatedKey}];
|
||||
}
|
||||
|
||||
if ($value instanceof EloquentCollection) {
|
||||
return $value->pluck($this->relatedKey)->all();
|
||||
}
|
||||
|
||||
if ($value instanceof BaseCollection || is_array($value)) {
|
||||
return (new BaseCollection($value))
|
||||
->map(fn ($item) => $item instanceof Model ? $item->{$this->relatedKey} : $item)
|
||||
->all();
|
||||
}
|
||||
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID from the given mixed value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function parseId($value)
|
||||
{
|
||||
return $value instanceof Model ? $value->{$this->relatedKey} : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the given keys to integers if they are numeric and string otherwise.
|
||||
*
|
||||
* @param array $keys
|
||||
* @return array
|
||||
*/
|
||||
protected function castKeys(array $keys)
|
||||
{
|
||||
return array_map(function ($v) {
|
||||
return $this->castKey($v);
|
||||
}, $keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the given key to convert to primary key type.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @return mixed
|
||||
*/
|
||||
protected function castKey($key)
|
||||
{
|
||||
return $this->getTypeSwapValue(
|
||||
$this->related->getKeyType(),
|
||||
$key
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the given pivot attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
protected function castAttributes($attributes)
|
||||
{
|
||||
return $this->using
|
||||
? $this->newPivot()->fill($attributes)->getAttributes()
|
||||
: $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given value to a given type value.
|
||||
*
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getTypeSwapValue($type, $value)
|
||||
{
|
||||
return match (strtolower($type)) {
|
||||
'int', 'integer' => (int) $value,
|
||||
'real', 'float', 'double' => (float) $value,
|
||||
'string' => (string) $value,
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
}
|
||||
+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,76 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel, \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>>
|
||||
*/
|
||||
class HasManyThrough extends HasOneOrManyThrough
|
||||
{
|
||||
use InteractsWithDictionary;
|
||||
|
||||
/**
|
||||
* Convert the relationship to a "has one through" relationship.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
|
||||
*/
|
||||
public function one()
|
||||
{
|
||||
return HasOneThrough::noConstraints(fn () => new HasOneThrough(
|
||||
tap($this->getQuery(), fn (Builder $query) => $query->getQuery()->joins = []),
|
||||
$this->farParent,
|
||||
$this->throughParent,
|
||||
$this->getFirstKeyName(),
|
||||
$this->getForeignKeyName(),
|
||||
$this->getLocalKeyName(),
|
||||
$this->getSecondLocalKeyName(),
|
||||
));
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function initRelation(array $models, $relation)
|
||||
{
|
||||
foreach ($models as $model) {
|
||||
$model->setRelation($relation, $this->related->newCollection());
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function match(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
$dictionary = $this->buildDictionary($results);
|
||||
|
||||
// Once we have the dictionary we can simply spin through the parent models to
|
||||
// link them up with their children using the keyed dictionary to make the
|
||||
// matching very convenient and easy work. Then we'll just return them.
|
||||
foreach ($models as $model) {
|
||||
$key = $this->getDictionaryKey($model->getAttribute($this->localKey));
|
||||
|
||||
if ($key !== null && isset($dictionary[$key])) {
|
||||
$model->setRelation(
|
||||
$relation, $this->related->newCollection($dictionary[$key])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getResults()
|
||||
{
|
||||
return ! is_null($this->farParent->{$this->localKey})
|
||||
? $this->get()
|
||||
: $this->related->newCollection();
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TRelatedModel, TDeclaringModel, ?TRelatedModel>
|
||||
*/
|
||||
class HasOne extends HasOneOrMany implements SupportsPartialRelations
|
||||
{
|
||||
use ComparesRelatedModels, CanBeOneOfMany, SupportsDefaultModels;
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getResults()
|
||||
{
|
||||
if (is_null($this->getParentKey())) {
|
||||
return $this->getDefaultFor($this->parent);
|
||||
}
|
||||
|
||||
return $this->query->first() ?: $this->getDefaultFor($this->parent);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function initRelation(array $models, $relation)
|
||||
{
|
||||
foreach ($models as $model) {
|
||||
$model->setRelation($relation, $this->getDefaultFor($model));
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function match(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
return $this->matchOne($models, $results, $relation);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
if ($this->isOneOfMany()) {
|
||||
$this->mergeOneOfManyJoinsTo($query);
|
||||
}
|
||||
|
||||
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add constraints for inner join subselect for one of many relationships.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param string|null $column
|
||||
* @param string|null $aggregate
|
||||
* @return void
|
||||
*/
|
||||
public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null)
|
||||
{
|
||||
$query->addSelect($this->foreignKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the columns that should be selected by the one of many subquery.
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getOneOfManySubQuerySelectColumns()
|
||||
{
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add join query constraints for one of many relationships.
|
||||
*
|
||||
* @param \Illuminate\Database\Query\JoinClause $join
|
||||
* @return void
|
||||
*/
|
||||
public function addOneOfManyJoinSubQueryConstraints(JoinClause $join)
|
||||
{
|
||||
$join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new related instance for the given model.
|
||||
*
|
||||
* @param TDeclaringModel $parent
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function newRelatedInstanceFor(Model $parent)
|
||||
{
|
||||
return tap($this->related->newInstance(), function ($instance) use ($parent) {
|
||||
$instance->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey});
|
||||
$this->applyInverseRelationToModel($instance, $parent);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the model's foreign key.
|
||||
*
|
||||
* @param TRelatedModel $model
|
||||
* @return int|string
|
||||
*/
|
||||
protected function getRelatedKeyFrom(Model $model)
|
||||
{
|
||||
return $model->getAttribute($this->getForeignKeyName());
|
||||
}
|
||||
}
|
||||
+629
@@ -0,0 +1,629 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TResult
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TDeclaringModel, TResult>
|
||||
*/
|
||||
abstract class HasOneOrMany extends Relation
|
||||
{
|
||||
use InteractsWithDictionary, SupportsInverseRelations;
|
||||
|
||||
/**
|
||||
* The foreign key of the parent model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $foreignKey;
|
||||
|
||||
/**
|
||||
* The local key of the parent model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $localKey;
|
||||
|
||||
/**
|
||||
* Create a new has one or many relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $foreignKey
|
||||
* @param string $localKey
|
||||
*/
|
||||
public function __construct(Builder $query, Model $parent, $foreignKey, $localKey)
|
||||
{
|
||||
$this->localKey = $localKey;
|
||||
$this->foreignKey = $foreignKey;
|
||||
|
||||
parent::__construct($query, $parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an un-saved instance of the related model.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function make(array $attributes = [])
|
||||
{
|
||||
return tap($this->related->newInstance($attributes), function ($instance) {
|
||||
$this->setForeignAttributesForCreate($instance);
|
||||
$this->applyInverseRelationToModel($instance);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an un-saved instance of the related models.
|
||||
*
|
||||
* @param iterable $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function makeMany($records)
|
||||
{
|
||||
$instances = $this->related->newCollection();
|
||||
|
||||
foreach ($records as $record) {
|
||||
$instances->push($this->make($record));
|
||||
}
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base constraints on the relation query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addConstraints()
|
||||
{
|
||||
if (static::$constraints) {
|
||||
$query = $this->getRelationQuery();
|
||||
|
||||
$query->where($this->foreignKey, '=', $this->getParentKey());
|
||||
|
||||
$query->whereNotNull($this->foreignKey);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addEagerConstraints(array $models)
|
||||
{
|
||||
$whereIn = $this->whereInMethod($this->parent, $this->localKey);
|
||||
|
||||
$this->whereInEager(
|
||||
$whereIn,
|
||||
$this->foreignKey,
|
||||
$this->getKeys($models, $this->localKey),
|
||||
$this->getRelationQuery()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the eagerly loaded results to their single parents.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @param string $relation
|
||||
* @return array<int, TDeclaringModel>
|
||||
*/
|
||||
public function matchOne(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
return $this->matchOneOrMany($models, $results, $relation, 'one');
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the eagerly loaded results to their many parents.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @param string $relation
|
||||
* @return array<int, TDeclaringModel>
|
||||
*/
|
||||
public function matchMany(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
return $this->matchOneOrMany($models, $results, $relation, 'many');
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the eagerly loaded results to their many parents.
|
||||
*
|
||||
* @param array<int, TDeclaringModel> $models
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @param string $relation
|
||||
* @param string $type
|
||||
* @return array<int, TDeclaringModel>
|
||||
*/
|
||||
protected function matchOneOrMany(array $models, EloquentCollection $results, $relation, $type)
|
||||
{
|
||||
$dictionary = $this->buildDictionary($results);
|
||||
|
||||
// Once we have the dictionary we can simply spin through the parent models to
|
||||
// link them up with their children using the keyed dictionary to make the
|
||||
// matching very convenient and easy work. Then we'll just return them.
|
||||
foreach ($models as $model) {
|
||||
$key = $this->getDictionaryKey($model->getAttribute($this->localKey));
|
||||
|
||||
if ($key !== null && isset($dictionary[$key])) {
|
||||
$related = $this->getRelationValue($dictionary, $key, $type);
|
||||
|
||||
$model->setRelation($relation, $related);
|
||||
|
||||
// Apply the inverse relation if we have one...
|
||||
$type === 'one'
|
||||
? $this->applyInverseRelationToModel($related, $model)
|
||||
: $this->applyInverseRelationToCollection($related, $model);
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a relationship by one or many type.
|
||||
*
|
||||
* @param array $dictionary
|
||||
* @param string $key
|
||||
* @param string $type
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRelationValue(array $dictionary, $key, $type)
|
||||
{
|
||||
$value = $dictionary[$key];
|
||||
|
||||
return $type === 'one' ? reset($value) : $this->related->newCollection($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build model dictionary keyed by the relation's foreign key.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @return array<array<array-key, TRelatedModel>>
|
||||
*/
|
||||
protected function buildDictionary(EloquentCollection $results)
|
||||
{
|
||||
$foreign = $this->getForeignKeyName();
|
||||
|
||||
$dictionary = [];
|
||||
|
||||
$isAssociative = Arr::isAssoc($results->all());
|
||||
|
||||
foreach ($results as $key => $item) {
|
||||
$pairKey = $this->getDictionaryKey($item->{$foreign});
|
||||
|
||||
if ($pairKey === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isAssociative) {
|
||||
$dictionary[$pairKey][$key] = $item;
|
||||
} else {
|
||||
$dictionary[$pairKey][] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a model by its primary key or return a new instance of the related model.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $columns
|
||||
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
|
||||
*/
|
||||
public function findOrNew($id, $columns = ['*'])
|
||||
{
|
||||
if (is_null($instance = $this->find($id, $columns))) {
|
||||
$instance = $this->related->newInstance();
|
||||
|
||||
$this->setForeignAttributesForCreate($instance);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first related model record matching the attributes or instantiate it.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param (\Closure(): array)|array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrNew(array $attributes = [], Closure|array $values = [])
|
||||
{
|
||||
if (is_null($instance = $this->where($attributes)->first())) {
|
||||
$instance = $this->related->newInstance(array_merge($attributes, value($values)));
|
||||
|
||||
$this->setForeignAttributesForCreate($instance);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first record matching the attributes. If the record is not found, create it.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param (\Closure(): array)|array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrCreate(array $attributes = [], Closure|array $values = [])
|
||||
{
|
||||
if (is_null($instance = (clone $this)->where($attributes)->first())) {
|
||||
$instance = $this->createOrFirst($attributes, $values);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param (\Closure(): array)|array $values
|
||||
* @return TRelatedModel
|
||||
*
|
||||
* @throws \Illuminate\Database\UniqueConstraintViolationException
|
||||
*/
|
||||
public function createOrFirst(array $attributes = [], Closure|array $values = [])
|
||||
{
|
||||
try {
|
||||
return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values))));
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
return $this->useWritePdo()->where($attributes)->first() ?? throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a related record matching the attributes, and fill it with values.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param (\Closure(): array)|array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function updateOrCreate(array $attributes, Closure|array $values = [])
|
||||
{
|
||||
return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) {
|
||||
if (! $instance->wasRecentlyCreated) {
|
||||
$instance->fill(value($values))->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new records or update the existing ones.
|
||||
*
|
||||
* @param array $values
|
||||
* @param array|string $uniqueBy
|
||||
* @param array|null $update
|
||||
* @return int
|
||||
*/
|
||||
public function upsert(array $values, $uniqueBy, $update = null)
|
||||
{
|
||||
if (! empty($values) && ! is_array(array_first($values))) {
|
||||
$values = [$values];
|
||||
}
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
$values[$key][$this->getForeignKeyName()] = $this->getParentKey();
|
||||
}
|
||||
|
||||
return $this->getQuery()->upsert($values, $uniqueBy, $update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a model instance to the parent model.
|
||||
*
|
||||
* @param TRelatedModel $model
|
||||
* @return TRelatedModel|false
|
||||
*/
|
||||
public function save(Model $model)
|
||||
{
|
||||
$this->setForeignAttributesForCreate($model);
|
||||
|
||||
return $model->save() ? $model : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a model instance without raising any events to the parent model.
|
||||
*
|
||||
* @param TRelatedModel $model
|
||||
* @return TRelatedModel|false
|
||||
*/
|
||||
public function saveQuietly(Model $model)
|
||||
{
|
||||
return Model::withoutEvents(function () use ($model) {
|
||||
return $this->save($model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a collection of models to the parent instance.
|
||||
*
|
||||
* @param iterable<TRelatedModel> $models
|
||||
* @return iterable<TRelatedModel>
|
||||
*/
|
||||
public function saveMany($models)
|
||||
{
|
||||
foreach ($models as $model) {
|
||||
$this->save($model);
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a collection of models to the parent instance without raising any events to the parent model.
|
||||
*
|
||||
* @param iterable<TRelatedModel> $models
|
||||
* @return iterable<TRelatedModel>
|
||||
*/
|
||||
public function saveManyQuietly($models)
|
||||
{
|
||||
return Model::withoutEvents(function () use ($models) {
|
||||
return $this->saveMany($models);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the related model.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function create(array $attributes = [])
|
||||
{
|
||||
return tap($this->related->newInstance($attributes), function ($instance) {
|
||||
$this->setForeignAttributesForCreate($instance);
|
||||
|
||||
$instance->save();
|
||||
|
||||
$this->applyInverseRelationToModel($instance);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the related model without raising any events to the parent model.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function createQuietly(array $attributes = [])
|
||||
{
|
||||
return Model::withoutEvents(fn () => $this->create($attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the related model. Allow mass-assignment.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function forceCreate(array $attributes = [])
|
||||
{
|
||||
$attributes[$this->getForeignKeyName()] = $this->getParentKey();
|
||||
|
||||
return $this->applyInverseRelationToModel($this->related->forceCreate($attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the related model with mass assignment without raising model events.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function forceCreateQuietly(array $attributes = [])
|
||||
{
|
||||
return Model::withoutEvents(fn () => $this->forceCreate($attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Collection of new instances of the related model.
|
||||
*
|
||||
* @param iterable $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function createMany(iterable $records)
|
||||
{
|
||||
$instances = $this->related->newCollection();
|
||||
|
||||
foreach ($records as $record) {
|
||||
$instances->push($this->create($record));
|
||||
}
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Collection of new instances of the related model without raising any events to the parent model.
|
||||
*
|
||||
* @param iterable $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function createManyQuietly(iterable $records)
|
||||
{
|
||||
return Model::withoutEvents(fn () => $this->createMany($records));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Collection of new instances of the related model, allowing mass-assignment.
|
||||
*
|
||||
* @param iterable $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function forceCreateMany(iterable $records)
|
||||
{
|
||||
$instances = $this->related->newCollection();
|
||||
|
||||
foreach ($records as $record) {
|
||||
$instances->push($this->forceCreate($record));
|
||||
}
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Collection of new instances of the related model, allowing mass-assignment and without raising any events to the parent model.
|
||||
*
|
||||
* @param iterable $records
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function forceCreateManyQuietly(iterable $records)
|
||||
{
|
||||
return Model::withoutEvents(fn () => $this->forceCreateMany($records));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the foreign ID for creating a related model.
|
||||
*
|
||||
* @param TRelatedModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function setForeignAttributesForCreate(Model $model)
|
||||
{
|
||||
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
|
||||
|
||||
foreach ($this->getQuery()->pendingAttributes as $key => $value) {
|
||||
$attributes ??= $model->getAttributes();
|
||||
|
||||
if (! array_key_exists($key, $attributes)) {
|
||||
$model->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyInverseRelationToModel($model);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
if ($query->getQuery()->from == $parentQuery->getQuery()->from) {
|
||||
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the constraints for a relationship query on the same table.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
|
||||
* @param mixed $columns
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
$query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());
|
||||
|
||||
$query->getModel()->setTable($hash);
|
||||
|
||||
return $query->select($columns)->whereColumn(
|
||||
$this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->getForeignKeyName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to set the "limit" value of the query.
|
||||
*
|
||||
* @param int $value
|
||||
* @return $this
|
||||
*/
|
||||
public function take($value)
|
||||
{
|
||||
return $this->limit($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "limit" value of the query.
|
||||
*
|
||||
* @param int $value
|
||||
* @return $this
|
||||
*/
|
||||
public function limit($value)
|
||||
{
|
||||
if ($this->parent->exists) {
|
||||
$this->query->limit($value);
|
||||
} else {
|
||||
$this->query->groupLimit($value, $this->getExistenceCompareKey());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for comparing against the parent key in "has" query.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExistenceCompareKey()
|
||||
{
|
||||
return $this->getQualifiedForeignKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key value of the parent's local key.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getParentKey()
|
||||
{
|
||||
return $this->parent->getAttribute($this->localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified parent key name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedParentKeyName()
|
||||
{
|
||||
return $this->parent->qualifyColumn($this->localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain foreign key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKeyName()
|
||||
{
|
||||
$segments = explode('.', $this->getQualifiedForeignKeyName());
|
||||
|
||||
return array_last($segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key for the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedForeignKeyName()
|
||||
{
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local key for the relationship.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalKeyName()
|
||||
{
|
||||
return $this->localKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,872 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||||
use Illuminate\Database\Query\Grammars\MySqlGrammar;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TResult
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TIntermediateModel, TResult>
|
||||
*/
|
||||
abstract class HasOneOrManyThrough extends Relation
|
||||
{
|
||||
use InteractsWithDictionary;
|
||||
|
||||
/**
|
||||
* The "through" parent model instance.
|
||||
*
|
||||
* @var TIntermediateModel
|
||||
*/
|
||||
protected $throughParent;
|
||||
|
||||
/**
|
||||
* The far parent model instance.
|
||||
*
|
||||
* @var TDeclaringModel
|
||||
*/
|
||||
protected $farParent;
|
||||
|
||||
/**
|
||||
* The near key on the relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $firstKey;
|
||||
|
||||
/**
|
||||
* The far key on the relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $secondKey;
|
||||
|
||||
/**
|
||||
* The local key on the relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $localKey;
|
||||
|
||||
/**
|
||||
* The local key on the intermediary model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $secondLocalKey;
|
||||
|
||||
/**
|
||||
* Create a new has many through relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $farParent
|
||||
* @param TIntermediateModel $throughParent
|
||||
* @param string $firstKey
|
||||
* @param string $secondKey
|
||||
* @param string $localKey
|
||||
* @param string $secondLocalKey
|
||||
*/
|
||||
public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
|
||||
{
|
||||
$this->localKey = $localKey;
|
||||
$this->firstKey = $firstKey;
|
||||
$this->secondKey = $secondKey;
|
||||
$this->farParent = $farParent;
|
||||
$this->throughParent = $throughParent;
|
||||
$this->secondLocalKey = $secondLocalKey;
|
||||
|
||||
parent::__construct($query, $throughParent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base constraints on the relation query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addConstraints()
|
||||
{
|
||||
$query = $this->getRelationQuery();
|
||||
|
||||
$this->performJoin($query);
|
||||
|
||||
if (static::$constraints) {
|
||||
$localValue = $this->farParent[$this->localKey];
|
||||
|
||||
$query->where($this->getQualifiedFirstKeyName(), '=', $localValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the join clause on the query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel>|null $query
|
||||
* @return void
|
||||
*/
|
||||
protected function performJoin(?Builder $query = null)
|
||||
{
|
||||
$query ??= $this->query;
|
||||
|
||||
$farKey = $this->getQualifiedFarKeyName();
|
||||
|
||||
$query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey);
|
||||
|
||||
if ($this->throughParentSoftDeletes()) {
|
||||
$query->withGlobalScope('SoftDeletableHasManyThrough', function ($query) {
|
||||
$query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified parent key name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedParentKeyName()
|
||||
{
|
||||
return $this->parent->qualifyColumn($this->secondLocalKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether "through" parent of the relation uses Soft Deletes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function throughParentSoftDeletes()
|
||||
{
|
||||
return $this->throughParent::isSoftDeletable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that trashed "through" parents should be included in the query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withTrashedParents()
|
||||
{
|
||||
$this->query->withoutGlobalScope('SoftDeletableHasManyThrough');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addEagerConstraints(array $models)
|
||||
{
|
||||
$whereIn = $this->whereInMethod($this->farParent, $this->localKey);
|
||||
|
||||
$this->whereInEager(
|
||||
$whereIn,
|
||||
$this->getQualifiedFirstKeyName(),
|
||||
$this->getKeys($models, $this->localKey),
|
||||
$this->getRelationQuery(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build model dictionary keyed by the relation's foreign key.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
|
||||
* @return array<array<array-key, TRelatedModel>>
|
||||
*/
|
||||
protected function buildDictionary(EloquentCollection $results)
|
||||
{
|
||||
$dictionary = [];
|
||||
|
||||
$isAssociative = Arr::isAssoc($results->all());
|
||||
|
||||
// First we will create a dictionary of models keyed by the foreign key of the
|
||||
// relationship as this will allow us to quickly access all of the related
|
||||
// models without having to do nested looping which will be quite slow.
|
||||
foreach ($results as $key => $result) {
|
||||
if ($isAssociative) {
|
||||
$dictionary[$result->laravel_through_key][$key] = $result;
|
||||
} else {
|
||||
$dictionary[$result->laravel_through_key][] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first related model record matching the attributes or instantiate it.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrNew(array $attributes = [], array $values = [])
|
||||
{
|
||||
if (! is_null($instance = $this->where($attributes)->first())) {
|
||||
return $instance;
|
||||
}
|
||||
|
||||
return $this->related->newInstance(array_merge($attributes, $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first record matching the attributes. If the record is not found, create it.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param (\Closure(): array)|array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function firstOrCreate(array $attributes = [], Closure|array $values = [])
|
||||
{
|
||||
if (! is_null($instance = (clone $this)->where($attributes)->first())) {
|
||||
return $instance;
|
||||
}
|
||||
|
||||
return $this->createOrFirst(array_merge($attributes, value($values)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param (\Closure(): array)|array $values
|
||||
* @return TRelatedModel
|
||||
*
|
||||
* @throws \Illuminate\Database\UniqueConstraintViolationException
|
||||
*/
|
||||
public function createOrFirst(array $attributes = [], Closure|array $values = [])
|
||||
{
|
||||
try {
|
||||
return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values))));
|
||||
} catch (UniqueConstraintViolationException $exception) {
|
||||
return $this->where($attributes)->first() ?? throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a related record matching the attributes, and fill it with values.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param array $values
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function updateOrCreate(array $attributes, array $values = [])
|
||||
{
|
||||
return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) {
|
||||
if (! $instance->wasRecentlyCreated) {
|
||||
$instance->fill($values)->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic where clause to the query, and return the first result.
|
||||
*
|
||||
* @param \Closure|string|array $column
|
||||
* @param mixed $operator
|
||||
* @param mixed $value
|
||||
* @param string $boolean
|
||||
* @return TRelatedModel|null
|
||||
*/
|
||||
public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
|
||||
{
|
||||
return $this->where($column, $operator, $value, $boolean)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get the first related model.
|
||||
*
|
||||
* @param array $columns
|
||||
* @return TRelatedModel|null
|
||||
*/
|
||||
public function first($columns = ['*'])
|
||||
{
|
||||
$results = $this->limit(1)->get($columns);
|
||||
|
||||
return count($results) > 0 ? $results->first() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get the first result or throw an exception.
|
||||
*
|
||||
* @param array $columns
|
||||
* @return TRelatedModel
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
|
||||
*/
|
||||
public function firstOrFail($columns = ['*'])
|
||||
{
|
||||
if (! is_null($model = $this->first($columns))) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
throw (new ModelNotFoundException)->setModel(get_class($this->related));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get the first result or call a callback.
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @param (\Closure(): TValue)|list<string> $columns
|
||||
* @param (\Closure(): TValue)|null $callback
|
||||
* @return TRelatedModel|TValue
|
||||
*/
|
||||
public function firstOr($columns = ['*'], ?Closure $callback = null)
|
||||
{
|
||||
if ($columns instanceof Closure) {
|
||||
$callback = $columns;
|
||||
|
||||
$columns = ['*'];
|
||||
}
|
||||
|
||||
if (! is_null($model = $this->first($columns))) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
return $callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a related model by its primary key.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $columns
|
||||
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel|null)
|
||||
*/
|
||||
public function find($id, $columns = ['*'])
|
||||
{
|
||||
if (is_array($id) || $id instanceof Arrayable) {
|
||||
return $this->findMany($id, $columns);
|
||||
}
|
||||
|
||||
return $this->where(
|
||||
$this->getRelated()->getQualifiedKeyName(), '=', $id
|
||||
)->first($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a sole related model by its primary key.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $columns
|
||||
* @return TRelatedModel
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
|
||||
* @throws \Illuminate\Database\MultipleRecordsFoundException
|
||||
*/
|
||||
public function findSole($id, $columns = ['*'])
|
||||
{
|
||||
return $this->where(
|
||||
$this->getRelated()->getQualifiedKeyName(), '=', $id
|
||||
)->sole($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple related models by their primary keys.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Support\Arrayable|array $ids
|
||||
* @param array $columns
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
|
||||
*/
|
||||
public function findMany($ids, $columns = ['*'])
|
||||
{
|
||||
$ids = $ids instanceof Arrayable ? $ids->toArray() : $ids;
|
||||
|
||||
if (empty($ids)) {
|
||||
return $this->getRelated()->newCollection();
|
||||
}
|
||||
|
||||
return $this->whereIn(
|
||||
$this->getRelated()->getQualifiedKeyName(), $ids
|
||||
)->get($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a related model by its primary key or throw an exception.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param array $columns
|
||||
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
|
||||
*/
|
||||
public function findOrFail($id, $columns = ['*'])
|
||||
{
|
||||
$result = $this->find($id, $columns);
|
||||
|
||||
$id = $id instanceof Arrayable ? $id->toArray() : $id;
|
||||
|
||||
if (is_array($id)) {
|
||||
if (count($result) === count(array_unique($id))) {
|
||||
return $result;
|
||||
}
|
||||
} elseif (! is_null($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a related model by its primary key or call a callback.
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param (\Closure(): TValue)|list<string>|string $columns
|
||||
* @param (\Closure(): TValue)|null $callback
|
||||
* @return (
|
||||
* $id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>)
|
||||
* ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>|TValue
|
||||
* : TRelatedModel|TValue
|
||||
* )
|
||||
*/
|
||||
public function findOr($id, $columns = ['*'], ?Closure $callback = null)
|
||||
{
|
||||
if ($columns instanceof Closure) {
|
||||
$callback = $columns;
|
||||
|
||||
$columns = ['*'];
|
||||
}
|
||||
|
||||
$result = $this->find($id, $columns);
|
||||
|
||||
$id = $id instanceof Arrayable ? $id->toArray() : $id;
|
||||
|
||||
if (is_array($id)) {
|
||||
if (count($result) === count(array_unique($id))) {
|
||||
return $result;
|
||||
}
|
||||
} elseif (! is_null($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $callback();
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get($columns = ['*'])
|
||||
{
|
||||
$builder = $this->prepareQueryBuilder($columns);
|
||||
|
||||
$models = $builder->getModels();
|
||||
|
||||
// If we actually found models we will also eager load any relationships that
|
||||
// have been specified as needing to be eager loaded. This will solve the
|
||||
// n + 1 query problem for the developer and also increase performance.
|
||||
if (count($models) > 0) {
|
||||
$models = $builder->eagerLoadRelations($models);
|
||||
}
|
||||
|
||||
return $this->query->applyAfterQueryCallbacks(
|
||||
$this->related->newCollection($models)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a paginator for the "select" statement.
|
||||
*
|
||||
* @param int|null $perPage
|
||||
* @param array $columns
|
||||
* @param string $pageName
|
||||
* @param int|null $page
|
||||
* @return \Illuminate\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
|
||||
{
|
||||
$this->query->addSelect($this->shouldSelect($columns));
|
||||
|
||||
return $this->query->paginate($perPage, $columns, $pageName, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the given query into a simple paginator.
|
||||
*
|
||||
* @param int|null $perPage
|
||||
* @param array $columns
|
||||
* @param string $pageName
|
||||
* @param int|null $page
|
||||
* @return \Illuminate\Contracts\Pagination\Paginator
|
||||
*/
|
||||
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
|
||||
{
|
||||
$this->query->addSelect($this->shouldSelect($columns));
|
||||
|
||||
return $this->query->simplePaginate($perPage, $columns, $pageName, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the given query into a cursor paginator.
|
||||
*
|
||||
* @param int|null $perPage
|
||||
* @param array $columns
|
||||
* @param string $cursorName
|
||||
* @param string|null $cursor
|
||||
* @return \Illuminate\Contracts\Pagination\CursorPaginator
|
||||
*/
|
||||
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
|
||||
{
|
||||
$this->query->addSelect($this->shouldSelect($columns));
|
||||
|
||||
return $this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the select clause for the relation query.
|
||||
*
|
||||
* @param array $columns
|
||||
* @return array
|
||||
*/
|
||||
protected function shouldSelect(array $columns = ['*'])
|
||||
{
|
||||
if ($columns == ['*']) {
|
||||
$columns = [$this->related->qualifyColumn('*')];
|
||||
}
|
||||
|
||||
return array_merge($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk the results of the query.
|
||||
*
|
||||
* @param int $count
|
||||
* @param callable $callback
|
||||
* @return bool
|
||||
*/
|
||||
public function chunk($count, callable $callback)
|
||||
{
|
||||
return $this->prepareQueryBuilder()->chunk($count, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk the results of a query by comparing numeric IDs.
|
||||
*
|
||||
* @param int $count
|
||||
* @param callable $callback
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return bool
|
||||
*/
|
||||
public function chunkById($count, callable $callback, $column = null, $alias = null)
|
||||
{
|
||||
$column ??= $this->getRelated()->getQualifiedKeyName();
|
||||
|
||||
$alias ??= $this->getRelated()->getKeyName();
|
||||
|
||||
return $this->prepareQueryBuilder()->chunkById($count, $callback, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk the results of a query by comparing IDs in descending order.
|
||||
*
|
||||
* @param int $count
|
||||
* @param callable $callback
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return bool
|
||||
*/
|
||||
public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
|
||||
{
|
||||
$column ??= $this->getRelated()->getQualifiedKeyName();
|
||||
|
||||
$alias ??= $this->getRelated()->getKeyName();
|
||||
|
||||
return $this->prepareQueryBuilder()->chunkByIdDesc($count, $callback, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback over each item while chunking by ID.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param int $count
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return bool
|
||||
*/
|
||||
public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
|
||||
{
|
||||
$column ??= $this->getRelated()->getQualifiedKeyName();
|
||||
|
||||
$alias ??= $this->getRelated()->getKeyName();
|
||||
|
||||
return $this->prepareQueryBuilder()->eachById($callback, $count, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a generator for the given query.
|
||||
*
|
||||
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
|
||||
*/
|
||||
public function cursor()
|
||||
{
|
||||
return $this->prepareQueryBuilder()->cursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback over each item while chunking.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param int $count
|
||||
* @return bool
|
||||
*/
|
||||
public function each(callable $callback, $count = 1000)
|
||||
{
|
||||
return $this->chunk($count, function ($results) use ($callback) {
|
||||
foreach ($results as $key => $value) {
|
||||
if ($callback($value, $key) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunks of the given size.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
|
||||
*/
|
||||
public function lazy($chunkSize = 1000)
|
||||
{
|
||||
return $this->prepareQueryBuilder()->lazy($chunkSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunking the results of a query by comparing IDs.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
|
||||
*/
|
||||
public function lazyById($chunkSize = 1000, $column = null, $alias = null)
|
||||
{
|
||||
$column ??= $this->getRelated()->getQualifiedKeyName();
|
||||
|
||||
$alias ??= $this->getRelated()->getKeyName();
|
||||
|
||||
return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunking the results of a query by comparing IDs in descending order.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return \Illuminate\Support\LazyCollection<int, TRelatedModel>
|
||||
*/
|
||||
public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
|
||||
{
|
||||
$column ??= $this->getRelated()->getQualifiedKeyName();
|
||||
|
||||
$alias ??= $this->getRelated()->getKeyName();
|
||||
|
||||
return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the query builder for query execution.
|
||||
*
|
||||
* @param array $columns
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
protected function prepareQueryBuilder($columns = ['*'])
|
||||
{
|
||||
$builder = $this->query->applyScopes();
|
||||
|
||||
return $builder->addSelect(
|
||||
$this->shouldSelect($builder->getQuery()->columns ? [] : $columns)
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
if ($parentQuery->getQuery()->from === $query->getQuery()->from) {
|
||||
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
if ($parentQuery->getQuery()->from === $this->throughParent->getTable()) {
|
||||
return $this->getRelationExistenceQueryForThroughSelfRelation($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
$this->performJoin($query);
|
||||
|
||||
return $query->select($columns)->whereColumn(
|
||||
$this->getQualifiedLocalKeyName(), '=', $this->getQualifiedFirstKeyName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the constraints for a relationship query on the same table.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
|
||||
* @param mixed $columns
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
$query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());
|
||||
|
||||
$query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->secondKey);
|
||||
|
||||
if ($this->throughParentSoftDeletes()) {
|
||||
$query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
|
||||
}
|
||||
|
||||
$query->getModel()->setTable($hash);
|
||||
|
||||
return $query->select($columns)->whereColumn(
|
||||
$parentQuery->getQuery()->from.'.'.$this->localKey, '=', $this->getQualifiedFirstKeyName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the constraints for a relationship query on the same table as the through parent.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TDeclaringModel> $parentQuery
|
||||
* @param mixed $columns
|
||||
* @return \Illuminate\Database\Eloquent\Builder<TRelatedModel>
|
||||
*/
|
||||
public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
$table = $this->throughParent->getTable().' as '.$hash = $this->getRelationCountHash();
|
||||
|
||||
$query->join($table, $hash.'.'.$this->secondLocalKey, '=', $this->getQualifiedFarKeyName());
|
||||
|
||||
if ($this->throughParentSoftDeletes()) {
|
||||
$query->whereNull($hash.'.'.$this->throughParent->getDeletedAtColumn());
|
||||
}
|
||||
|
||||
return $query->select($columns)->whereColumn(
|
||||
$parentQuery->getQuery()->from.'.'.$this->localKey, '=', $hash.'.'.$this->firstKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to set the "limit" value of the query.
|
||||
*
|
||||
* @param int $value
|
||||
* @return $this
|
||||
*/
|
||||
public function take($value)
|
||||
{
|
||||
return $this->limit($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "limit" value of the query.
|
||||
*
|
||||
* @param int $value
|
||||
* @return $this
|
||||
*/
|
||||
public function limit($value)
|
||||
{
|
||||
if ($this->farParent->exists) {
|
||||
$this->query->limit($value);
|
||||
} else {
|
||||
$column = $this->getQualifiedFirstKeyName();
|
||||
|
||||
$grammar = $this->query->getQuery()->getGrammar();
|
||||
|
||||
if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) {
|
||||
$column = 'laravel_through_key';
|
||||
}
|
||||
|
||||
$this->query->groupLimit($value, $column);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the qualified foreign key on the related model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedFarKeyName()
|
||||
{
|
||||
return $this->getQualifiedForeignKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key on the "through" model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFirstKeyName()
|
||||
{
|
||||
return $this->firstKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the qualified foreign key on the "through" model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedFirstKeyName()
|
||||
{
|
||||
return $this->throughParent->qualifyColumn($this->firstKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key on the related model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKeyName()
|
||||
{
|
||||
return $this->secondKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the qualified foreign key on the related model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedForeignKeyName()
|
||||
{
|
||||
return $this->related->qualifyColumn($this->secondKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local key on the far parent model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalKeyName()
|
||||
{
|
||||
return $this->localKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the qualified local key on the far parent model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedLocalKeyName()
|
||||
{
|
||||
return $this->farParent->qualifyColumn($this->localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local key on the intermediary model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSecondLocalKeyName()
|
||||
{
|
||||
return $this->secondLocalKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||||
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel, ?TRelatedModel>
|
||||
*/
|
||||
class HasOneThrough extends HasOneOrManyThrough implements SupportsPartialRelations
|
||||
{
|
||||
use ComparesRelatedModels, CanBeOneOfMany, InteractsWithDictionary, SupportsDefaultModels;
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getResults()
|
||||
{
|
||||
if (is_null($this->getParentKey())) {
|
||||
return $this->getDefaultFor($this->farParent);
|
||||
}
|
||||
|
||||
return $this->first() ?: $this->getDefaultFor($this->farParent);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function initRelation(array $models, $relation)
|
||||
{
|
||||
foreach ($models as $model) {
|
||||
$model->setRelation($relation, $this->getDefaultFor($model));
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function match(array $models, EloquentCollection $results, $relation)
|
||||
{
|
||||
$dictionary = $this->buildDictionary($results);
|
||||
|
||||
// Once we have the dictionary we can simply spin through the parent models to
|
||||
// link them up with their children using the keyed dictionary to make the
|
||||
// matching very convenient and easy work. Then we'll just return them.
|
||||
foreach ($models as $model) {
|
||||
$key = $this->getDictionaryKey($model->getAttribute($this->localKey));
|
||||
|
||||
if ($key !== null && isset($dictionary[$key])) {
|
||||
$value = $dictionary[$key];
|
||||
|
||||
$model->setRelation(
|
||||
$relation, reset($value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
if ($this->isOneOfMany()) {
|
||||
$this->mergeOneOfManyJoinsTo($query);
|
||||
}
|
||||
|
||||
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null)
|
||||
{
|
||||
$query->addSelect([$this->getQualifiedFirstKeyName()]);
|
||||
|
||||
// We need to join subqueries that aren't the inner-most subquery which is joined in the CanBeOneOfMany::ofMany method...
|
||||
if ($this->getOneOfManySubQuery() !== null) {
|
||||
$this->performJoin($query);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getOneOfManySubQuerySelectColumns()
|
||||
{
|
||||
return [$this->getQualifiedFirstKeyName()];
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addOneOfManyJoinSubQueryConstraints(JoinClause $join)
|
||||
{
|
||||
$join->on($this->qualifySubSelectColumn($this->firstKey), '=', $this->getQualifiedFirstKeyName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new related instance for the given model.
|
||||
*
|
||||
* @param TDeclaringModel $parent
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function newRelatedInstanceFor(Model $parent)
|
||||
{
|
||||
return $this->related->newInstance();
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
protected function getRelatedKeyFrom(Model $model)
|
||||
{
|
||||
return $model->getAttribute($this->getForeignKeyName());
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getParentKey()
|
||||
{
|
||||
return $this->farParent->getAttribute($this->localKey);
|
||||
}
|
||||
}
|
||||
+69
@@ -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());
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
|
||||
* @template TResult
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TRelatedModel, TDeclaringModel, TResult>
|
||||
*/
|
||||
abstract class MorphOneOrMany extends HasOneOrMany
|
||||
{
|
||||
/**
|
||||
* The foreign key type for the relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* The morph class of the parent model.
|
||||
*
|
||||
* @var class-string<TDeclaringModel>|string
|
||||
*/
|
||||
protected $morphClass;
|
||||
|
||||
/**
|
||||
* Create a new morph one or many relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
|
||||
* @param TDeclaringModel $parent
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @param string $localKey
|
||||
*/
|
||||
public function __construct(Builder $query, Model $parent, $type, $id, $localKey)
|
||||
{
|
||||
$this->morphType = $type;
|
||||
|
||||
$this->morphClass = $parent->getMorphClass();
|
||||
|
||||
parent::__construct($query, $parent, $id, $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base constraints on the relation query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addConstraints()
|
||||
{
|
||||
if (static::$constraints) {
|
||||
$this->getRelationQuery()->where($this->morphType, $this->morphClass);
|
||||
|
||||
parent::addConstraints();
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function addEagerConstraints(array $models)
|
||||
{
|
||||
parent::addEagerConstraints($models);
|
||||
|
||||
$this->getRelationQuery()->where($this->morphType, $this->morphClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the related model. Allow mass-assignment.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return TRelatedModel
|
||||
*/
|
||||
public function forceCreate(array $attributes = [])
|
||||
{
|
||||
$attributes[$this->getForeignKeyName()] = $this->getParentKey();
|
||||
$attributes[$this->getMorphType()] = $this->morphClass;
|
||||
|
||||
return $this->applyInverseRelationToModel($this->related->forceCreate($attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the foreign ID and type for creating a related model.
|
||||
*
|
||||
* @param TRelatedModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function setForeignAttributesForCreate(Model $model)
|
||||
{
|
||||
$model->{$this->getForeignKeyName()} = $this->getParentKey();
|
||||
|
||||
$model->{$this->getMorphType()} = $this->morphClass;
|
||||
|
||||
foreach ($this->getQuery()->pendingAttributes as $key => $value) {
|
||||
$attributes ??= $model->getAttributes();
|
||||
|
||||
if (! array_key_exists($key, $attributes)) {
|
||||
$model->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyInverseRelationToModel($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new records or update the existing ones.
|
||||
*
|
||||
* @param array $values
|
||||
* @param array|string $uniqueBy
|
||||
* @param array|null $update
|
||||
* @return int
|
||||
*/
|
||||
public function upsert(array $values, $uniqueBy, $update = null)
|
||||
{
|
||||
if (! empty($values) && ! is_array(array_first($values))) {
|
||||
$values = [$values];
|
||||
}
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
$values[$key][$this->getMorphType()] = $this->getMorphClass();
|
||||
}
|
||||
|
||||
return parent::upsert($values, $uniqueBy, $update);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
||||
{
|
||||
return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where(
|
||||
$query->qualifyColumn($this->getMorphType()), $this->morphClass
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the foreign key "type" name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQualifiedMorphType()
|
||||
{
|
||||
return $this->morphType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain morph type name without the table.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphType()
|
||||
{
|
||||
return last(explode('.', $this->morphType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the morph class of the parent model.
|
||||
*
|
||||
* @return class-string<TDeclaringModel>|string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return $this->morphClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the possible inverse relations for the parent model.
|
||||
*
|
||||
* @return array<non-empty-string>
|
||||
*/
|
||||
protected function getPossibleInverseRelations(): array
|
||||
{
|
||||
return array_unique([
|
||||
Str::beforeLast($this->getMorphType(), '_type'),
|
||||
...parent::getPossibleInverseRelations(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Eloquent\Relations;
|
||||
|
||||
class MorphPivot extends Pivot
|
||||
{
|
||||
/**
|
||||
* The type of the polymorphic relation.
|
||||
*
|
||||
* Explicitly define this so it's not included in saved attributes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* The value of the polymorphic relation.
|
||||
*
|
||||
* Explicitly define this so it's not included in saved attributes.
|
||||
*
|
||||
* @var class-string|string
|
||||
*/
|
||||
protected $morphClass;
|
||||
|
||||
/**
|
||||
* Set the keys for a save update query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<static> $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function setKeysForSaveQuery($query)
|
||||
{
|
||||
$query->where($this->morphType, $this->morphClass);
|
||||
|
||||
return parent::setKeysForSaveQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the keys for a select query.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<static> $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function setKeysForSelectQuery($query)
|
||||
{
|
||||
$query->where($this->morphType, $this->morphClass);
|
||||
|
||||
return parent::setKeysForSelectQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the pivot model record from the database.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if (isset($this->attributes[$this->getKeyName()])) {
|
||||
return (int) parent::delete();
|
||||
}
|
||||
|
||||
if ($this->fireModelEvent('deleting') === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->getDeleteQuery();
|
||||
|
||||
$query->where($this->morphType, $this->morphClass);
|
||||
|
||||
return tap($query->delete(), function () {
|
||||
$this->exists = false;
|
||||
|
||||
$this->fireModelEvent('deleted', false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the morph type for the pivot.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphType()
|
||||
{
|
||||
return $this->morphType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the morph type for the pivot.
|
||||
*
|
||||
* @param string $morphType
|
||||
* @return $this
|
||||
*/
|
||||
public function setMorphType($morphType)
|
||||
{
|
||||
$this->morphType = $morphType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the morph class for the pivot.
|
||||
*
|
||||
* @param class-string|string $morphClass
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphPivot
|
||||
*/
|
||||
public function setMorphClass($morphClass)
|
||||
{
|
||||
$this->morphClass = $morphClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the queueable identity for the entity.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getQueueableId()
|
||||
{
|
||||
if (isset($this->attributes[$this->getKeyName()])) {
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s:%s:%s:%s:%s:%s',
|
||||
$this->foreignKey, $this->getAttribute($this->foreignKey),
|
||||
$this->relatedKey, $this->getAttribute($this->relatedKey),
|
||||
$this->morphType, $this->morphClass
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query to restore one or more models by their queueable IDs.
|
||||
*
|
||||
* @param array|int $ids
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
public function newQueryForRestoration($ids)
|
||||
{
|
||||
if (is_array($ids)) {
|
||||
return $this->newQueryForCollectionRestoration($ids);
|
||||
}
|
||||
|
||||
if (! str_contains($ids, ':')) {
|
||||
return parent::newQueryForRestoration($ids);
|
||||
}
|
||||
|
||||
$segments = explode(':', $ids);
|
||||
|
||||
return $this->newQueryWithoutScopes()
|
||||
->where($segments[0], $segments[1])
|
||||
->where($segments[2], $segments[3])
|
||||
->where($segments[4], $segments[5]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query to restore multiple models by their queueable IDs.
|
||||
*
|
||||
* @param array $ids
|
||||
* @return \Illuminate\Database\Eloquent\Builder<static>
|
||||
*/
|
||||
protected function newQueryForCollectionRestoration(array $ids)
|
||||
{
|
||||
$ids = array_values($ids);
|
||||
|
||||
if (! str_contains($ids[0], ':')) {
|
||||
return parent::newQueryForRestoration($ids);
|
||||
}
|
||||
|
||||
$query = $this->newQueryWithoutScopes();
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$segments = explode(':', $id);
|
||||
|
||||
$query->orWhere(function ($query) use ($segments) {
|
||||
return $query->where($segments[0], $segments[1])
|
||||
->where($segments[2], $segments[3])
|
||||
->where($segments[4], $segments[5]);
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
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)
|
||||
{
|
||||
$isAssociative = Arr::isAssoc($models->all());
|
||||
|
||||
foreach ($models as $key => $model) {
|
||||
if ($model->{$this->morphType}) {
|
||||
$morphTypeKey = $this->getDictionaryKey($model->{$this->morphType});
|
||||
$foreignKeyKey = $this->getDictionaryKey($model->{$this->foreignKey});
|
||||
|
||||
if ($morphTypeKey === null || $foreignKeyKey === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isAssociative) {
|
||||
$this->dictionary[$morphTypeKey][$foreignKeyKey][$key] = $model;
|
||||
} else {
|
||||
$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 = $this->getDictionaryKey(! is_null($this->ownerKey) ? $result->{$this->ownerKey} : $result->getKey());
|
||||
|
||||
if ($ownerKey !== null && 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,233 @@
|
||||
<?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
|
||||
* @template TPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot = \Illuminate\Database\Eloquent\Relations\MorphPivot
|
||||
* @template TAccessor of string = 'pivot'
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, TDeclaringModel, TPivotModel, TAccessor>
|
||||
*/
|
||||
class MorphToMany extends BelongsToMany
|
||||
{
|
||||
/**
|
||||
* The type of the polymorphic relation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* The morph class of the morph type constraint.
|
||||
*
|
||||
* @var class-string|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
|
||||
*/
|
||||
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, filtered by related model keys.
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @return \Illuminate\Support\Collection<int, TPivotModel>
|
||||
*/
|
||||
protected function getCurrentlyAttachedPivotsForIds($ids = null)
|
||||
{
|
||||
return parent::getCurrentlyAttachedPivotsForIds($ids)->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 TPivotModel
|
||||
*/
|
||||
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()
|
||||
{
|
||||
return (new Collection([
|
||||
$this->foreignPivotKey,
|
||||
$this->relatedPivotKey,
|
||||
$this->morphType,
|
||||
...$this->pivotColumns,
|
||||
]))
|
||||
->map(fn ($column) => $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 class-string|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 = [];
|
||||
}
|
||||
+550
@@ -0,0 +1,550 @@
|
||||
<?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 morph names to their class names in the database.
|
||||
*
|
||||
* @var array<string, class-string<\Illuminate\Database\Eloquent\Model>>
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @template TReturn of mixed
|
||||
*
|
||||
* @param Closure(): TReturn $callback
|
||||
* @return TReturn
|
||||
*/
|
||||
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->related->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->limit(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 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<array-key, class-string<\Illuminate\Database\Eloquent\Model>> $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<array-key, class-string<\Illuminate\Database\Eloquent\Model>>|null $map
|
||||
* @param bool $merge
|
||||
* @return array<string, class-string<\Illuminate\Database\Eloquent\Model>>
|
||||
*/
|
||||
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 array<array-key, class-string<\Illuminate\Database\Eloquent\Model>>|null $models
|
||||
* @return array<string, class-string<\Illuminate\Database\Eloquent\Model>>|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 class-string<\Illuminate\Database\Eloquent\Model>|null
|
||||
*/
|
||||
public static function getMorphedModel($alias)
|
||||
{
|
||||
return static::$morphMap[$alias] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the alias associated with a custom polymorphic class.
|
||||
*
|
||||
* @param class-string<\Illuminate\Database\Eloquent\Model> $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user