HasManyThrough.php 16 KB

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