CanBeOneOfMany.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <?php
  2. namespace Illuminate\Database\Eloquent\Relations\Concerns;
  3. use Closure;
  4. use Illuminate\Database\Eloquent\Builder;
  5. use Illuminate\Database\Query\JoinClause;
  6. use Illuminate\Support\Arr;
  7. use InvalidArgumentException;
  8. trait CanBeOneOfMany
  9. {
  10. /**
  11. * Determines whether the relationship is one-of-many.
  12. *
  13. * @var bool
  14. */
  15. protected $isOneOfMany = false;
  16. /**
  17. * The name of the relationship.
  18. *
  19. * @var string
  20. */
  21. protected $relationName;
  22. /**
  23. * The one of many inner join subselect query builder instance.
  24. *
  25. * @var \Illuminate\Database\Eloquent\Builder|null
  26. */
  27. protected $oneOfManySubQuery;
  28. /**
  29. * Add constraints for inner join subselect for one of many relationships.
  30. *
  31. * @param \Illuminate\Database\Eloquent\Builder $query
  32. * @param string|null $column
  33. * @param string|null $aggregate
  34. * @return void
  35. */
  36. abstract public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null);
  37. /**
  38. * Get the columns the determine the relationship groups.
  39. *
  40. * @return array|string
  41. */
  42. abstract public function getOneOfManySubQuerySelectColumns();
  43. /**
  44. * Add join query constraints for one of many relationships.
  45. *
  46. * @param \Illuminate\Database\Query\JoinClause $join
  47. * @return void
  48. */
  49. abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join);
  50. /**
  51. * Indicate that the relation is a single result of a larger one-to-many relationship.
  52. *
  53. * @param string|array|null $column
  54. * @param string|\Closure|null $aggregate
  55. * @param string|null $relation
  56. * @return $this
  57. *
  58. * @throws \InvalidArgumentException
  59. */
  60. public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null)
  61. {
  62. $this->isOneOfMany = true;
  63. $this->relationName = $relation ?: $this->getDefaultOneOfManyJoinAlias(
  64. $this->guessRelationship()
  65. );
  66. $keyName = $this->query->getModel()->getKeyName();
  67. $columns = is_string($columns = $column) ? [
  68. $column => $aggregate,
  69. $keyName => $aggregate,
  70. ] : $column;
  71. if (! array_key_exists($keyName, $columns)) {
  72. $columns[$keyName] = 'MAX';
  73. }
  74. if ($aggregate instanceof Closure) {
  75. $closure = $aggregate;
  76. }
  77. foreach ($columns as $column => $aggregate) {
  78. if (! in_array(strtolower($aggregate), ['min', 'max'])) {
  79. throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX");
  80. }
  81. $subQuery = $this->newOneOfManySubQuery(
  82. $this->getOneOfManySubQuerySelectColumns(),
  83. array_merge([$column], $previous['columns'] ?? []),
  84. $aggregate,
  85. );
  86. if (isset($previous)) {
  87. $this->addOneOfManyJoinSubQuery(
  88. $subQuery,
  89. $previous['subQuery'],
  90. $previous['columns'],
  91. );
  92. }
  93. if (isset($closure)) {
  94. $closure($subQuery);
  95. }
  96. if (! isset($previous)) {
  97. $this->oneOfManySubQuery = $subQuery;
  98. }
  99. if (array_key_last($columns) == $column) {
  100. $this->addOneOfManyJoinSubQuery(
  101. $this->query,
  102. $subQuery,
  103. array_merge([$column], $previous['columns'] ?? []),
  104. );
  105. }
  106. $previous = [
  107. 'subQuery' => $subQuery,
  108. 'columns' => array_merge([$column], $previous['columns'] ?? []),
  109. ];
  110. }
  111. $this->addConstraints();
  112. $columns = $this->query->getQuery()->columns;
  113. if (is_null($columns) || $columns === ['*']) {
  114. $this->select([$this->qualifyColumn('*')]);
  115. }
  116. return $this;
  117. }
  118. /**
  119. * Indicate that the relation is the latest single result of a larger one-to-many relationship.
  120. *
  121. * @param string|array|null $column
  122. * @param string|null $relation
  123. * @return $this
  124. */
  125. public function latestOfMany($column = 'id', $relation = null)
  126. {
  127. return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) {
  128. return [$column => 'MAX'];
  129. })->all(), 'MAX', $relation);
  130. }
  131. /**
  132. * Indicate that the relation is the oldest single result of a larger one-to-many relationship.
  133. *
  134. * @param string|array|null $column
  135. * @param string|null $relation
  136. * @return $this
  137. */
  138. public function oldestOfMany($column = 'id', $relation = null)
  139. {
  140. return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) {
  141. return [$column => 'MIN'];
  142. })->all(), 'MIN', $relation);
  143. }
  144. /**
  145. * Get the default alias for the one of many inner join clause.
  146. *
  147. * @param string $relation
  148. * @return string
  149. */
  150. protected function getDefaultOneOfManyJoinAlias($relation)
  151. {
  152. return $relation == $this->query->getModel()->getTable()
  153. ? $relation.'_of_many'
  154. : $relation;
  155. }
  156. /**
  157. * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship.
  158. *
  159. * @param string|array $groupBy
  160. * @param array<string>|null $columns
  161. * @param string|null $aggregate
  162. * @return \Illuminate\Database\Eloquent\Builder
  163. */
  164. protected function newOneOfManySubQuery($groupBy, $columns = null, $aggregate = null)
  165. {
  166. $subQuery = $this->query->getModel()
  167. ->newQuery()
  168. ->withoutGlobalScopes($this->removedScopes());
  169. foreach (Arr::wrap($groupBy) as $group) {
  170. $subQuery->groupBy($this->qualifyRelatedColumn($group));
  171. }
  172. if (! is_null($columns)) {
  173. foreach ($columns as $key => $column) {
  174. $aggregatedColumn = $subQuery->getQuery()->grammar->wrap($subQuery->qualifyColumn($column));
  175. if ($key === 0) {
  176. $aggregatedColumn = "{$aggregate}({$aggregatedColumn})";
  177. } else {
  178. $aggregatedColumn = "min({$aggregatedColumn})";
  179. }
  180. $subQuery->selectRaw($aggregatedColumn.' as '.$subQuery->getQuery()->grammar->wrap($column.'_aggregate'));
  181. }
  182. }
  183. $this->addOneOfManySubQueryConstraints($subQuery, $groupBy, $columns, $aggregate);
  184. return $subQuery;
  185. }
  186. /**
  187. * Add the join subquery to the given query on the given column and the relationship's foreign key.
  188. *
  189. * @param \Illuminate\Database\Eloquent\Builder $parent
  190. * @param \Illuminate\Database\Eloquent\Builder $subQuery
  191. * @param array<string> $on
  192. * @return void
  193. */
  194. protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on)
  195. {
  196. $parent->beforeQuery(function ($parent) use ($subQuery, $on) {
  197. $subQuery->applyBeforeQueryCallbacks();
  198. $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) {
  199. foreach ($on as $onColumn) {
  200. $join->on($this->qualifySubSelectColumn($onColumn.'_aggregate'), '=', $this->qualifyRelatedColumn($onColumn));
  201. }
  202. $this->addOneOfManyJoinSubQueryConstraints($join, $on);
  203. });
  204. });
  205. }
  206. /**
  207. * Merge the relationship query joins to the given query builder.
  208. *
  209. * @param \Illuminate\Database\Eloquent\Builder $query
  210. * @return void
  211. */
  212. protected function mergeOneOfManyJoinsTo(Builder $query)
  213. {
  214. $query->getQuery()->beforeQueryCallbacks = $this->query->getQuery()->beforeQueryCallbacks;
  215. $query->applyBeforeQueryCallbacks();
  216. }
  217. /**
  218. * Get the query builder that will contain the relationship constraints.
  219. *
  220. * @return \Illuminate\Database\Eloquent\Builder
  221. */
  222. protected function getRelationQuery()
  223. {
  224. return $this->isOneOfMany()
  225. ? $this->oneOfManySubQuery
  226. : $this->query;
  227. }
  228. /**
  229. * Get the one of many inner join subselect builder instance.
  230. *
  231. * @return \Illuminate\Database\Eloquent\Builder|void
  232. */
  233. public function getOneOfManySubQuery()
  234. {
  235. return $this->oneOfManySubQuery;
  236. }
  237. /**
  238. * Get the qualified column name for the one-of-many relationship using the subselect join query's alias.
  239. *
  240. * @param string $column
  241. * @return string
  242. */
  243. public function qualifySubSelectColumn($column)
  244. {
  245. return $this->getRelationName().'.'.last(explode('.', $column));
  246. }
  247. /**
  248. * Qualify related column using the related table name if it is not already qualified.
  249. *
  250. * @param string $column
  251. * @return string
  252. */
  253. protected function qualifyRelatedColumn($column)
  254. {
  255. return str_contains($column, '.') ? $column : $this->query->getModel()->getTable().'.'.$column;
  256. }
  257. /**
  258. * Guess the "hasOne" relationship's name via backtrace.
  259. *
  260. * @return string
  261. */
  262. protected function guessRelationship()
  263. {
  264. return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
  265. }
  266. /**
  267. * Determine whether the relationship is a one-of-many relationship.
  268. *
  269. * @return bool
  270. */
  271. public function isOneOfMany()
  272. {
  273. return $this->isOneOfMany;
  274. }
  275. /**
  276. * Get the name of the relationship.
  277. *
  278. * @return string
  279. */
  280. public function getRelationName()
  281. {
  282. return $this->relationName;
  283. }
  284. }