BUGFIX: Sync the node type before moving the node in other dimensions - #103
BUGFIX: Sync the node type before moving the node in other dimensions#103gradinarufelix wants to merge 3 commits into
Conversation
|
The red CI here is not caused by this PR. I exported the base commit ( Cause and fix are in #104. Once that is merged I will rebase this branch and CI should go green. |
mficzel
left a comment
There was a problem hiding this comment.
fine by verifying only sync node is affected ...
The node type is stored per dimension variant, so a node that is retyped and moved in the same publication still carries the old node type name in the other dimensions. Moving validates the node type the moved node currently has against the constraints of the new parent, so the sync moved the target variant while it still resolved to Neos.Neos:FallbackNode and aborted the whole publication with a NodeConstraintException. Sync the node type before the move so the constraint check sees the new node type. If the source node type cannot be resolved either, skip the preset with a warning instead, as writing the fallback node type would destroy the node type name the target dimension still has stored.
The flag was reset only after the preset loop, so any exception inside the loop left it disabled for the rest of the request and silently disarmed the AroundMoveNodeDataAspect. Restore the previous value in a finally block, the same way afterAdoptNode already does.
d5be3bb to
2ee1ecf
Compare
mficzel
left a comment
There was a problem hiding this comment.
Generally fine but after a rebase on 2.0 with your ci fixes the teste for 8.3 now fail.
Once this is adjusted it can be merged and released.
|
Fixed it. I was accessing a method to get the typed node type in the node data that was introduced only in 8.4. Replaced it with a direct access. |
Problem
Changing a node's type and moving the node in the same publication aborts the whole publication with
nodeTypeis a plain string column onNodeData, so it exists once per dimension variant. After theeditor retypes the node in the default language, only that row carries the new type; the other
language rows still carry the old name. If that node type was refactored away,
NodeTypeManagersilently resolves the old name to
Neos.Neos:FallbackNode, which only supertypesNeos.Neos:Nodeand therefore fails any
constraints.nodeTypesallowlist.NodeTranslationService::syncNode()moved the target-language variant before repairing its nodetype, and
Node::moveInto()resolves$this->getNodeType()at call time to validate it against thenew parent. Nothing catches the exception: it escapes
syncNode()→translateNodes()→PersistenceManager::persistAll()and the editor loses the entire publication.The ordering has been this way since v2.1.0 (
d5746d8) — the move block was inserted abovepre-existing
setNodeType()code, so it looks unintentional rather than deliberate.Fix
BUGFIX: Sync the node type before moving the node in other dimensions$targetNode->setNodeType($sourceNode->getNodeType())moves up, directly afteradoptNode()andbefore the move block, so the constraint check sees the new type. This is safe:
Node::setNodeType()performs no constraint validation, creates or removes no tethered childnodes, and touches neither path nor index.
setIndex()deliberately stays after the move,because
moveInto()assignsPOSITION_LASTfirst.A guard skips the preset, with a warning, when the source node type itself cannot be resolved.
Without it the hoist is not enough — see below.
TASK: Restore the recursion prevention flag if the node sync failsrecursionPreventionEnabledwas set tofalseat the top of the preset loop and back totrueonlyafter it, so any exception inside the loop left it disabled for the rest of the request and silently
disarmed
AroundMoveNodeDataAspect. The loop is now wrapped intry/finallythat restores theprevious value — the same shape
afterAdoptNode()already uses (33f4020), which skippedsyncNode()at the time.Why the hoist alone is not enough
Node::setNodeType()early-returns on NodeType object identity, andNodeTypeManagerhands outone shared
Neos.Neos:FallbackNodeinstance for every unresolvable name. So when the source nodetype is unresolvable too — node moved but never retyped — the hoisted
setNodeType()is a no-op andmoveInto()throws exactly as before. Hence the guard, whichcontinues to the next preset insteadof writing the type, moving, or translating.
A note on the guard's predicate
The guard uses
$sourceNode->getNodeType()->getName() === $this->fallbackNodeTypeName. The obvioussharper test — comparing the stored name from
getNodeTypeNameWithoutFallback()against the resolvedone, the way
Neos\Neos\Fusion\Helper\NodeHelper::nodeType()does — was implemented and does notwork here:
NodeData::similarize()copiesnodeTypeviaObjectAccess::getProperty($source, 'nodeType')without
forceDirectAccess(onlycreationDateTimeis force-direct), so ObjectAccess prefers thegetNodeType()accessor, gets the resolved NodeType, and the matching setter writes$nodeType->getName()back into the column.Node::materializeNodeData()andWorkspace::replaceNodeData()both call it, so the dead name has already been replaced byNeos.Neos:FallbackNodebeforesyncNode()ever sees the node. Instrumented run of the second test:With that predicate the guard never fires and the second test fails.
!hasNodeType($storedName)fails for the same reason. Comparing against the configured fallback is the only signal that
survives to this point in the code. The cost is that a node genuinely of the configured fallback type
is skipped too; under the shipped default (
Neos.Neos:FallbackNode) those are broken leftoversanyway, and installations that point
fallbackNodeTypeat a usable type cannot distinguish the twocases at all once
similarize()has run.Tests
Two functional tests in
Tests/Functional/ContentRepository/NodeTranslationServiceTest.php, plus aConfiguration/Testing/NodeTypes.yamlfixture with a tetheredmainNeos.Neos:ContentCollectionwhose
constraints.nodeTypesdenies the wildcard ('*': ~) and allows one type. The tethered shapematters: it routes the check through
NodeType::allowsGrandchildNodeType(), which is the path theproduction failure takes — a plain
allowsChildNodeTypefixture does not reproduce it. The node typeis made to disappear via the documented
NodeTypeManager::overrideNodeTypes()hook and restored intearDown().nodeWithRemovedNodeTypeInGermanIsRetypedBeforeItIsMovedInEnglish— retype plus move in onepublication. Verified to fail on
2.0withNodeConstraintException #1404648124, passes here.It asserts on the English
NodeDatadirectly (includinggetDimensionValues()), so a dimensionfallback cannot make it pass by reading the German row.
nodeWithRemovedNodeTypeInGermanIsNotSyncedIntoEnglish— move without retype. Asserts noexception, the English variant stays where it was, its stored node type name is untouched, and a
warning is logged. Verified to fail if the guard is removed but the hoist kept.
Run against Neos 8.4.7 / Flow 8.4.4 on PHP 8.3: 15 tests, 94 assertions, 1 failure —
removedNodeInGermanIsAlsoRemovedInEnglish, which fails identically on unmodified2.0in the sameenvironment and is unrelated to this change.
composer test:styleandcomposer test:unitare green;composer test:stanreports 4 errors, all pre-existing inClasses/Ui/Changes/*.phpand unrelatedto this change — they are fixed separately in #104. None in
NodeTranslationService.php.For the reviewer
test:functionalis not part of CI..github/workflows/build.ymlrunscomposer test, whichis style + stan + unit only, so the two new tests will not run on this PR. Wiring the functional
suite in needs a Neos installation in the workflow and felt out of scope here — happy to add it in
a separate PR if you want it.
BUGFIX/…convention. Say the word and I will rename.required. For a node with an unresolvable type that is only edited or reordered, v2.7.1 synced the
hidden flags, index and properties harmlessly; this branch logs and skips instead. Most visible in
./flow translation:sync, which walks the tree and never moves anything. A narrower guard thatskipped only
setNodeType()and the move would preserve that behaviour — I went with the broaderskip because syncing a node whose type no longer exists is questionable either way, but I am happy
to narrow it.
try/catchisolation intranslateNodes()(it turnspublication failures into log lines and deserves its own discussion), sorting of the publish batch,
and reconciling auto-created child nodes after a type change.
3.0is unaffected —Classes/ContentRepository/NodeTranslationService.phpdoes not exist thereand sync is not implemented for Neos 9 yet (Reimplement sync for Neos 9 #54).