Craft 5.9.1
Craft knows how to reorder nested elements, but only the control panel can ask it to. The logic sits inline in NestedElementsController::actionReorder(), behind requireCpRequest(), so plugins, console commands and migrations have no way to reach it.
What exists today
craft\controllers\NestedElementsController::actionReorder() (since 5.0.0) reorders by renumbering elements_owners directly:
// Update all the incorrect sort orders
foreach ($allIds as $i => $id) {
$sortOrder = $i + 1;
if (!isset($oldSortOrders[$id]) || $sortOrder !== $oldSortOrders[$id]) {
Db::update(Table::ELEMENTS_OWNERS, [
'sortOrder' => $sortOrder,
], [
'ownerId' => $this->owner->id,
'elementId' => $id,
]);
}
}
Craft::$app->getElements()->invalidateCachesForElement($this->owner);
That's a complete, self-contained implementation. The problem is purely where it lives. beforeAction() requires a CP request, and authorization runs through a session token (manageNestedElements::<ownerId>::<attribute>) that the control panel mints when it renders an element index. Nothing is delegated to a service, so there's no seam for other code to call.
Why it comes up
I ran into this building content tooling on top of Craft, where an external client needs to add, edit and rearrange Matrix blocks on a page. Adding and editing a single block are both well served: every block is a real entry, so it has an id and the normal element APIs apply to it. Moving one is the operation with no equivalent.
What the public API offers instead
Two routes, and both are expensive for what they do.
NestedElementTrait::setSortOrder() only sets the value in memory. Persisting it means calling saveElement(), and moving one block shifts its siblings too, so a single reorder becomes one element save per sibling, each of them firing save events and reindexing search for an operation that changes no content at all.
Matrix::normalizeValue() accepts a sortOrder key, but only as part of the whole submitted field value. That means resending every block in the field. Anything left out of the payload is deleted, which makes it unsafe for partial updates and unusable when the caller only knows about the one block it wants to move.
Suggested shape
Move the body of actionReorder() into a service method and let the controller call it:
public function reorderNestedElements(
ElementInterface $owner,
string $attribute,
array $elementIds,
int $offset,
): void
NestedElementManager may be the more natural home, since it already owns maintainNestedElements(), duplicateNestedElements(), deleteNestedElements() and restoreNestedElements(). Reordering is the one lifecycle operation missing from that set.
Authorization can stay exactly where it is, in the controller. The extracted method would only need the owner, the attribute, the element ids and the offset.
Prior art in core
reorderSites(), reorderVolumes(), reorderWidgets() and reorderSets() are all service methods with thin controller actions in front of them. Nested elements look like the case where that pattern stopped at the controller.
Related
#16718 and #18043 both cover creating nested elements programmatically, and both are resolved. Reordering them is the remaining gap in the same area.
Craft 5.9.1
Craft knows how to reorder nested elements, but only the control panel can ask it to. The logic sits inline in
NestedElementsController::actionReorder(), behindrequireCpRequest(), so plugins, console commands and migrations have no way to reach it.What exists today
craft\controllers\NestedElementsController::actionReorder()(since 5.0.0) reorders by renumberingelements_ownersdirectly:That's a complete, self-contained implementation. The problem is purely where it lives.
beforeAction()requires a CP request, and authorization runs through a session token (manageNestedElements::<ownerId>::<attribute>) that the control panel mints when it renders an element index. Nothing is delegated to a service, so there's no seam for other code to call.Why it comes up
I ran into this building content tooling on top of Craft, where an external client needs to add, edit and rearrange Matrix blocks on a page. Adding and editing a single block are both well served: every block is a real entry, so it has an id and the normal element APIs apply to it. Moving one is the operation with no equivalent.
What the public API offers instead
Two routes, and both are expensive for what they do.
NestedElementTrait::setSortOrder()only sets the value in memory. Persisting it means callingsaveElement(), and moving one block shifts its siblings too, so a single reorder becomes one element save per sibling, each of them firing save events and reindexing search for an operation that changes no content at all.Matrix::normalizeValue()accepts asortOrderkey, but only as part of the whole submitted field value. That means resending every block in the field. Anything left out of the payload is deleted, which makes it unsafe for partial updates and unusable when the caller only knows about the one block it wants to move.Suggested shape
Move the body of
actionReorder()into a service method and let the controller call it:NestedElementManagermay be the more natural home, since it already ownsmaintainNestedElements(),duplicateNestedElements(),deleteNestedElements()andrestoreNestedElements(). Reordering is the one lifecycle operation missing from that set.Authorization can stay exactly where it is, in the controller. The extracted method would only need the owner, the attribute, the element ids and the offset.
Prior art in core
reorderSites(),reorderVolumes(),reorderWidgets()andreorderSets()are all service methods with thin controller actions in front of them. Nested elements look like the case where that pattern stopped at the controller.Related
#16718 and #18043 both cover creating nested elements programmatically, and both are resolved. Reordering them is the remaining gap in the same area.