Skip to content

Commit

Permalink
Add ability to define "with" relations as a nested array.
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Jun 7, 2022
1 parent 9f4eb34 commit a336c66
Show file tree
Hide file tree
Showing 3 changed files with 364 additions and 17 deletions.
104 changes: 90 additions & 14 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1430,22 +1430,13 @@ public function newModelInstance($attributes = [])
*/
protected function parseWithRelations(array $relations)
{
$results = [];
if ($relations === []) {
return [];
}

foreach ($relations as $name => $constraints) {
// If the "name" value is a numeric key, we can assume that no constraints
// have been specified. We will just put an empty Closure there so that
// we can treat these all the same while we are looping through them.
if (is_numeric($name)) {
$name = $constraints;

[$name, $constraints] = str_contains($name, ':')
? $this->createSelectWithConstraint($name)
: [$name, static function () {
//
}];
}
$results = [];

foreach ($this->prepareNestedWithRelationships($relations) as $name => $constraints) {
// We need to separate out any nested includes, which allows the developers
// to load deep relationships using "dots" without stating each level of
// the relationship with its own key in the array of eager-load names.
Expand All @@ -1457,6 +1448,91 @@ protected function parseWithRelations(array $relations)
return $results;
}

/**
* Prepare nested with relationships.
*
* @param array $relations
* @param string $prefix
* @return array
*/
protected function prepareNestedWithRelationships($relations, $prefix = '')
{
$preparedRelationships = [];

if ($prefix !== '') {
$prefix .= '.';
}

// If any of the relationships are formatted with the [$attribute => array()]
// syntax, we shall loop over the nested relations and prepend each key of
// the array while flattening into the traditional dot notation format.
foreach ($relations as $key => $value) {
if (! is_string($key) || ! is_array($value)) {
continue;
}

[$attribute, $attributeSelectConstraint] = $this->parseNameAndAttributeSelectionConstraint($key);

$preparedRelationships = array_merge(
$preparedRelationships,
["{$prefix}{$attribute}" => $attributeSelectConstraint],
$this->prepareNestedWithRelationships($value, "{$prefix}{$attribute}"),
);

unset($relations[$key]);
}

// We now know that the remaining relationships are in a dot notation format.
// The values maybe a string or a Closure. We'll loop over them and ensure
// existing Closures are merged and strings are made into a constraint.
foreach ($relations as $key => $value) {
if (is_numeric($key) && is_string($value)) {
[$key, $value] = $this->parseNameAndAttributeSelectionConstraint($value);
}

$preparedRelationships[$prefix.$key] = $this->combineContraints([
$value,
$preparedRelationships[$prefix.$key] ?? static function () {
//
},
]);
}

return $preparedRelationships;
}

/**
* Combine an array of constraints into a single constraint.
*
* @param array $constriants
* @return \Closure
*/
protected function combineContraints(array $constraints)
{
return function ($builder) use ($constraints) {
foreach ($constraints as $constraint) {
$builder = $constraint($builder) ?? $builder;
}

return $builder;
};
}

/**
* Parse the attribute select constraints from the name.
*
* @param string $name
* @return array
*/
protected function parseNameAndAttributeSelectionConstraint($name)
{
return str_contains($name, ':')
? $this->createSelectWithConstraint($name)
: [$name, static function () {
//
}];
}

/**
* Create a constraint to select the given columns for the relation.
*
Expand Down
6 changes: 3 additions & 3 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ public function testEagerLoadParsingSetsProperRelationships()
}]);
$eagers = $builder->getEagerLoads();

$this->assertSame('foo', $eagers['orders']());
$this->assertSame('foo', $eagers['orders']($this->getBuilder()));

$builder = $this->getBuilder();
$builder->with(['orders.lines' => function () {
Expand All @@ -852,7 +852,7 @@ public function testEagerLoadParsingSetsProperRelationships()

$this->assertInstanceOf(Closure::class, $eagers['orders']);
$this->assertNull($eagers['orders']());
$this->assertSame('foo', $eagers['orders.lines']());
$this->assertSame('foo', $eagers['orders.lines']($this->getBuilder()));

$builder = $this->getBuilder();
$builder->with('orders.lines', function () {
Expand All @@ -862,7 +862,7 @@ public function testEagerLoadParsingSetsProperRelationships()

$this->assertInstanceOf(Closure::class, $eagers['orders']);
$this->assertNull($eagers['orders']());
$this->assertSame('foo', $eagers['orders.lines']());
$this->assertSame('foo', $eagers['orders.lines']($this->getBuilder()));
}

public function testQueryPassThru()
Expand Down
Loading

0 comments on commit a336c66

Please sign in to comment.