| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- <?php
- namespace Illuminate\Database\Eloquent\Relations\Concerns;
- use Closure;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Database\Query\JoinClause;
- use Illuminate\Support\Arr;
- use InvalidArgumentException;
- trait CanBeOneOfMany
- {
- /**
- * Determines whether the relationship is one-of-many.
- *
- * @var bool
- */
- protected $isOneOfMany = false;
- /**
- * The name of the relationship.
- *
- * @var string
- */
- protected $relationName;
- /**
- * The one of many inner join subselect query builder instance.
- *
- * @var \Illuminate\Database\Eloquent\Builder|null
- */
- protected $oneOfManySubQuery;
- /**
- * Add constraints for inner join subselect for one of many relationships.
- *
- * @param \Illuminate\Database\Eloquent\Builder $query
- * @param string|null $column
- * @param string|null $aggregate
- * @return void
- */
- abstract public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null);
- /**
- * Get the columns the determine the relationship groups.
- *
- * @return array|string
- */
- abstract public function getOneOfManySubQuerySelectColumns();
- /**
- * Add join query constraints for one of many relationships.
- *
- * @param \Illuminate\Database\Query\JoinClause $join
- * @return void
- */
- abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join);
- /**
- * Indicate that the relation is a single result of a larger one-to-many relationship.
- *
- * @param string|array|null $column
- * @param string|\Closure|null $aggregate
- * @param string|null $relation
- * @return $this
- *
- * @throws \InvalidArgumentException
- */
- public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null)
- {
- $this->isOneOfMany = true;
- $this->relationName = $relation ?: $this->getDefaultOneOfManyJoinAlias(
- $this->guessRelationship()
- );
- $keyName = $this->query->getModel()->getKeyName();
- $columns = is_string($columns = $column) ? [
- $column => $aggregate,
- $keyName => $aggregate,
- ] : $column;
- if (! array_key_exists($keyName, $columns)) {
- $columns[$keyName] = 'MAX';
- }
- if ($aggregate instanceof Closure) {
- $closure = $aggregate;
- }
- foreach ($columns as $column => $aggregate) {
- if (! in_array(strtolower($aggregate), ['min', 'max'])) {
- throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX");
- }
- $subQuery = $this->newOneOfManySubQuery(
- $this->getOneOfManySubQuerySelectColumns(),
- array_merge([$column], $previous['columns'] ?? []),
- $aggregate,
- );
- if (isset($previous)) {
- $this->addOneOfManyJoinSubQuery(
- $subQuery,
- $previous['subQuery'],
- $previous['columns'],
- );
- }
- if (isset($closure)) {
- $closure($subQuery);
- }
- if (! isset($previous)) {
- $this->oneOfManySubQuery = $subQuery;
- }
- if (array_key_last($columns) == $column) {
- $this->addOneOfManyJoinSubQuery(
- $this->query,
- $subQuery,
- array_merge([$column], $previous['columns'] ?? []),
- );
- }
- $previous = [
- 'subQuery' => $subQuery,
- 'columns' => array_merge([$column], $previous['columns'] ?? []),
- ];
- }
- $this->addConstraints();
- $columns = $this->query->getQuery()->columns;
- if (is_null($columns) || $columns === ['*']) {
- $this->select([$this->qualifyColumn('*')]);
- }
- return $this;
- }
- /**
- * Indicate that the relation is the latest single result of a larger one-to-many relationship.
- *
- * @param string|array|null $column
- * @param string|null $relation
- * @return $this
- */
- public function latestOfMany($column = 'id', $relation = null)
- {
- return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) {
- return [$column => 'MAX'];
- })->all(), 'MAX', $relation);
- }
- /**
- * Indicate that the relation is the oldest single result of a larger one-to-many relationship.
- *
- * @param string|array|null $column
- * @param string|null $relation
- * @return $this
- */
- public function oldestOfMany($column = 'id', $relation = null)
- {
- return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) {
- return [$column => 'MIN'];
- })->all(), 'MIN', $relation);
- }
- /**
- * Get the default alias for the one of many inner join clause.
- *
- * @param string $relation
- * @return string
- */
- protected function getDefaultOneOfManyJoinAlias($relation)
- {
- return $relation == $this->query->getModel()->getTable()
- ? $relation.'_of_many'
- : $relation;
- }
- /**
- * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship.
- *
- * @param string|array $groupBy
- * @param array<string>|null $columns
- * @param string|null $aggregate
- * @return \Illuminate\Database\Eloquent\Builder
- */
- protected function newOneOfManySubQuery($groupBy, $columns = null, $aggregate = null)
- {
- $subQuery = $this->query->getModel()
- ->newQuery()
- ->withoutGlobalScopes($this->removedScopes());
- foreach (Arr::wrap($groupBy) as $group) {
- $subQuery->groupBy($this->qualifyRelatedColumn($group));
- }
- if (! is_null($columns)) {
- foreach ($columns as $key => $column) {
- $aggregatedColumn = $subQuery->getQuery()->grammar->wrap($subQuery->qualifyColumn($column));
- if ($key === 0) {
- $aggregatedColumn = "{$aggregate}({$aggregatedColumn})";
- } else {
- $aggregatedColumn = "min({$aggregatedColumn})";
- }
- $subQuery->selectRaw($aggregatedColumn.' as '.$subQuery->getQuery()->grammar->wrap($column.'_aggregate'));
- }
- }
- $this->addOneOfManySubQueryConstraints($subQuery, $groupBy, $columns, $aggregate);
- return $subQuery;
- }
- /**
- * Add the join subquery to the given query on the given column and the relationship's foreign key.
- *
- * @param \Illuminate\Database\Eloquent\Builder $parent
- * @param \Illuminate\Database\Eloquent\Builder $subQuery
- * @param array<string> $on
- * @return void
- */
- protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on)
- {
- $parent->beforeQuery(function ($parent) use ($subQuery, $on) {
- $subQuery->applyBeforeQueryCallbacks();
- $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) {
- foreach ($on as $onColumn) {
- $join->on($this->qualifySubSelectColumn($onColumn.'_aggregate'), '=', $this->qualifyRelatedColumn($onColumn));
- }
- $this->addOneOfManyJoinSubQueryConstraints($join, $on);
- });
- });
- }
- /**
- * Merge the relationship query joins to the given query builder.
- *
- * @param \Illuminate\Database\Eloquent\Builder $query
- * @return void
- */
- protected function mergeOneOfManyJoinsTo(Builder $query)
- {
- $query->getQuery()->beforeQueryCallbacks = $this->query->getQuery()->beforeQueryCallbacks;
- $query->applyBeforeQueryCallbacks();
- }
- /**
- * Get the query builder that will contain the relationship constraints.
- *
- * @return \Illuminate\Database\Eloquent\Builder
- */
- protected function getRelationQuery()
- {
- return $this->isOneOfMany()
- ? $this->oneOfManySubQuery
- : $this->query;
- }
- /**
- * Get the one of many inner join subselect builder instance.
- *
- * @return \Illuminate\Database\Eloquent\Builder|void
- */
- public function getOneOfManySubQuery()
- {
- return $this->oneOfManySubQuery;
- }
- /**
- * Get the qualified column name for the one-of-many relationship using the subselect join query's alias.
- *
- * @param string $column
- * @return string
- */
- public function qualifySubSelectColumn($column)
- {
- return $this->getRelationName().'.'.last(explode('.', $column));
- }
- /**
- * Qualify related column using the related table name if it is not already qualified.
- *
- * @param string $column
- * @return string
- */
- protected function qualifyRelatedColumn($column)
- {
- return str_contains($column, '.') ? $column : $this->query->getModel()->getTable().'.'.$column;
- }
- /**
- * Guess the "hasOne" relationship's name via backtrace.
- *
- * @return string
- */
- protected function guessRelationship()
- {
- return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
- }
- /**
- * Determine whether the relationship is a one-of-many relationship.
- *
- * @return bool
- */
- public function isOneOfMany()
- {
- return $this->isOneOfMany;
- }
- /**
- * Get the name of the relationship.
- *
- * @return string
- */
- public function getRelationName()
- {
- return $this->relationName;
- }
- }
|