448 lines
12 KiB
PHP
448 lines
12 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Illuminate\Database\Eloquent\Relations;
|
||
|
|
||
|
use BadMethodCallException;
|
||
|
use Illuminate\Database\Eloquent\Builder;
|
||
|
use Illuminate\Database\Eloquent\Collection;
|
||
|
use Illuminate\Database\Eloquent\Model;
|
||
|
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
||
|
|
||
|
class MorphTo extends BelongsTo
|
||
|
{
|
||
|
use InteractsWithDictionary;
|
||
|
|
||
|
/**
|
||
|
* The type of the polymorphic relation.
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
protected $morphType;
|
||
|
|
||
|
/**
|
||
|
* The models whose relations are being eager loaded.
|
||
|
*
|
||
|
* @var \Illuminate\Database\Eloquent\Collection
|
||
|
*/
|
||
|
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 $query
|
||
|
* @param \Illuminate\Database\Eloquent\Model $parent
|
||
|
* @param string $foreignKey
|
||
|
* @param string $ownerKey
|
||
|
* @param string $type
|
||
|
* @param string $relation
|
||
|
* @return void
|
||
|
*/
|
||
|
public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation)
|
||
|
{
|
||
|
$this->morphType = $type;
|
||
|
|
||
|
parent::__construct($query, $parent, $foreignKey, $ownerKey, $relation);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the constraints for an eager load of the relation.
|
||
|
*
|
||
|
* @param array $models
|
||
|
* @return void
|
||
|
*/
|
||
|
public function addEagerConstraints(array $models)
|
||
|
{
|
||
|
$this->buildDictionary($this->models = Collection::make($models));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Build a dictionary with the models.
|
||
|
*
|
||
|
* @param \Illuminate\Database\Eloquent\Collection $models
|
||
|
* @return void
|
||
|
*/
|
||
|
protected function buildDictionary(Collection $models)
|
||
|
{
|
||
|
foreach ($models as $model) {
|
||
|
if ($model->{$this->morphType}) {
|
||
|
$morphTypeKey = $this->getDictionaryKey($model->{$this->morphType});
|
||
|
$foreignKeyKey = $this->getDictionaryKey($model->{$this->foreignKey});
|
||
|
|
||
|
$this->dictionary[$morphTypeKey][$foreignKeyKey][] = $model;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the results of the relationship.
|
||
|
*
|
||
|
* Called via eager load method of Eloquent query builder.
|
||
|
*
|
||
|
* @return mixed
|
||
|
*/
|
||
|
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
|
||
|
*/
|
||
|
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->getTable().'.'.$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 \Illuminate\Database\Eloquent\Model
|
||
|
*/
|
||
|
public function createModelByType($type)
|
||
|
{
|
||
|
$class = Model::getActualClassNameForMorph($type);
|
||
|
|
||
|
return tap(new $class, function ($instance) {
|
||
|
if (! $instance->getConnectionName()) {
|
||
|
$instance->setConnection($this->getConnection()->getName());
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Match the eagerly loaded results to their parents.
|
||
|
*
|
||
|
* @param array $models
|
||
|
* @param \Illuminate\Database\Eloquent\Collection $results
|
||
|
* @param string $relation
|
||
|
* @return array
|
||
|
*/
|
||
|
public function match(array $models, Collection $results, $relation)
|
||
|
{
|
||
|
return $models;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Match the results for a given type to their parents.
|
||
|
*
|
||
|
* @param string $type
|
||
|
* @param \Illuminate\Database\Eloquent\Collection $results
|
||
|
* @return void
|
||
|
*/
|
||
|
protected function matchToMorphParents($type, Collection $results)
|
||
|
{
|
||
|
foreach ($results as $result) {
|
||
|
$ownerKey = ! is_null($this->ownerKey) ? $this->getDictionaryKey($result->{$this->ownerKey}) : $result->getKey();
|
||
|
|
||
|
if (isset($this->dictionary[$type][$ownerKey])) {
|
||
|
foreach ($this->dictionary[$type][$ownerKey] as $model) {
|
||
|
$model->setRelation($this->relationName, $result);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Associate the model instance to the given parent.
|
||
|
*
|
||
|
* @param \Illuminate\Database\Eloquent\Model|null $model
|
||
|
* @return \Illuminate\Database\Eloquent\Model
|
||
|
*/
|
||
|
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 \Illuminate\Database\Eloquent\Model
|
||
|
*/
|
||
|
public function dissociate()
|
||
|
{
|
||
|
$this->parent->setAttribute($this->foreignKey, null);
|
||
|
|
||
|
$this->parent->setAttribute($this->morphType, null);
|
||
|
|
||
|
return $this->parent->setRelation($this->relationName, null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Touch all of the related models for the relationship.
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
public function touch()
|
||
|
{
|
||
|
if (! is_null($this->child->{$this->foreignKey})) {
|
||
|
parent::touch();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make a new related instance for the given model.
|
||
|
*
|
||
|
* @param \Illuminate\Database\Eloquent\Model $parent
|
||
|
* @return \Illuminate\Database\Eloquent\Model
|
||
|
*/
|
||
|
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 \Illuminate\Database\Eloquent\Relations\MorphTo
|
||
|
*/
|
||
|
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 \Illuminate\Database\Eloquent\Relations\MorphTo
|
||
|
*/
|
||
|
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 \Illuminate\Database\Eloquent\Relations\MorphTo
|
||
|
*/
|
||
|
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 $query
|
||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||
|
*/
|
||
|
protected function replayMacros(Builder $query)
|
||
|
{
|
||
|
foreach ($this->macroBuffer as $macro) {
|
||
|
$query->{$macro['method']}(...$macro['parameters']);
|
||
|
}
|
||
|
|
||
|
return $query;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
}
|
||
|
}
|