Problem/Motivation
Child 2 of #3624396: Plan: Harden InstanceInterface, IslandInterface and DisplayBuildableInterface as public API before RC1. Needs #3627027: InstanceInterface: list the nodes of a slot.
The merge request is stacked on #3627027: InstanceInterface: list the nodes of a slot: it carries that commit until its merge request is merged. Review the last commit only.
The tree methods of InstanceInterface report a failure in four different ways:
moveToRoot()andmoveToSlot()returnFALSEfor a node or a parent that does not exist, andmoveToSlot()for a move into the node itself or one of its descendants.remove()andsetThirdPartySettings()do nothing, silently, for a node that does not exist.setSource()throws, with a message that says nothing.attachToRoot()andattachToSlot()throw\OutOfRangeExceptionwhen full, and do not declare it.getNode()returns an empty array for a missing node.getParentId()returnsNULLfor a root node and for a node that does not exist alike.
A caller cannot write one error path, and cannot tell "nothing to do" from "this went wrong".
Proposed resolution
Apply the rules of the plan to every tree method:
getNode()returns?array:NULLfor a missing node.getParentId()throwsInvalidNodeExceptionfor a node that does not exist.NULLkeeps meaning "at the root".moveToRoot()andmoveToSlot()returnstatic. They throwInvalidNodeExceptionfor an unknown node or parent, and for a move into the node itself or its descendant, checked before the cardinality. A full root or slot throws\OutOfRangeException, as today.remove(),setSource()andsetThirdPartySettings()returnstaticand throwInvalidNodeExceptionfor an unknown node.- Every tree method declares what it throws with
@throws,attachToRoot()andattachToSlot()included.
In the module:
- No controller catches these call by call. The exception subscriber of #3626659: Paste, duplicate and preset insert end in a server error when the target is full or gone answers both exceptions on the API routes with the error toast.
- Callers that cannot take
NULLfromgetNode()go through one helper onApiControllerBase, which throwsInvalidNodeException. - The tests pinning the silent returns are turned around.
display_builder_ai and the dev tools call these methods. Checked: every call already guards its read, no change needed there.
This breaks a return type and adds exceptions, which a deprecation cannot express. So it lands before RC1, in a beta, with a change record.
Remaining tasks
- Decide whether an unknown slot ID throws.
attachToSlot()andmoveToSlot()store any slot key on an existing parent today. ThrowingInvalidNodeExceptionneeds every source with slots to list all the slots it accepts. - Publish the change record when this is merged.
User interface changes
None intended. A refused change shows the error toast of #3626659: Paste, duplicate and preset insert end in a server error when the target is full or gone.
Moving a node that no longer exists, or into its own descendant, now shows that toast instead of "[attachToSlot] moveToSlot failed with invalid data".
API changes
getNode(): ?array, wasarray.getParentId()throws for a missing node.moveToRoot()andmoveToSlot()returnstatic, werebool, and throw where they returnedFALSE.remove(),setSource()andsetThirdPartySettings()returnstatic, werevoid, and throw for an unknown node.\OutOfRangeExceptionnow carries a message naming the full root or slot.- New protected
ApiControllerBase::getExistingNode().SourceTree::isDescendant()is public.
Data model changes
None.
Issue fork display_builder-3627028
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mogtofu33 commentedComment #3
mogtofu33 commentedComment #5
mogtofu33 commentedWhat changed
Instance::getExistingNodeData(), throws the one "Node %s does not exist." message for the six methods that need it.moveToSlot()refuses a move into the node itself or its descendant before it checks the cardinality, so a cyclic move into a full slot isInvalidNodeException, not\OutOfRangeException.ApiController::attachToRoot()andattachToSlot()lose their "move failed" error branch: the exception subscriber of #3626659: Paste, duplicate and preset insert end in a server error when the target is full or gone answers instead. The complexity ofApiControllergoes from 57 to 55, for a PHPMD limit of 60.update(),thirdPartySettingsUpdate()andsaveAsPreset()read the node throughApiControllerBase::getExistingNode().get()and the islands keep an empty array for a missing node.ViewPanelBase::replaceNode()renders nothing for one, where it raised an "Undefined array key" warning.Tests
InstanceTest: every method on a missing node, both cyclic moves, a cyclic move into a full slot, the returned instance, and a refused change adding no history step.ApiControllerTestandApiActionsControllerTest: update, third party settings, both moves, delete, save as preset, paste and delete styles on a missing node.For review
attachToSlot()andmoveToSlot()still store any slot key on an existing parent. I left it out: throwing needs every source with slots checked first. Decide it here, or in a follow-up?Comment #6
pdureau commentedWaiting #3627027: InstanceInterface: list the nodes of a slot strategy to be acted.
Comment #7
mogtofu33 commentedRebased with #3627027: InstanceInterface: list the nodes of a slot, ready for first review.
Complementary review
Unknown slot IDs
attachToSlot()andmoveToSlot()check that the parent exists and that the slot is not full. They never check that the slot exists. Two cases, checked with a kernel test on this branch:attachToSlot($component_id, 'no_such_slot', ...): accepted and saved undercomponent.slots.no_such_slot. ui_patterns renders only the slots of the component definition. The node is stored but never shown, neither in the Canvas nor on the page.SourceTreewrites children back only into sources implementingSourceWithSlotsInterface.The builder UI never hits this, because its drop targets come from the slot definitions. Code does.
display_builder_aitakes the slot ID from the model's tool call, so a made-up slot name ends as a hidden node and a "component added" answer.Proposal: both methods throw
InvalidNodeExceptionwhen the parent source does not implementSourceWithSlotsInterface, or when$slot_idis not a key of itsgetSlotDefinitions().What it costs:
getSlotDefinitions()becomes binding: it must list every slot the source accepts. Its docblock says so, and the change record tells contrib sources. The three sources in Display Builder already comply: component slots from the SDC definition, layout regions, and page layout regions of the default theme.Comment #8
pdureau commentedGreat.
Comment #10
mogtofu33 commented