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