Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions modules/backend/behaviors/RelationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,13 @@ protected function isNestedField()
* a) AMBIENT - the nesting stack is non-empty, meaning a manage
* form is actively rendering right now (see
* relationMakePartial()) and this field is nested directly
* under whatever's on top of it. $model is trusted as-is - it's
* the widget's own bound model.
* under whatever's on top of it. $model canNOT be trusted
* here: initRelation() is reached as
* relationRender() -> validateField() -> initRelation($this->model),
* and $this->model is the record of the ENCLOSING relation,
* not of the manage form actually rendering. Those coincide at
* one level of nesting and diverge at two, so the parent is
* walked from the root exactly as in (b).
* b) RECONSTRUCTED - $field is already a bracket path (e.g.
* "items[5][taxes]"), walked from the root to find the actual
* model. This is the common case for anything other than an
Expand Down Expand Up @@ -574,7 +579,9 @@ protected function resolveNestedContext($model, $field)

if ($ambientPath !== null) {
$this->nestedField = $ambientPath . '[' . $field . ']';
return [$model, $field, true];
[$hops] = $this->parseBracketField($this->nestedField);

return [$this->resolveModelForHops($this->rootModel, $hops), $field, true];
}

if (strpos($field, '[') !== false) {
Expand Down
51 changes: 51 additions & 0 deletions modules/backend/tests/behaviors/RelationControllerNestedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,57 @@ public function testAmbientFieldDistinguishesBetweenSiblingItems()
$this->assertNotEquals($nestedFieldForA, $nestedFieldForB, 'Two sibling items\' ambient contexts must resolve to distinct qualified fields');
}

/**
* Two levels of ambient nesting - an Item's manage form rendering
* a sub-item's manage form, which itself renders a relation field.
*
* The model RelationManager hands initRelation() is always
* $this->model, i.e. the record of the ENCLOSING relation. At one
* level of nesting that IS the parent, so trusting it works. At
* two it is the grandparent, and trusting it resolves the leaf
* relation against the wrong ancestor - in the real world a
* BadMethodCallException, since the grandparent's class usually
* has no such relation at all.
*/
public function testAmbientFieldTwoLevelsDeepResolvesInnermostAncestorNotEnclosingRecord()
{
$order = RelationTestOrder::create(['name' => 'Order 1']);
$itemA = RelationTestItem::create(['order_id' => $order->id, 'name' => 'Item A']);
$subItem = RelationTestItem::create(['parent_item_id' => $itemA->id, 'name' => 'Sub A1']);

$controller = $this->makeController($order);
$behavior = $controller->asExtension(RelationController::class);
$controller->initRelation($order, 'items');

// Item A's manage form is rendering; its sub-items field
// resolves ambiently against it.
$this->pushNestingStack($behavior, "items[{$itemA->id}]");
$controller->initRelation($itemA, 'items');

// The sub-item's own manage form is now rendering INSIDE Item
// A's, and a relation field within it resolves ambiently.
$this->pushNestingStack($behavior, "items[{$itemA->id}][items][{$subItem->id}]");

// Item A, not the sub-item: exactly what
// validateField() -> initRelation($this->model) passes here.
$controller->initRelation($itemA, 'taxes');

$this->popNestingStack($behavior);
$this->popNestingStack($behavior);

$this->assertEquals(
"items[{$itemA->id}][items][{$subItem->id}][taxes]",
$this->readProtectedProperty($behavior, 'nestedField'),
'Two levels of ambient nesting should qualify the field against the whole stack path'
);

$this->assertEquals(
$subItem->id,
$this->readProtectedProperty($behavior, 'model')->id,
'A field nested two levels deep must resolve against the innermost ancestor, not the enclosing record'
);
}

/**
* Narrower test for relationMakePartial() specifically - confirms
* it actually pushes before rendering and pops after, regardless
Expand Down
Loading