1<?php
2
3namespace Illuminate\Database\Eloquent\Relations;
4
5use BadMethodCallException;
6use Illuminate\Database\Eloquent\Builder;
7use Illuminate\Database\Eloquent\Collection;
8use Illuminate\Database\Eloquent\Model;
9use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
10
11class MorphTo extends BelongsTo
12{
13    use InteractsWithDictionary;
14
15    /**
16     * The type of the polymorphic relation.
17     *
18     * @var string
19     */
20    protected $morphType;
21
22    /**
23     * The models whose relations are being eager loaded.
24     *
25     * @var \Illuminate\Database\Eloquent\Collection
26     */
27    protected $models;
28
29    /**
30     * All of the models keyed by ID.
31     *
32     * @var array
33     */
34    protected $dictionary = [];
35
36    /**
37     * A buffer of dynamic calls to query macros.
38     *
39     * @var array
40     */
41    protected $macroBuffer = [];
42
43    /**
44     * A map of relations to load for each individual morph type.
45     *
46     * @var array
47     */
48    protected $morphableEagerLoads = [];
49
50    /**
51     * A map of relationship counts to load for each individual morph type.
52     *
53     * @var array
54     */
55    protected $morphableEagerLoadCounts = [];
56
57    /**
58     * A map of constraints to apply for each individual morph type.
59     *
60     * @var array
61     */
62    protected $morphableConstraints = [];
63
64    /**
65     * Create a new morph to relationship instance.
66     *
67     * @param  \Illuminate\Database\Eloquent\Builder  $query
68     * @param  \Illuminate\Database\Eloquent\Model  $parent
69     * @param  string  $foreignKey
70     * @param  string  $ownerKey
71     * @param  string  $type
72     * @param  string  $relation
73     * @return void
74     */
75    public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation)
76    {
77        $this->morphType = $type;
78
79        parent::__construct($query, $parent, $foreignKey, $ownerKey, $relation);
80    }
81
82    /**
83     * Set the constraints for an eager load of the relation.
84     *
85     * @param  array  $models
86     * @return void
87     */
88    public function addEagerConstraints(array $models)
89    {
90        $this->buildDictionary($this->models = Collection::make($models));
91    }
92
93    /**
94     * Build a dictionary with the models.
95     *
96     * @param  \Illuminate\Database\Eloquent\Collection  $models
97     * @return void
98     */
99    protected function buildDictionary(Collection $models)
100    {
101        foreach ($models as $model) {
102            if ($model->{$this->morphType}) {
103                $morphTypeKey = $this->getDictionaryKey($model->{$this->morphType});
104                $foreignKeyKey = $this->getDictionaryKey($model->{$this->foreignKey});
105
106                $this->dictionary[$morphTypeKey][$foreignKeyKey][] = $model;
107            }
108        }
109    }
110
111    /**
112     * Get the results of the relationship.
113     *
114     * Called via eager load method of Eloquent query builder.
115     *
116     * @return mixed
117     */
118    public function getEager()
119    {
120        foreach (array_keys($this->dictionary) as $type) {
121            $this->matchToMorphParents($type, $this->getResultsByType($type));
122        }
123
124        return $this->models;
125    }
126
127    /**
128     * Get all of the relation results for a type.
129     *
130     * @param  string  $type
131     * @return \Illuminate\Database\Eloquent\Collection
132     */
133    protected function getResultsByType($type)
134    {
135        $instance = $this->createModelByType($type);
136
137        $ownerKey = $this->ownerKey ?? $instance->getKeyName();
138
139        $query = $this->replayMacros($instance->newQuery())
140                            ->mergeConstraintsFrom($this->getQuery())
141                            ->with(array_merge(
142                                $this->getQuery()->getEagerLoads(),
143                                (array) ($this->morphableEagerLoads[get_class($instance)] ?? [])
144                            ))
145                            ->withCount(
146                                (array) ($this->morphableEagerLoadCounts[get_class($instance)] ?? [])
147                            );
148
149        if ($callback = ($this->morphableConstraints[get_class($instance)] ?? null)) {
150            $callback($query);
151        }
152
153        $whereIn = $this->whereInMethod($instance, $ownerKey);
154
155        return $query->{$whereIn}(
156            $instance->getTable().'.'.$ownerKey, $this->gatherKeysByType($type, $instance->getKeyType())
157        )->get();
158    }
159
160    /**
161     * Gather all of the foreign keys for a given type.
162     *
163     * @param  string  $type
164     * @param  string  $keyType
165     * @return array
166     */
167    protected function gatherKeysByType($type, $keyType)
168    {
169        return $keyType !== 'string'
170                    ? array_keys($this->dictionary[$type])
171                    : array_map(function ($modelId) {
172                        return (string) $modelId;
173                    }, array_filter(array_keys($this->dictionary[$type])));
174    }
175
176    /**
177     * Create a new model instance by type.
178     *
179     * @param  string  $type
180     * @return \Illuminate\Database\Eloquent\Model
181     */
182    public function createModelByType($type)
183    {
184        $class = Model::getActualClassNameForMorph($type);
185
186        return tap(new $class, function ($instance) {
187            if (! $instance->getConnectionName()) {
188                $instance->setConnection($this->getConnection()->getName());
189            }
190        });
191    }
192
193    /**
194     * Match the eagerly loaded results to their parents.
195     *
196     * @param  array  $models
197     * @param  \Illuminate\Database\Eloquent\Collection  $results
198     * @param  string  $relation
199     * @return array
200     */
201    public function match(array $models, Collection $results, $relation)
202    {
203        return $models;
204    }
205
206    /**
207     * Match the results for a given type to their parents.
208     *
209     * @param  string  $type
210     * @param  \Illuminate\Database\Eloquent\Collection  $results
211     * @return void
212     */
213    protected function matchToMorphParents($type, Collection $results)
214    {
215        foreach ($results as $result) {
216            $ownerKey = ! is_null($this->ownerKey) ? $this->getDictionaryKey($result->{$this->ownerKey}) : $result->getKey();
217
218            if (isset($this->dictionary[$type][$ownerKey])) {
219                foreach ($this->dictionary[$type][$ownerKey] as $model) {
220                    $model->setRelation($this->relationName, $result);
221                }
222            }
223        }
224    }
225
226    /**
227     * Associate the model instance to the given parent.
228     *
229     * @param  \Illuminate\Database\Eloquent\Model  $model
230     * @return \Illuminate\Database\Eloquent\Model
231     */
232    public function associate($model)
233    {
234        if ($model instanceof Model) {
235            $foreignKey = $this->ownerKey && $model->{$this->ownerKey}
236                            ? $this->ownerKey
237                            : $model->getKeyName();
238        }
239
240        $this->parent->setAttribute(
241            $this->foreignKey, $model instanceof Model ? $model->{$foreignKey} : null
242        );
243
244        $this->parent->setAttribute(
245            $this->morphType, $model instanceof Model ? $model->getMorphClass() : null
246        );
247
248        return $this->parent->setRelation($this->relationName, $model);
249    }
250
251    /**
252     * Dissociate previously associated model from the given parent.
253     *
254     * @return \Illuminate\Database\Eloquent\Model
255     */
256    public function dissociate()
257    {
258        $this->parent->setAttribute($this->foreignKey, null);
259
260        $this->parent->setAttribute($this->morphType, null);
261
262        return $this->parent->setRelation($this->relationName, null);
263    }
264
265    /**
266     * Touch all of the related models for the relationship.
267     *
268     * @return void
269     */
270    public function touch()
271    {
272        if (! is_null($this->child->{$this->foreignKey})) {
273            parent::touch();
274        }
275    }
276
277    /**
278     * Make a new related instance for the given model.
279     *
280     * @param  \Illuminate\Database\Eloquent\Model  $parent
281     * @return \Illuminate\Database\Eloquent\Model
282     */
283    protected function newRelatedInstanceFor(Model $parent)
284    {
285        return $parent->{$this->getRelationName()}()->getRelated()->newInstance();
286    }
287
288    /**
289     * Get the foreign key "type" name.
290     *
291     * @return string
292     */
293    public function getMorphType()
294    {
295        return $this->morphType;
296    }
297
298    /**
299     * Get the dictionary used by the relationship.
300     *
301     * @return array
302     */
303    public function getDictionary()
304    {
305        return $this->dictionary;
306    }
307
308    /**
309     * Specify which relations to load for a given morph type.
310     *
311     * @param  array  $with
312     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
313     */
314    public function morphWith(array $with)
315    {
316        $this->morphableEagerLoads = array_merge(
317            $this->morphableEagerLoads, $with
318        );
319
320        return $this;
321    }
322
323    /**
324     * Specify which relationship counts to load for a given morph type.
325     *
326     * @param  array  $withCount
327     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
328     */
329    public function morphWithCount(array $withCount)
330    {
331        $this->morphableEagerLoadCounts = array_merge(
332            $this->morphableEagerLoadCounts, $withCount
333        );
334
335        return $this;
336    }
337
338    /**
339     * Specify constraints on the query for a given morph type.
340     *
341     * @param  array  $callbacks
342     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
343     */
344    public function constrain(array $callbacks)
345    {
346        $this->morphableConstraints = array_merge(
347            $this->morphableConstraints, $callbacks
348        );
349
350        return $this;
351    }
352
353    /**
354     * Replay stored macro calls on the actual related instance.
355     *
356     * @param  \Illuminate\Database\Eloquent\Builder  $query
357     * @return \Illuminate\Database\Eloquent\Builder
358     */
359    protected function replayMacros(Builder $query)
360    {
361        foreach ($this->macroBuffer as $macro) {
362            $query->{$macro['method']}(...$macro['parameters']);
363        }
364
365        return $query;
366    }
367
368    /**
369     * Handle dynamic method calls to the relationship.
370     *
371     * @param  string  $method
372     * @param  array  $parameters
373     * @return mixed
374     */
375    public function __call($method, $parameters)
376    {
377        try {
378            $result = parent::__call($method, $parameters);
379
380            if (in_array($method, ['select', 'selectRaw', 'selectSub', 'addSelect', 'withoutGlobalScopes'])) {
381                $this->macroBuffer[] = compact('method', 'parameters');
382            }
383
384            return $result;
385        }
386
387        // If we tried to call a method that does not exist on the parent Builder instance,
388        // we'll assume that we want to call a query macro (e.g. withTrashed) that only
389        // exists on related models. We will just store the call and replay it later.
390        catch (BadMethodCallException $e) {
391            $this->macroBuffer[] = compact('method', 'parameters');
392
393            return $this;
394        }
395    }
396}
397