CanBeOneOfMany.php
TLDR
The file CanBeOneOfMany.php
is a trait that provides functionality for handling one-of-many relationships in the Eloquent ORM. It defines several methods and properties related to the handling of these relationships.
Methods
addOneOfManySubQueryConstraints
This method adds constraints for the inner join subselect for one-of-many relationships.
getOneOfManySubQuerySelectColumns
This method retrieves the columns that determine the relationship groups for the one-of-many subselect query.
addOneOfManyJoinSubQueryConstraints
This method adds join query constraints for one-of-many relationships.
ofMany
This method indicates that the relation is a single result of a larger one-to-many relationship.
latestOfMany
This method indicates that the relation is the latest single result of a larger one-to-many relationship.
oldestOfMany
This method indicates that the relation is the oldest single result of a larger one-to-many relationship.
getDefaultOneOfManyJoinAlias
This method retrieves the default alias for the one-of-many inner join clause.
newOneOfManySubQuery
This method creates a new query for the related model, grouping the query by the given column, often the foreign key of the relationship.
addOneOfManyJoinSubQuery
This method adds the join subquery to the given query on the given column and the relationship's foreign key.
mergeOneOfManyJoinsTo
This method merges the relationship query joins to the given query builder.
getRelationQuery
This method retrieves the query builder that will contain the relationship constraints.
getOneOfManySubQuery
This method retrieves the one-of-many inner join subselect builder instance.
qualifySubSelectColumn
This method gets the qualified column name for the one-of-many relationship using the subselect join query's alias.
qualifyRelatedColumn
This method qualifies a related column using the related table name if it is not already qualified.
guessRelationship
This method guesses the "hasOne" relationship's name via backtrace.
isOneOfMany
This method determines whether the relationship is a one-of-many relationship.
getRelationName
This method gets the name of the relationship.
Classes
No classes are defined in this file.
<?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;
}
}