HasManyThrough.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <?php
  2. namespace Illuminate\Database\Eloquent\Relations;
  3. use Illuminate\Database\Eloquent\Model;
  4. use Illuminate\Database\Eloquent\Builder;
  5. use Illuminate\Database\Eloquent\Collection;
  6. use Illuminate\Database\Eloquent\SoftDeletes;
  7. use Illuminate\Database\Eloquent\ModelNotFoundException;
  8. class HasManyThrough extends Relation
  9. {
  10. /**
  11. * The "through" parent model instance.
  12. *
  13. * @var \Illuminate\Database\Eloquent\Model
  14. */
  15. protected $throughParent;
  16. /**
  17. * The far parent model instance.
  18. *
  19. * @var \Illuminate\Database\Eloquent\Model
  20. */
  21. protected $farParent;
  22. /**
  23. * The near key on the relationship.
  24. *
  25. * @var string
  26. */
  27. protected $firstKey;
  28. /**
  29. * The far key on the relationship.
  30. *
  31. * @var string
  32. */
  33. protected $secondKey;
  34. /**
  35. * The local key on the relationship.
  36. *
  37. * @var string
  38. */
  39. protected $localKey;
  40. /**
  41. * The local key on the intermediary model.
  42. *
  43. * @var string
  44. */
  45. protected $secondLocalKey;
  46. /**
  47. * The count of self joins.
  48. *
  49. * @var int
  50. */
  51. protected static $selfJoinCount = 0;
  52. /**
  53. * Create a new has many through relationship instance.
  54. *
  55. * @param \Illuminate\Database\Eloquent\Builder $query
  56. * @param \Illuminate\Database\Eloquent\Model $farParent
  57. * @param \Illuminate\Database\Eloquent\Model $throughParent
  58. * @param string $firstKey
  59. * @param string $secondKey
  60. * @param string $localKey
  61. * @param string $secondLocalKey
  62. * @return void
  63. */
  64. public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
  65. {
  66. $this->localKey = $localKey;
  67. $this->firstKey = $firstKey;
  68. $this->secondKey = $secondKey;
  69. $this->farParent = $farParent;
  70. $this->throughParent = $throughParent;
  71. $this->secondLocalKey = $secondLocalKey;
  72. parent::__construct($query, $throughParent);
  73. }
  74. /**
  75. * Set the base constraints on the relation query.
  76. *
  77. * @return void
  78. */
  79. public function addConstraints()
  80. {
  81. $localValue = $this->farParent[$this->localKey];
  82. $this->performJoin();
  83. if (static::$constraints) {
  84. $this->query->where($this->getQualifiedFirstKeyName(), '=', $localValue);
  85. }
  86. }
  87. /**
  88. * Set the join clause on the query.
  89. *
  90. * @param \Illuminate\Database\Eloquent\Builder|null $query
  91. * @return void
  92. */
  93. protected function performJoin(Builder $query = null)
  94. {
  95. $query = $query ?: $this->query;
  96. $farKey = $this->getQualifiedFarKeyName();
  97. $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey);
  98. if ($this->throughParentSoftDeletes()) {
  99. $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
  100. }
  101. }
  102. /**
  103. * Get the fully qualified parent key name.
  104. *
  105. * @return string
  106. */
  107. public function getQualifiedParentKeyName()
  108. {
  109. return $this->parent->qualifyColumn($this->secondLocalKey);
  110. }
  111. /**
  112. * Determine whether "through" parent of the relation uses Soft Deletes.
  113. *
  114. * @return bool
  115. */
  116. public function throughParentSoftDeletes()
  117. {
  118. return in_array(SoftDeletes::class, class_uses_recursive(
  119. get_class($this->throughParent)
  120. ));
  121. }
  122. /**
  123. * Set the constraints for an eager load of the relation.
  124. *
  125. * @param array $models
  126. * @return void
  127. */
  128. public function addEagerConstraints(array $models)
  129. {
  130. $this->query->whereIn(
  131. $this->getQualifiedFirstKeyName(), $this->getKeys($models, $this->localKey)
  132. );
  133. }
  134. /**
  135. * Initialize the relation on a set of models.
  136. *
  137. * @param array $models
  138. * @param string $relation
  139. * @return array
  140. */
  141. public function initRelation(array $models, $relation)
  142. {
  143. foreach ($models as $model) {
  144. $model->setRelation($relation, $this->related->newCollection());
  145. }
  146. return $models;
  147. }
  148. /**
  149. * Match the eagerly loaded results to their parents.
  150. *
  151. * @param array $models
  152. * @param \Illuminate\Database\Eloquent\Collection $results
  153. * @param string $relation
  154. * @return array
  155. */
  156. public function match(array $models, Collection $results, $relation)
  157. {
  158. $dictionary = $this->buildDictionary($results);
  159. // Once we have the dictionary we can simply spin through the parent models to
  160. // link them up with their children using the keyed dictionary to make the
  161. // matching very convenient and easy work. Then we'll just return them.
  162. foreach ($models as $model) {
  163. if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) {
  164. $model->setRelation(
  165. $relation, $this->related->newCollection($dictionary[$key])
  166. );
  167. }
  168. }
  169. return $models;
  170. }
  171. /**
  172. * Build model dictionary keyed by the relation's foreign key.
  173. *
  174. * @param \Illuminate\Database\Eloquent\Collection $results
  175. * @return array
  176. */
  177. protected function buildDictionary(Collection $results)
  178. {
  179. $dictionary = [];
  180. // First we will create a dictionary of models keyed by the foreign key of the
  181. // relationship as this will allow us to quickly access all of the related
  182. // models without having to do nested looping which will be quite slow.
  183. foreach ($results as $result) {
  184. $dictionary[$result->{$this->firstKey}][] = $result;
  185. }
  186. return $dictionary;
  187. }
  188. /**
  189. * Get the first related model record matching the attributes or instantiate it.
  190. *
  191. * @param array $attributes
  192. * @return \Illuminate\Database\Eloquent\Model
  193. */
  194. public function firstOrNew(array $attributes)
  195. {
  196. if (is_null($instance = $this->where($attributes)->first())) {
  197. $instance = $this->related->newInstance($attributes);
  198. }
  199. return $instance;
  200. }
  201. /**
  202. * Create or update a related record matching the attributes, and fill it with values.
  203. *
  204. * @param array $attributes
  205. * @param array $values
  206. * @return \Illuminate\Database\Eloquent\Model
  207. */
  208. public function updateOrCreate(array $attributes, array $values = [])
  209. {
  210. $instance = $this->firstOrNew($attributes);
  211. $instance->fill($values)->save();
  212. return $instance;
  213. }
  214. /**
  215. * Execute the query and get the first related model.
  216. *
  217. * @param array $columns
  218. * @return mixed
  219. */
  220. public function first($columns = ['*'])
  221. {
  222. $results = $this->take(1)->get($columns);
  223. return count($results) > 0 ? $results->first() : null;
  224. }
  225. /**
  226. * Execute the query and get the first result or throw an exception.
  227. *
  228. * @param array $columns
  229. * @return \Illuminate\Database\Eloquent\Model|static
  230. *
  231. * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
  232. */
  233. public function firstOrFail($columns = ['*'])
  234. {
  235. if (! is_null($model = $this->first($columns))) {
  236. return $model;
  237. }
  238. throw (new ModelNotFoundException)->setModel(get_class($this->related));
  239. }
  240. /**
  241. * Find a related model by its primary key.
  242. *
  243. * @param mixed $id
  244. * @param array $columns
  245. * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
  246. */
  247. public function find($id, $columns = ['*'])
  248. {
  249. if (is_array($id)) {
  250. return $this->findMany($id, $columns);
  251. }
  252. return $this->where(
  253. $this->getRelated()->getQualifiedKeyName(), '=', $id
  254. )->first($columns);
  255. }
  256. /**
  257. * Find multiple related models by their primary keys.
  258. *
  259. * @param mixed $ids
  260. * @param array $columns
  261. * @return \Illuminate\Database\Eloquent\Collection
  262. */
  263. public function findMany($ids, $columns = ['*'])
  264. {
  265. if (empty($ids)) {
  266. return $this->getRelated()->newCollection();
  267. }
  268. return $this->whereIn(
  269. $this->getRelated()->getQualifiedKeyName(), $ids
  270. )->get($columns);
  271. }
  272. /**
  273. * Find a related model by its primary key or throw an exception.
  274. *
  275. * @param mixed $id
  276. * @param array $columns
  277. * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
  278. *
  279. * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
  280. */
  281. public function findOrFail($id, $columns = ['*'])
  282. {
  283. $result = $this->find($id, $columns);
  284. if (is_array($id)) {
  285. if (count($result) === count(array_unique($id))) {
  286. return $result;
  287. }
  288. } elseif (! is_null($result)) {
  289. return $result;
  290. }
  291. throw (new ModelNotFoundException)->setModel(get_class($this->related));
  292. }
  293. /**
  294. * Get the results of the relationship.
  295. *
  296. * @return mixed
  297. */
  298. public function getResults()
  299. {
  300. return $this->get();
  301. }
  302. /**
  303. * Execute the query as a "select" statement.
  304. *
  305. * @param array $columns
  306. * @return \Illuminate\Database\Eloquent\Collection
  307. */
  308. public function get($columns = ['*'])
  309. {
  310. $builder = $this->prepareQueryBuilder($columns);
  311. $models = $builder->getModels();
  312. // If we actually found models we will also eager load any relationships that
  313. // have been specified as needing to be eager loaded. This will solve the
  314. // n + 1 query problem for the developer and also increase performance.
  315. if (count($models) > 0) {
  316. $models = $builder->eagerLoadRelations($models);
  317. }
  318. return $this->related->newCollection($models);
  319. }
  320. /**
  321. * Get a paginator for the "select" statement.
  322. *
  323. * @param int $perPage
  324. * @param array $columns
  325. * @param string $pageName
  326. * @param int $page
  327. * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
  328. */
  329. public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
  330. {
  331. $this->query->addSelect($this->shouldSelect($columns));
  332. return $this->query->paginate($perPage, $columns, $pageName, $page);
  333. }
  334. /**
  335. * Paginate the given query into a simple paginator.
  336. *
  337. * @param int $perPage
  338. * @param array $columns
  339. * @param string $pageName
  340. * @param int|null $page
  341. * @return \Illuminate\Contracts\Pagination\Paginator
  342. */
  343. public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
  344. {
  345. $this->query->addSelect($this->shouldSelect($columns));
  346. return $this->query->simplePaginate($perPage, $columns, $pageName, $page);
  347. }
  348. /**
  349. * Set the select clause for the relation query.
  350. *
  351. * @param array $columns
  352. * @return array
  353. */
  354. protected function shouldSelect(array $columns = ['*'])
  355. {
  356. if ($columns == ['*']) {
  357. $columns = [$this->related->getTable().'.*'];
  358. }
  359. return array_merge($columns, [$this->getQualifiedFirstKeyName()]);
  360. }
  361. /**
  362. * Chunk the results of the query.
  363. *
  364. * @param int $count
  365. * @param callable $callback
  366. * @return bool
  367. */
  368. public function chunk($count, callable $callback)
  369. {
  370. return $this->prepareQueryBuilder()->chunk($count, $callback);
  371. }
  372. /**
  373. * Execute a callback over each item while chunking.
  374. *
  375. * @param callable $callback
  376. * @param int $count
  377. * @return bool
  378. */
  379. public function each(callable $callback, $count = 1000)
  380. {
  381. return $this->chunk($count, function ($results) use ($callback) {
  382. foreach ($results as $key => $value) {
  383. if ($callback($value, $key) === false) {
  384. return false;
  385. }
  386. }
  387. });
  388. }
  389. /**
  390. * Prepare the query builder for query execution.
  391. *
  392. * @param array $columns
  393. * @return \Illuminate\Database\Eloquent\Builder
  394. */
  395. protected function prepareQueryBuilder($columns = ['*'])
  396. {
  397. return $this->query->applyScopes()->addSelect(
  398. $this->shouldSelect($this->query->getQuery()->columns ? [] : $columns)
  399. );
  400. }
  401. /**
  402. * Add the constraints for a relationship query.
  403. *
  404. * @param \Illuminate\Database\Eloquent\Builder $query
  405. * @param \Illuminate\Database\Eloquent\Builder $parentQuery
  406. * @param array|mixed $columns
  407. * @return \Illuminate\Database\Eloquent\Builder
  408. */
  409. public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
  410. {
  411. if ($parentQuery->getQuery()->from == $query->getQuery()->from) {
  412. return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
  413. }
  414. $this->performJoin($query);
  415. return $query->select($columns)->whereColumn(
  416. $this->getQualifiedLocalKeyName(), '=', $this->getQualifiedFirstKeyName()
  417. );
  418. }
  419. /**
  420. * Add the constraints for a relationship query on the same table.
  421. *
  422. * @param \Illuminate\Database\Eloquent\Builder $query
  423. * @param \Illuminate\Database\Eloquent\Builder $parentQuery
  424. * @param array|mixed $columns
  425. * @return \Illuminate\Database\Eloquent\Builder
  426. */
  427. public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
  428. {
  429. $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());
  430. $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->secondLocalKey);
  431. if ($this->throughParentSoftDeletes()) {
  432. $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
  433. }
  434. $query->getModel()->setTable($hash);
  435. return $query->select($columns)->whereColumn(
  436. $parentQuery->getQuery()->from.'.'.$query->getModel()->getKeyName(), '=', $this->getQualifiedFirstKeyName()
  437. );
  438. }
  439. /**
  440. * Get a relationship join table hash.
  441. *
  442. * @return string
  443. */
  444. public function getRelationCountHash()
  445. {
  446. return 'laravel_reserved_'.static::$selfJoinCount++;
  447. }
  448. /**
  449. * Get the qualified foreign key on the related model.
  450. *
  451. * @return string
  452. */
  453. public function getQualifiedFarKeyName()
  454. {
  455. return $this->getQualifiedForeignKeyName();
  456. }
  457. /**
  458. * Get the qualified foreign key on the "through" model.
  459. *
  460. * @return string
  461. */
  462. public function getQualifiedFirstKeyName()
  463. {
  464. return $this->throughParent->qualifyColumn($this->firstKey);
  465. }
  466. /**
  467. * Get the qualified foreign key on the related model.
  468. *
  469. * @return string
  470. */
  471. public function getQualifiedForeignKeyName()
  472. {
  473. return $this->related->qualifyColumn($this->secondKey);
  474. }
  475. /**
  476. * Get the qualified local key on the far parent model.
  477. *
  478. * @return string
  479. */
  480. public function getQualifiedLocalKeyName()
  481. {
  482. return $this->farParent->qualifyColumn($this->localKey);
  483. }
  484. }