Skip to content

Commit 1c0b3d2

Browse files
[9.x] Add ability to define "with" relations as a nested array (#42690)
* Add ability to define "with" relations as a nested array. * typo * formatting Co-authored-by: Taylor Otwell <[email protected]>
1 parent c1bb5ed commit 1c0b3d2

File tree

3 files changed

+364
-17
lines changed

3 files changed

+364
-17
lines changed

src/Illuminate/Database/Eloquent/Builder.php

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,22 +1430,13 @@ public function newModelInstance($attributes = [])
14301430
*/
14311431
protected function parseWithRelations(array $relations)
14321432
{
1433-
$results = [];
1433+
if ($relations === []) {
1434+
return [];
1435+
}
14341436

1435-
foreach ($relations as $name => $constraints) {
1436-
// If the "name" value is a numeric key, we can assume that no constraints
1437-
// have been specified. We will just put an empty Closure there so that
1438-
// we can treat these all the same while we are looping through them.
1439-
if (is_numeric($name)) {
1440-
$name = $constraints;
1441-
1442-
[$name, $constraints] = str_contains($name, ':')
1443-
? $this->createSelectWithConstraint($name)
1444-
: [$name, static function () {
1445-
//
1446-
}];
1447-
}
1437+
$results = [];
14481438

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

1451+
/**
1452+
* Prepare nested with relationships.
1453+
*
1454+
* @param array $relations
1455+
* @param string $prefix
1456+
* @return array
1457+
*/
1458+
protected function prepareNestedWithRelationships($relations, $prefix = '')
1459+
{
1460+
$preparedRelationships = [];
1461+
1462+
if ($prefix !== '') {
1463+
$prefix .= '.';
1464+
}
1465+
1466+
// If any of the relationships are formatted with the [$attribute => array()]
1467+
// syntax, we shall loop over the nested relations and prepend each key of
1468+
// this array while flattening into the traditional dot notation format.
1469+
foreach ($relations as $key => $value) {
1470+
if (! is_string($key) || ! is_array($value)) {
1471+
continue;
1472+
}
1473+
1474+
[$attribute, $attributeSelectConstraint] = $this->parseNameAndAttributeSelectionConstraint($key);
1475+
1476+
$preparedRelationships = array_merge(
1477+
$preparedRelationships,
1478+
["{$prefix}{$attribute}" => $attributeSelectConstraint],
1479+
$this->prepareNestedWithRelationships($value, "{$prefix}{$attribute}"),
1480+
);
1481+
1482+
unset($relations[$key]);
1483+
}
1484+
1485+
// We now know that the remaining relationships are in a dot notation format
1486+
// and may be a string or Closure. We'll loop over them and ensure all of
1487+
// the present Closures are merged + strings are made into constraints.
1488+
foreach ($relations as $key => $value) {
1489+
if (is_numeric($key) && is_string($value)) {
1490+
[$key, $value] = $this->parseNameAndAttributeSelectionConstraint($value);
1491+
}
1492+
1493+
$preparedRelationships[$prefix.$key] = $this->combineConstraints([
1494+
$value,
1495+
$preparedRelationships[$prefix.$key] ?? static function () {
1496+
//
1497+
},
1498+
]);
1499+
}
1500+
1501+
return $preparedRelationships;
1502+
}
1503+
1504+
/**
1505+
* Combine an array of constraints into a single constraint.
1506+
*
1507+
* @param array $constraints
1508+
* @return \Closure
1509+
*/
1510+
protected function combineConstraints(array $constraints)
1511+
{
1512+
return function ($builder) use ($constraints) {
1513+
foreach ($constraints as $constraint) {
1514+
$builder = $constraint($builder) ?? $builder;
1515+
}
1516+
1517+
return $builder;
1518+
};
1519+
}
1520+
1521+
/**
1522+
* Parse the attribute select constraints from the name.
1523+
*
1524+
* @param string $name
1525+
* @return array
1526+
*/
1527+
protected function parseNameAndAttributeSelectionConstraint($name)
1528+
{
1529+
return str_contains($name, ':')
1530+
? $this->createSelectWithConstraint($name)
1531+
: [$name, static function () {
1532+
//
1533+
}];
1534+
}
1535+
14601536
/**
14611537
* Create a constraint to select the given columns for the relation.
14621538
*

tests/Database/DatabaseEloquentBuilderTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,7 @@ public function testEagerLoadParsingSetsProperRelationships()
842842
}]);
843843
$eagers = $builder->getEagerLoads();
844844

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

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

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

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

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

868868
public function testQueryPassThru()

0 commit comments

Comments
 (0)