QueriesRelationships.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. <?php
  2. namespace Illuminate\Database\Eloquent\Concerns;
  3. use BadMethodCallException;
  4. use Closure;
  5. use Illuminate\Database\Eloquent\Builder;
  6. use Illuminate\Database\Eloquent\Collection;
  7. use Illuminate\Database\Eloquent\RelationNotFoundException;
  8. use Illuminate\Database\Eloquent\Relations\BelongsTo;
  9. use Illuminate\Database\Eloquent\Relations\MorphTo;
  10. use Illuminate\Database\Eloquent\Relations\Relation;
  11. use Illuminate\Database\Query\Builder as QueryBuilder;
  12. use Illuminate\Database\Query\Expression;
  13. use Illuminate\Support\Str;
  14. use InvalidArgumentException;
  15. trait QueriesRelationships
  16. {
  17. /**
  18. * Add a relationship count / exists condition to the query.
  19. *
  20. * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation
  21. * @param string $operator
  22. * @param int $count
  23. * @param string $boolean
  24. * @param \Closure|null $callback
  25. * @return \Illuminate\Database\Eloquent\Builder|static
  26. *
  27. * @throws \RuntimeException
  28. */
  29. public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
  30. {
  31. if (is_string($relation)) {
  32. if (str_contains($relation, '.')) {
  33. return $this->hasNested($relation, $operator, $count, $boolean, $callback);
  34. }
  35. $relation = $this->getRelationWithoutConstraints($relation);
  36. }
  37. if ($relation instanceof MorphTo) {
  38. return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback);
  39. }
  40. // If we only need to check for the existence of the relation, then we can optimize
  41. // the subquery to only run a "where exists" clause instead of this full "count"
  42. // clause. This will make these queries run much faster compared with a count.
  43. $method = $this->canUseExistsForExistenceCheck($operator, $count)
  44. ? 'getRelationExistenceQuery'
  45. : 'getRelationExistenceCountQuery';
  46. $hasQuery = $relation->{$method}(
  47. $relation->getRelated()->newQueryWithoutRelationships(), $this
  48. );
  49. // Next we will call any given callback as an "anonymous" scope so they can get the
  50. // proper logical grouping of the where clauses if needed by this Eloquent query
  51. // builder. Then, we will be ready to finalize and return this query instance.
  52. if ($callback) {
  53. $hasQuery->callScope($callback);
  54. }
  55. return $this->addHasWhere(
  56. $hasQuery, $relation, $operator, $count, $boolean
  57. );
  58. }
  59. /**
  60. * Add nested relationship count / exists conditions to the query.
  61. *
  62. * Sets up recursive call to whereHas until we finish the nested relation.
  63. *
  64. * @param string $relations
  65. * @param string $operator
  66. * @param int $count
  67. * @param string $boolean
  68. * @param \Closure|null $callback
  69. * @return \Illuminate\Database\Eloquent\Builder|static
  70. */
  71. protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
  72. {
  73. $relations = explode('.', $relations);
  74. $doesntHave = $operator === '<' && $count === 1;
  75. if ($doesntHave) {
  76. $operator = '>=';
  77. $count = 1;
  78. }
  79. $closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback) {
  80. // In order to nest "has", we need to add count relation constraints on the
  81. // callback Closure. We'll do this by simply passing the Closure its own
  82. // reference to itself so it calls itself recursively on each segment.
  83. count($relations) > 1
  84. ? $q->whereHas(array_shift($relations), $closure)
  85. : $q->has(array_shift($relations), $operator, $count, 'and', $callback);
  86. };
  87. return $this->has(array_shift($relations), $doesntHave ? '<' : '>=', 1, $boolean, $closure);
  88. }
  89. /**
  90. * Add a relationship count / exists condition to the query with an "or".
  91. *
  92. * @param string $relation
  93. * @param string $operator
  94. * @param int $count
  95. * @return \Illuminate\Database\Eloquent\Builder|static
  96. */
  97. public function orHas($relation, $operator = '>=', $count = 1)
  98. {
  99. return $this->has($relation, $operator, $count, 'or');
  100. }
  101. /**
  102. * Add a relationship count / exists condition to the query.
  103. *
  104. * @param string $relation
  105. * @param string $boolean
  106. * @param \Closure|null $callback
  107. * @return \Illuminate\Database\Eloquent\Builder|static
  108. */
  109. public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null)
  110. {
  111. return $this->has($relation, '<', 1, $boolean, $callback);
  112. }
  113. /**
  114. * Add a relationship count / exists condition to the query with an "or".
  115. *
  116. * @param string $relation
  117. * @return \Illuminate\Database\Eloquent\Builder|static
  118. */
  119. public function orDoesntHave($relation)
  120. {
  121. return $this->doesntHave($relation, 'or');
  122. }
  123. /**
  124. * Add a relationship count / exists condition to the query with where clauses.
  125. *
  126. * @param string $relation
  127. * @param \Closure|null $callback
  128. * @param string $operator
  129. * @param int $count
  130. * @return \Illuminate\Database\Eloquent\Builder|static
  131. */
  132. public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
  133. {
  134. return $this->has($relation, $operator, $count, 'and', $callback);
  135. }
  136. /**
  137. * Add a relationship count / exists condition to the query with where clauses.
  138. *
  139. * Also load the relationship with same condition.
  140. *
  141. * @param string $relation
  142. * @param \Closure|null $callback
  143. * @param string $operator
  144. * @param int $count
  145. * @return \Illuminate\Database\Eloquent\Builder|static
  146. */
  147. public function withWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
  148. {
  149. return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count)
  150. ->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation);
  151. }
  152. /**
  153. * Add a relationship count / exists condition to the query with where clauses and an "or".
  154. *
  155. * @param string $relation
  156. * @param \Closure|null $callback
  157. * @param string $operator
  158. * @param int $count
  159. * @return \Illuminate\Database\Eloquent\Builder|static
  160. */
  161. public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
  162. {
  163. return $this->has($relation, $operator, $count, 'or', $callback);
  164. }
  165. /**
  166. * Add a relationship count / exists condition to the query with where clauses.
  167. *
  168. * @param string $relation
  169. * @param \Closure|null $callback
  170. * @return \Illuminate\Database\Eloquent\Builder|static
  171. */
  172. public function whereDoesntHave($relation, ?Closure $callback = null)
  173. {
  174. return $this->doesntHave($relation, 'and', $callback);
  175. }
  176. /**
  177. * Add a relationship count / exists condition to the query with where clauses and an "or".
  178. *
  179. * @param string $relation
  180. * @param \Closure|null $callback
  181. * @return \Illuminate\Database\Eloquent\Builder|static
  182. */
  183. public function orWhereDoesntHave($relation, ?Closure $callback = null)
  184. {
  185. return $this->doesntHave($relation, 'or', $callback);
  186. }
  187. /**
  188. * Add a polymorphic relationship count / exists condition to the query.
  189. *
  190. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  191. * @param string|array $types
  192. * @param string $operator
  193. * @param int $count
  194. * @param string $boolean
  195. * @param \Closure|null $callback
  196. * @return \Illuminate\Database\Eloquent\Builder|static
  197. */
  198. public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
  199. {
  200. if (is_string($relation)) {
  201. $relation = $this->getRelationWithoutConstraints($relation);
  202. }
  203. $types = (array) $types;
  204. if ($types === ['*']) {
  205. $types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all();
  206. }
  207. if (empty($types)) {
  208. return $this->where(new Expression('0'), $operator, $count, $boolean);
  209. }
  210. foreach ($types as &$type) {
  211. $type = Relation::getMorphedModel($type) ?? $type;
  212. }
  213. return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types) {
  214. foreach ($types as $type) {
  215. $query->orWhere(function ($query) use ($relation, $callback, $operator, $count, $type) {
  216. $belongsTo = $this->getBelongsToRelation($relation, $type);
  217. if ($callback) {
  218. $callback = function ($query) use ($callback, $type) {
  219. return $callback($query, $type);
  220. };
  221. }
  222. $query->where($this->qualifyColumn($relation->getMorphType()), '=', (new $type)->getMorphClass())
  223. ->whereHas($belongsTo, $callback, $operator, $count);
  224. });
  225. }
  226. }, null, null, $boolean);
  227. }
  228. /**
  229. * Get the BelongsTo relationship for a single polymorphic type.
  230. *
  231. * @param \Illuminate\Database\Eloquent\Relations\MorphTo $relation
  232. * @param string $type
  233. * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
  234. */
  235. protected function getBelongsToRelation(MorphTo $relation, $type)
  236. {
  237. $belongsTo = Relation::noConstraints(function () use ($relation, $type) {
  238. return $this->model->belongsTo(
  239. $type,
  240. $relation->getForeignKeyName(),
  241. $relation->getOwnerKeyName()
  242. );
  243. });
  244. $belongsTo->getQuery()->mergeConstraintsFrom($relation->getQuery());
  245. return $belongsTo;
  246. }
  247. /**
  248. * Add a polymorphic relationship count / exists condition to the query with an "or".
  249. *
  250. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  251. * @param string|array $types
  252. * @param string $operator
  253. * @param int $count
  254. * @return \Illuminate\Database\Eloquent\Builder|static
  255. */
  256. public function orHasMorph($relation, $types, $operator = '>=', $count = 1)
  257. {
  258. return $this->hasMorph($relation, $types, $operator, $count, 'or');
  259. }
  260. /**
  261. * Add a polymorphic relationship count / exists condition to the query.
  262. *
  263. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  264. * @param string|array $types
  265. * @param string $boolean
  266. * @param \Closure|null $callback
  267. * @return \Illuminate\Database\Eloquent\Builder|static
  268. */
  269. public function doesntHaveMorph($relation, $types, $boolean = 'and', ?Closure $callback = null)
  270. {
  271. return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback);
  272. }
  273. /**
  274. * Add a polymorphic relationship count / exists condition to the query with an "or".
  275. *
  276. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  277. * @param string|array $types
  278. * @return \Illuminate\Database\Eloquent\Builder|static
  279. */
  280. public function orDoesntHaveMorph($relation, $types)
  281. {
  282. return $this->doesntHaveMorph($relation, $types, 'or');
  283. }
  284. /**
  285. * Add a polymorphic relationship count / exists condition to the query with where clauses.
  286. *
  287. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  288. * @param string|array $types
  289. * @param \Closure|null $callback
  290. * @param string $operator
  291. * @param int $count
  292. * @return \Illuminate\Database\Eloquent\Builder|static
  293. */
  294. public function whereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1)
  295. {
  296. return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback);
  297. }
  298. /**
  299. * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
  300. *
  301. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  302. * @param string|array $types
  303. * @param \Closure|null $callback
  304. * @param string $operator
  305. * @param int $count
  306. * @return \Illuminate\Database\Eloquent\Builder|static
  307. */
  308. public function orWhereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1)
  309. {
  310. return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback);
  311. }
  312. /**
  313. * Add a polymorphic relationship count / exists condition to the query with where clauses.
  314. *
  315. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  316. * @param string|array $types
  317. * @param \Closure|null $callback
  318. * @return \Illuminate\Database\Eloquent\Builder|static
  319. */
  320. public function whereDoesntHaveMorph($relation, $types, ?Closure $callback = null)
  321. {
  322. return $this->doesntHaveMorph($relation, $types, 'and', $callback);
  323. }
  324. /**
  325. * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
  326. *
  327. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  328. * @param string|array $types
  329. * @param \Closure|null $callback
  330. * @return \Illuminate\Database\Eloquent\Builder|static
  331. */
  332. public function orWhereDoesntHaveMorph($relation, $types, ?Closure $callback = null)
  333. {
  334. return $this->doesntHaveMorph($relation, $types, 'or', $callback);
  335. }
  336. /**
  337. * Add a basic where clause to a relationship query.
  338. *
  339. * @param string $relation
  340. * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
  341. * @param mixed $operator
  342. * @param mixed $value
  343. * @return \Illuminate\Database\Eloquent\Builder|static
  344. */
  345. public function whereRelation($relation, $column, $operator = null, $value = null)
  346. {
  347. return $this->whereHas($relation, function ($query) use ($column, $operator, $value) {
  348. if ($column instanceof Closure) {
  349. $column($query);
  350. } else {
  351. $query->where($column, $operator, $value);
  352. }
  353. });
  354. }
  355. /**
  356. * Add an "or where" clause to a relationship query.
  357. *
  358. * @param string $relation
  359. * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
  360. * @param mixed $operator
  361. * @param mixed $value
  362. * @return \Illuminate\Database\Eloquent\Builder|static
  363. */
  364. public function orWhereRelation($relation, $column, $operator = null, $value = null)
  365. {
  366. return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) {
  367. if ($column instanceof Closure) {
  368. $column($query);
  369. } else {
  370. $query->where($column, $operator, $value);
  371. }
  372. });
  373. }
  374. /**
  375. * Add a polymorphic relationship condition to the query with a where clause.
  376. *
  377. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  378. * @param string|array $types
  379. * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
  380. * @param mixed $operator
  381. * @param mixed $value
  382. * @return \Illuminate\Database\Eloquent\Builder|static
  383. */
  384. public function whereMorphRelation($relation, $types, $column, $operator = null, $value = null)
  385. {
  386. return $this->whereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) {
  387. $query->where($column, $operator, $value);
  388. });
  389. }
  390. /**
  391. * Add a polymorphic relationship condition to the query with an "or where" clause.
  392. *
  393. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  394. * @param string|array $types
  395. * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
  396. * @param mixed $operator
  397. * @param mixed $value
  398. * @return \Illuminate\Database\Eloquent\Builder|static
  399. */
  400. public function orWhereMorphRelation($relation, $types, $column, $operator = null, $value = null)
  401. {
  402. return $this->orWhereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) {
  403. $query->where($column, $operator, $value);
  404. });
  405. }
  406. /**
  407. * Add a morph-to relationship condition to the query.
  408. *
  409. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  410. * @param \Illuminate\Database\Eloquent\Model|string|null $model
  411. * @return \Illuminate\Database\Eloquent\Builder|static
  412. */
  413. public function whereMorphedTo($relation, $model, $boolean = 'and')
  414. {
  415. if (is_string($relation)) {
  416. $relation = $this->getRelationWithoutConstraints($relation);
  417. }
  418. if (is_null($model)) {
  419. return $this->whereNull($relation->getMorphType(), $boolean);
  420. }
  421. if (is_string($model)) {
  422. $morphMap = Relation::morphMap();
  423. if (! empty($morphMap) && in_array($model, $morphMap)) {
  424. $model = array_search($model, $morphMap, true);
  425. }
  426. return $this->where($relation->getMorphType(), $model, null, $boolean);
  427. }
  428. return $this->where(function ($query) use ($relation, $model) {
  429. $query->where($relation->getMorphType(), $model->getMorphClass())
  430. ->where($relation->getForeignKeyName(), $model->getKey());
  431. }, null, null, $boolean);
  432. }
  433. /**
  434. * Add a not morph-to relationship condition to the query.
  435. *
  436. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  437. * @param \Illuminate\Database\Eloquent\Model|string $model
  438. * @return \Illuminate\Database\Eloquent\Builder|static
  439. */
  440. public function whereNotMorphedTo($relation, $model, $boolean = 'and')
  441. {
  442. if (is_string($relation)) {
  443. $relation = $this->getRelationWithoutConstraints($relation);
  444. }
  445. if (is_string($model)) {
  446. $morphMap = Relation::morphMap();
  447. if (! empty($morphMap) && in_array($model, $morphMap)) {
  448. $model = array_search($model, $morphMap, true);
  449. }
  450. return $this->whereNot($relation->getMorphType(), '<=>', $model, $boolean);
  451. }
  452. return $this->whereNot(function ($query) use ($relation, $model) {
  453. $query->where($relation->getMorphType(), '<=>', $model->getMorphClass())
  454. ->where($relation->getForeignKeyName(), '<=>', $model->getKey());
  455. }, null, null, $boolean);
  456. }
  457. /**
  458. * Add a morph-to relationship condition to the query with an "or where" clause.
  459. *
  460. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  461. * @param \Illuminate\Database\Eloquent\Model|string|null $model
  462. * @return \Illuminate\Database\Eloquent\Builder|static
  463. */
  464. public function orWhereMorphedTo($relation, $model)
  465. {
  466. return $this->whereMorphedTo($relation, $model, 'or');
  467. }
  468. /**
  469. * Add a not morph-to relationship condition to the query with an "or where" clause.
  470. *
  471. * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
  472. * @param \Illuminate\Database\Eloquent\Model|string $model
  473. * @return \Illuminate\Database\Eloquent\Builder|static
  474. */
  475. public function orWhereNotMorphedTo($relation, $model)
  476. {
  477. return $this->whereNotMorphedTo($relation, $model, 'or');
  478. }
  479. /**
  480. * Add a "belongs to" relationship where clause to the query.
  481. *
  482. * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $related
  483. * @param string|null $relationshipName
  484. * @param string $boolean
  485. * @return $this
  486. *
  487. * @throws \Illuminate\Database\Eloquent\RelationNotFoundException
  488. */
  489. public function whereBelongsTo($related, $relationshipName = null, $boolean = 'and')
  490. {
  491. if (! $related instanceof Collection) {
  492. $relatedCollection = $related->newCollection([$related]);
  493. } else {
  494. $relatedCollection = $related;
  495. $related = $relatedCollection->first();
  496. }
  497. if ($relatedCollection->isEmpty()) {
  498. throw new InvalidArgumentException('Collection given to whereBelongsTo method may not be empty.');
  499. }
  500. if ($relationshipName === null) {
  501. $relationshipName = Str::camel(class_basename($related));
  502. }
  503. try {
  504. $relationship = $this->model->{$relationshipName}();
  505. } catch (BadMethodCallException) {
  506. throw RelationNotFoundException::make($this->model, $relationshipName);
  507. }
  508. if (! $relationship instanceof BelongsTo) {
  509. throw RelationNotFoundException::make($this->model, $relationshipName, BelongsTo::class);
  510. }
  511. $this->whereIn(
  512. $relationship->getQualifiedForeignKeyName(),
  513. $relatedCollection->pluck($relationship->getOwnerKeyName())->toArray(),
  514. $boolean,
  515. );
  516. return $this;
  517. }
  518. /**
  519. * Add an "BelongsTo" relationship with an "or where" clause to the query.
  520. *
  521. * @param \Illuminate\Database\Eloquent\Model $related
  522. * @param string|null $relationshipName
  523. * @return $this
  524. *
  525. * @throws \RuntimeException
  526. */
  527. public function orWhereBelongsTo($related, $relationshipName = null)
  528. {
  529. return $this->whereBelongsTo($related, $relationshipName, 'or');
  530. }
  531. /**
  532. * Add subselect queries to include an aggregate value for a relationship.
  533. *
  534. * @param mixed $relations
  535. * @param \Illuminate\Contracts\Database\Query\Expression|string $column
  536. * @param string $function
  537. * @return $this
  538. */
  539. public function withAggregate($relations, $column, $function = null)
  540. {
  541. if (empty($relations)) {
  542. return $this;
  543. }
  544. if (is_null($this->query->columns)) {
  545. $this->query->select([$this->query->from.'.*']);
  546. }
  547. $relations = is_array($relations) ? $relations : [$relations];
  548. foreach ($this->parseWithRelations($relations) as $name => $constraints) {
  549. // First we will determine if the name has been aliased using an "as" clause on the name
  550. // and if it has we will extract the actual relationship name and the desired name of
  551. // the resulting column. This allows multiple aggregates on the same relationships.
  552. $segments = explode(' ', $name);
  553. unset($alias);
  554. if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
  555. [$name, $alias] = [$segments[0], $segments[2]];
  556. }
  557. $relation = $this->getRelationWithoutConstraints($name);
  558. if ($function) {
  559. if ($this->getQuery()->getGrammar()->isExpression($column)) {
  560. $aggregateColumn = $this->getQuery()->getGrammar()->getValue($column);
  561. } else {
  562. $hashedColumn = $this->getRelationHashedColumn($column, $relation);
  563. $aggregateColumn = $this->getQuery()->getGrammar()->wrap(
  564. $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn)
  565. );
  566. }
  567. $expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn);
  568. } else {
  569. $expression = $this->getQuery()->getGrammar()->getValue($column);
  570. }
  571. // Here, we will grab the relationship sub-query and prepare to add it to the main query
  572. // as a sub-select. First, we'll get the "has" query and use that to get the relation
  573. // sub-query. We'll format this relationship name and append this column if needed.
  574. $query = $relation->getRelationExistenceQuery(
  575. $relation->getRelated()->newQuery(), $this, new Expression($expression)
  576. )->setBindings([], 'select');
  577. $query->callScope($constraints);
  578. $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
  579. // If the query contains certain elements like orderings / more than one column selected
  580. // then we will remove those elements from the query so that it will execute properly
  581. // when given to the database. Otherwise, we may receive SQL errors or poor syntax.
  582. $query->orders = null;
  583. $query->setBindings([], 'order');
  584. if (count($query->columns) > 1) {
  585. $query->columns = [$query->columns[0]];
  586. $query->bindings['select'] = [];
  587. }
  588. // Finally, we will make the proper column alias to the query and run this sub-select on
  589. // the query builder. Then, we will return the builder instance back to the developer
  590. // for further constraint chaining that needs to take place on the query as needed.
  591. $alias ??= Str::snake(
  592. preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function {$this->getQuery()->getGrammar()->getValue($column)}")
  593. );
  594. if ($function === 'exists') {
  595. $this->selectRaw(
  596. sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)),
  597. $query->getBindings()
  598. )->withCasts([$alias => 'bool']);
  599. } else {
  600. $this->selectSub(
  601. $function ? $query : $query->limit(1),
  602. $alias
  603. );
  604. }
  605. }
  606. return $this;
  607. }
  608. /**
  609. * Get the relation hashed column name for the given column and relation.
  610. *
  611. * @param string $column
  612. * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
  613. * @return string
  614. */
  615. protected function getRelationHashedColumn($column, $relation)
  616. {
  617. if (str_contains($column, '.')) {
  618. return $column;
  619. }
  620. return $this->getQuery()->from === $relation->getQuery()->getQuery()->from
  621. ? "{$relation->getRelationCountHash(false)}.$column"
  622. : $column;
  623. }
  624. /**
  625. * Add subselect queries to count the relations.
  626. *
  627. * @param mixed $relations
  628. * @return $this
  629. */
  630. public function withCount($relations)
  631. {
  632. return $this->withAggregate(is_array($relations) ? $relations : func_get_args(), '*', 'count');
  633. }
  634. /**
  635. * Add subselect queries to include the max of the relation's column.
  636. *
  637. * @param string|array $relation
  638. * @param \Illuminate\Contracts\Database\Query\Expression|string $column
  639. * @return $this
  640. */
  641. public function withMax($relation, $column)
  642. {
  643. return $this->withAggregate($relation, $column, 'max');
  644. }
  645. /**
  646. * Add subselect queries to include the min of the relation's column.
  647. *
  648. * @param string|array $relation
  649. * @param \Illuminate\Contracts\Database\Query\Expression|string $column
  650. * @return $this
  651. */
  652. public function withMin($relation, $column)
  653. {
  654. return $this->withAggregate($relation, $column, 'min');
  655. }
  656. /**
  657. * Add subselect queries to include the sum of the relation's column.
  658. *
  659. * @param string|array $relation
  660. * @param \Illuminate\Contracts\Database\Query\Expression|string $column
  661. * @return $this
  662. */
  663. public function withSum($relation, $column)
  664. {
  665. return $this->withAggregate($relation, $column, 'sum');
  666. }
  667. /**
  668. * Add subselect queries to include the average of the relation's column.
  669. *
  670. * @param string|array $relation
  671. * @param \Illuminate\Contracts\Database\Query\Expression|string $column
  672. * @return $this
  673. */
  674. public function withAvg($relation, $column)
  675. {
  676. return $this->withAggregate($relation, $column, 'avg');
  677. }
  678. /**
  679. * Add subselect queries to include the existence of related models.
  680. *
  681. * @param string|array $relation
  682. * @return $this
  683. */
  684. public function withExists($relation)
  685. {
  686. return $this->withAggregate($relation, '*', 'exists');
  687. }
  688. /**
  689. * Add the "has" condition where clause to the query.
  690. *
  691. * @param \Illuminate\Database\Eloquent\Builder $hasQuery
  692. * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
  693. * @param string $operator
  694. * @param int $count
  695. * @param string $boolean
  696. * @return \Illuminate\Database\Eloquent\Builder|static
  697. */
  698. protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
  699. {
  700. $hasQuery->mergeConstraintsFrom($relation->getQuery());
  701. return $this->canUseExistsForExistenceCheck($operator, $count)
  702. ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1)
  703. : $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
  704. }
  705. /**
  706. * Merge the where constraints from another query to the current query.
  707. *
  708. * @param \Illuminate\Database\Eloquent\Builder $from
  709. * @return \Illuminate\Database\Eloquent\Builder|static
  710. */
  711. public function mergeConstraintsFrom(Builder $from)
  712. {
  713. $whereBindings = $from->getQuery()->getRawBindings()['where'] ?? [];
  714. $wheres = $from->getQuery()->from !== $this->getQuery()->from
  715. ? $this->requalifyWhereTables(
  716. $from->getQuery()->wheres,
  717. $from->getQuery()->grammar->getValue($from->getQuery()->from),
  718. $this->getModel()->getTable()
  719. ) : $from->getQuery()->wheres;
  720. // Here we have some other query that we want to merge the where constraints from. We will
  721. // copy over any where constraints on the query as well as remove any global scopes the
  722. // query might have removed. Then we will return ourselves with the finished merging.
  723. return $this->withoutGlobalScopes(
  724. $from->removedScopes()
  725. )->mergeWheres(
  726. $wheres, $whereBindings
  727. );
  728. }
  729. /**
  730. * Updates the table name for any columns with a new qualified name.
  731. *
  732. * @param array $wheres
  733. * @param string $from
  734. * @param string $to
  735. * @return array
  736. */
  737. protected function requalifyWhereTables(array $wheres, string $from, string $to): array
  738. {
  739. return collect($wheres)->map(function ($where) use ($from, $to) {
  740. return collect($where)->map(function ($value) use ($from, $to) {
  741. return is_string($value) && str_starts_with($value, $from.'.')
  742. ? $to.'.'.Str::afterLast($value, '.')
  743. : $value;
  744. });
  745. })->toArray();
  746. }
  747. /**
  748. * Add a sub-query count clause to this query.
  749. *
  750. * @param \Illuminate\Database\Query\Builder $query
  751. * @param string $operator
  752. * @param int $count
  753. * @param string $boolean
  754. * @return $this
  755. */
  756. protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and')
  757. {
  758. $this->query->addBinding($query->getBindings(), 'where');
  759. return $this->where(
  760. new Expression('('.$query->toSql().')'),
  761. $operator,
  762. is_numeric($count) ? new Expression($count) : $count,
  763. $boolean
  764. );
  765. }
  766. /**
  767. * Get the "has relation" base query instance.
  768. *
  769. * @param string $relation
  770. * @return \Illuminate\Database\Eloquent\Relations\Relation
  771. */
  772. protected function getRelationWithoutConstraints($relation)
  773. {
  774. return Relation::noConstraints(function () use ($relation) {
  775. return $this->getModel()->{$relation}();
  776. });
  777. }
  778. /**
  779. * Check if we can run an "exists" query to optimize performance.
  780. *
  781. * @param string $operator
  782. * @param int $count
  783. * @return bool
  784. */
  785. protected function canUseExistsForExistenceCheck($operator, $count)
  786. {
  787. return ($operator === '>=' || $operator === '<') && $count === 1;
  788. }
  789. }