QueriesRelationships.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <?php
  2. namespace Illuminate\Database\Eloquent\Concerns;
  3. use Closure;
  4. use Illuminate\Support\Str;
  5. use Illuminate\Database\Eloquent\Builder;
  6. use Illuminate\Database\Query\Expression;
  7. use Illuminate\Database\Eloquent\Relations\Relation;
  8. use Illuminate\Database\Query\Builder as QueryBuilder;
  9. trait QueriesRelationships
  10. {
  11. /**
  12. * Add a relationship count / exists condition to the query.
  13. *
  14. * @param string $relation
  15. * @param string $operator
  16. * @param int $count
  17. * @param string $boolean
  18. * @param \Closure|null $callback
  19. * @return \Illuminate\Database\Eloquent\Builder|static
  20. */
  21. public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
  22. {
  23. if (strpos($relation, '.') !== false) {
  24. return $this->hasNested($relation, $operator, $count, $boolean, $callback);
  25. }
  26. $relation = $this->getRelationWithoutConstraints($relation);
  27. // If we only need to check for the existence of the relation, then we can optimize
  28. // the subquery to only run a "where exists" clause instead of this full "count"
  29. // clause. This will make these queries run much faster compared with a count.
  30. $method = $this->canUseExistsForExistenceCheck($operator, $count)
  31. ? 'getRelationExistenceQuery'
  32. : 'getRelationExistenceCountQuery';
  33. $hasQuery = $relation->{$method}(
  34. $relation->getRelated()->newQuery(), $this
  35. );
  36. // Next we will call any given callback as an "anonymous" scope so they can get the
  37. // proper logical grouping of the where clauses if needed by this Eloquent query
  38. // builder. Then, we will be ready to finalize and return this query instance.
  39. if ($callback) {
  40. $hasQuery->callScope($callback);
  41. }
  42. return $this->addHasWhere(
  43. $hasQuery, $relation, $operator, $count, $boolean
  44. );
  45. }
  46. /**
  47. * Add nested relationship count / exists conditions to the query.
  48. *
  49. * Sets up recursive call to whereHas until we finish the nested relation.
  50. *
  51. * @param string $relations
  52. * @param string $operator
  53. * @param int $count
  54. * @param string $boolean
  55. * @param \Closure|null $callback
  56. * @return \Illuminate\Database\Eloquent\Builder|static
  57. */
  58. protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
  59. {
  60. $relations = explode('.', $relations);
  61. $closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback) {
  62. // In order to nest "has", we need to add count relation constraints on the
  63. // callback Closure. We'll do this by simply passing the Closure its own
  64. // reference to itself so it calls itself recursively on each segment.
  65. count($relations) > 1
  66. ? $q->whereHas(array_shift($relations), $closure)
  67. : $q->has(array_shift($relations), $operator, $count, 'and', $callback);
  68. };
  69. return $this->has(array_shift($relations), '>=', 1, $boolean, $closure);
  70. }
  71. /**
  72. * Add a relationship count / exists condition to the query with an "or".
  73. *
  74. * @param string $relation
  75. * @param string $operator
  76. * @param int $count
  77. * @return \Illuminate\Database\Eloquent\Builder|static
  78. */
  79. public function orHas($relation, $operator = '>=', $count = 1)
  80. {
  81. return $this->has($relation, $operator, $count, 'or');
  82. }
  83. /**
  84. * Add a relationship count / exists condition to the query.
  85. *
  86. * @param string $relation
  87. * @param string $boolean
  88. * @param \Closure|null $callback
  89. * @return \Illuminate\Database\Eloquent\Builder|static
  90. */
  91. public function doesntHave($relation, $boolean = 'and', Closure $callback = null)
  92. {
  93. return $this->has($relation, '<', 1, $boolean, $callback);
  94. }
  95. /**
  96. * Add a relationship count / exists condition to the query with an "or".
  97. *
  98. * @param string $relation
  99. * @return \Illuminate\Database\Eloquent\Builder|static
  100. */
  101. public function orDoesntHave($relation)
  102. {
  103. return $this->doesntHave($relation, 'or');
  104. }
  105. /**
  106. * Add a relationship count / exists condition to the query with where clauses.
  107. *
  108. * @param string $relation
  109. * @param \Closure|null $callback
  110. * @param string $operator
  111. * @param int $count
  112. * @return \Illuminate\Database\Eloquent\Builder|static
  113. */
  114. public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
  115. {
  116. return $this->has($relation, $operator, $count, 'and', $callback);
  117. }
  118. /**
  119. * Add a relationship count / exists condition to the query with where clauses and an "or".
  120. *
  121. * @param string $relation
  122. * @param \Closure $callback
  123. * @param string $operator
  124. * @param int $count
  125. * @return \Illuminate\Database\Eloquent\Builder|static
  126. */
  127. public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
  128. {
  129. return $this->has($relation, $operator, $count, 'or', $callback);
  130. }
  131. /**
  132. * Add a relationship count / exists condition to the query with where clauses.
  133. *
  134. * @param string $relation
  135. * @param \Closure|null $callback
  136. * @return \Illuminate\Database\Eloquent\Builder|static
  137. */
  138. public function whereDoesntHave($relation, Closure $callback = null)
  139. {
  140. return $this->doesntHave($relation, 'and', $callback);
  141. }
  142. /**
  143. * Add a relationship count / exists condition to the query with where clauses and an "or".
  144. *
  145. * @param string $relation
  146. * @param \Closure $callback
  147. * @return \Illuminate\Database\Eloquent\Builder|static
  148. */
  149. public function orWhereDoesntHave($relation, Closure $callback = null)
  150. {
  151. return $this->doesntHave($relation, 'or', $callback);
  152. }
  153. /**
  154. * Add subselect queries to count the relations.
  155. *
  156. * @param mixed $relations
  157. * @return $this
  158. */
  159. public function withCount($relations)
  160. {
  161. if (empty($relations)) {
  162. return $this;
  163. }
  164. if (is_null($this->query->columns)) {
  165. $this->query->select([$this->query->from.'.*']);
  166. }
  167. $relations = is_array($relations) ? $relations : func_get_args();
  168. foreach ($this->parseWithRelations($relations) as $name => $constraints) {
  169. // First we will determine if the name has been aliased using an "as" clause on the name
  170. // and if it has we will extract the actual relationship name and the desired name of
  171. // the resulting column. This allows multiple counts on the same relationship name.
  172. $segments = explode(' ', $name);
  173. unset($alias);
  174. if (count($segments) == 3 && Str::lower($segments[1]) == 'as') {
  175. list($name, $alias) = [$segments[0], $segments[2]];
  176. }
  177. $relation = $this->getRelationWithoutConstraints($name);
  178. // Here we will get the relationship count query and prepare to add it to the main query
  179. // as a sub-select. First, we'll get the "has" query and use that to get the relation
  180. // count query. We will normalize the relation name then append _count as the name.
  181. $query = $relation->getRelationExistenceCountQuery(
  182. $relation->getRelated()->newQuery(), $this
  183. );
  184. $query->callScope($constraints);
  185. $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
  186. if (count($query->columns) > 1) {
  187. $query->columns = [$query->columns[0]];
  188. }
  189. // Finally we will add the proper result column alias to the query and run the subselect
  190. // statement against the query builder. Then we will return the builder instance back
  191. // to the developer for further constraint chaining that needs to take place on it.
  192. $column = $alias ?? Str::snake($name.'_count');
  193. $this->selectSub($query, $column);
  194. }
  195. return $this;
  196. }
  197. /**
  198. * Add the "has" condition where clause to the query.
  199. *
  200. * @param \Illuminate\Database\Eloquent\Builder $hasQuery
  201. * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
  202. * @param string $operator
  203. * @param int $count
  204. * @param string $boolean
  205. * @return \Illuminate\Database\Eloquent\Builder|static
  206. */
  207. protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
  208. {
  209. $hasQuery->mergeConstraintsFrom($relation->getQuery());
  210. return $this->canUseExistsForExistenceCheck($operator, $count)
  211. ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1)
  212. : $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
  213. }
  214. /**
  215. * Merge the where constraints from another query to the current query.
  216. *
  217. * @param \Illuminate\Database\Eloquent\Builder $from
  218. * @return \Illuminate\Database\Eloquent\Builder|static
  219. */
  220. public function mergeConstraintsFrom(Builder $from)
  221. {
  222. $whereBindings = $from->getQuery()->getRawBindings()['where'] ?? [];
  223. // Here we have some other query that we want to merge the where constraints from. We will
  224. // copy over any where constraints on the query as well as remove any global scopes the
  225. // query might have removed. Then we will return ourselves with the finished merging.
  226. return $this->withoutGlobalScopes(
  227. $from->removedScopes()
  228. )->mergeWheres(
  229. $from->getQuery()->wheres, $whereBindings
  230. );
  231. }
  232. /**
  233. * Add a sub-query count clause to this query.
  234. *
  235. * @param \Illuminate\Database\Query\Builder $query
  236. * @param string $operator
  237. * @param int $count
  238. * @param string $boolean
  239. * @return $this
  240. */
  241. protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and')
  242. {
  243. $this->query->addBinding($query->getBindings(), 'where');
  244. return $this->where(
  245. new Expression('('.$query->toSql().')'),
  246. $operator,
  247. is_numeric($count) ? new Expression($count) : $count,
  248. $boolean
  249. );
  250. }
  251. /**
  252. * Get the "has relation" base query instance.
  253. *
  254. * @param string $relation
  255. * @return \Illuminate\Database\Eloquent\Relations\Relation
  256. */
  257. protected function getRelationWithoutConstraints($relation)
  258. {
  259. return Relation::noConstraints(function () use ($relation) {
  260. return $this->getModel()->{$relation}();
  261. });
  262. }
  263. /**
  264. * Check if we can run an "exists" query to optimize performance.
  265. *
  266. * @param string $operator
  267. * @param int $count
  268. * @return bool
  269. */
  270. protected function canUseExistsForExistenceCheck($operator, $count)
  271. {
  272. return ($operator === '>=' || $operator === '<') && $count === 1;
  273. }
  274. }