<?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 $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;
}
/**
* 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 $e) {
$this->macroBuffer[] = compact('method', 'parameters');
return $this;
}
}
}