forked from laravel/framework
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[8.x] Add one-of-many relationship (inner join) (laravel#37362)
* added one-of-many to has-one * Apply fixes from StyleCI * fixed getResults * added query methods to forwardToOneOfManyQuery * Apply fixes from StyleCI * improvements & tests * Apply fixes from StyleCI * use where or having * Apply fixes from StyleCI * join * wip * wip * fixes style * updated contract * multiple aggregastes * Apply fixes from StyleCI * formatting * formatting * Apply fixes from StyleCI * formatting * rename class * add file * rename array key * add of-many to morph-one * Apply fixes from StyleCI * fixed pivot test * Apply fixes from StyleCI * fixed return type * formatting * add shortcut methods * move test * multiple columns in shortcut * Apply fixes from StyleCI * add key when missing * Apply fixes from StyleCI * use collections * fail for invalid aggregates * Apply fixes from StyleCI * formatting Co-authored-by: Taylor Otwell <[email protected]>
- Loading branch information
1 parent
2020bc3
commit b7a54de
Showing
9 changed files
with
1,050 additions
and
5 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
src/Illuminate/Contracts/Database/Eloquent/SupportsPartialRelations.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
|
||
namespace Illuminate\Contracts\Database\Eloquent; | ||
|
||
use Closure; | ||
|
||
interface SupportsPartialRelations | ||
{ | ||
/** | ||
* Indicate that the relation is a single result of a larger one-to-many relationship. | ||
* | ||
* @param Closure|string|null $column | ||
* @param string|null $relation | ||
* @param string $relation | ||
* @return $this | ||
*/ | ||
public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null); | ||
|
||
/** | ||
* Determine whether the relationship is a one-of-many relationship. | ||
* | ||
* @return bool | ||
*/ | ||
public function isOneOfMany(); | ||
} |
237 changes: 237 additions & 0 deletions
237
src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
<?php | ||
|
||
namespace Illuminate\Database\Eloquent\Relations\Concerns; | ||
|
||
use Closure; | ||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Query\JoinClause; | ||
use Illuminate\Support\Arr; | ||
use Illuminate\Support\Str; | ||
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; | ||
|
||
/** | ||
* 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\Eloquent\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->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->newSubQuery( | ||
isset($previous) ? $previous['column'] : $this->getOneOfManySubQuerySelectColumns(), | ||
$column, $aggregate | ||
); | ||
|
||
if (isset($previous)) { | ||
$this->addJoinSub($subQuery, $previous['subQuery'], $previous['column']); | ||
} elseif (isset($closure)) { | ||
$closure($subQuery); | ||
} | ||
|
||
if (array_key_last($columns) == $column) { | ||
$this->addJoinSub($this->query, $subQuery, $column); | ||
} | ||
|
||
$previous = [ | ||
'subQuery' => $subQuery, | ||
'column' => $column, | ||
]; | ||
} | ||
|
||
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|Closure|null $aggregate | ||
* @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 ?: $this->guessRelationship()); | ||
} | ||
|
||
/** | ||
* Indicate that the relation is the oldest 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 | ||
*/ | ||
public function oldestOfMany($column = 'id', $relation = null) | ||
{ | ||
return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) { | ||
return [$column => 'MIN']; | ||
})->all(), 'MIN', $relation ?: $this->guessRelationship()); | ||
} | ||
|
||
/** | ||
* 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 string|null $column | ||
* @param string|null $aggregate | ||
* @return \Illuminate\Database\Eloquent\Builder | ||
*/ | ||
protected function newSubQuery($groupBy, $column = null, $aggregate = null) | ||
{ | ||
$subQuery = $this->query->getModel() | ||
->newQuery(); | ||
|
||
foreach (Arr::wrap($groupBy) as $group) { | ||
$subQuery->groupBy($this->qualifyRelatedColumn($group)); | ||
} | ||
|
||
if (! is_null($column)) { | ||
$subQuery->selectRaw($aggregate.'('.$column.') as '.$column); | ||
} | ||
|
||
$this->addOneOfManySubQueryConstraints($subQuery, $groupBy, $column, $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 string $on | ||
* @return void | ||
*/ | ||
protected function addJoinSub(Builder $parent, Builder $subQuery, $on) | ||
{ | ||
$parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { | ||
$join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); | ||
|
||
$this->addOneOfManyJoinSubQueryConstraints($join, $on); | ||
}); | ||
} | ||
|
||
/** | ||
* 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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.