Improvement description
Pimcore\Workflow\Manager::isDeniedInWorkflow() resolves the element's entire workflow permission map on every call and then reads a single key out of it:
public function isDeniedInWorkflow(ElementInterface $element, string $permissionType): bool
{
$userPermissions = $this->getWorkflowUserPermissions($element);
return ($userPermissions[$permissionType] ?? null) === false;
}
getWorkflowUserPermissions() is private, and it is not cheap — for the element it iterates every registered workflow, resolves each marking (a database read under StateTableMarkingStore) and evaluates the ordered place configs.
Because the map cannot be obtained any other way, a caller that needs several permission types for one element has no choice but to call isDeniedInWorkflow() once per type, paying the full scan each time.
The admin UI does exactly this, for every element in a tree or grid listing:
// Pimcore\Bundle\AdminBundle\Service\ElementService::mergeWorkflowPermissions()
$workflowPermission = [
'settings' => !$workflowManager->isDeniedInWorkflow($element, 'settings'),
'rename' => !$workflowManager->isDeniedInWorkflow($element, 'rename'),
'publish' => !$workflowManager->isDeniedInWorkflow($element, 'publish'),
];
if ($element instanceof Asset) {
$workflowPermission['remove'] = !$workflowManager->isDeniedInWorkflow($element, 'delete');
} elseif ($element instanceof DataObject) {
$workflowPermission['delete'] = !$workflowManager->isDeniedInWorkflow($element, 'delete');
}
Four calls, four full scans, to produce one element's four flags — multiplied by every element in the listing.
Element\AbstractElement::isAllowed() has the same shape for any caller that checks more than one type on the same element. Note that isAllowed() already caches the DAO result per request via PermissionCache, with a comment stating that the workflow-deny check deliberately stays outside that cache — so the repeated workflow work is a known, uncached cost.
Proposal
Make Manager::getWorkflowUserPermissions() public, so a caller that needs more than one permission type for an element can read the map once and check it n times. isDeniedInWorkflow() stays as it is and remains the right call for a single check — no behaviour change, no new code paths.
Caching the map inside Manager was considered and rejected: a marking can change within a request (apply a transition, then re-read permissions), so an element-keyed memo could serve a stale map. Letting the caller choose the scope of the reuse preserves current semantics exactly.
A batch method that keeps the "denied" convention inside Manager — e.g. getDeniedActionsInWorkflow(ElementInterface $element, array $permissionTypes): array — would achieve the same and is an equally acceptable shape.
Affected version
2026.x (present unchanged on 12.3).
Improvement description
Pimcore\Workflow\Manager::isDeniedInWorkflow()resolves the element's entire workflow permission map on every call and then reads a single key out of it:getWorkflowUserPermissions()is private, and it is not cheap — for the element it iterates every registered workflow, resolves each marking (a database read underStateTableMarkingStore) and evaluates the ordered place configs.Because the map cannot be obtained any other way, a caller that needs several permission types for one element has no choice but to call
isDeniedInWorkflow()once per type, paying the full scan each time.The admin UI does exactly this, for every element in a tree or grid listing:
Four calls, four full scans, to produce one element's four flags — multiplied by every element in the listing.
Element\AbstractElement::isAllowed()has the same shape for any caller that checks more than one type on the same element. Note thatisAllowed()already caches the DAO result per request viaPermissionCache, with a comment stating that the workflow-deny check deliberately stays outside that cache — so the repeated workflow work is a known, uncached cost.Proposal
Make
Manager::getWorkflowUserPermissions()public, so a caller that needs more than one permission type for an element can read the map once and check it n times.isDeniedInWorkflow()stays as it is and remains the right call for a single check — no behaviour change, no new code paths.Caching the map inside
Managerwas considered and rejected: a marking can change within a request (apply a transition, then re-read permissions), so an element-keyed memo could serve a stale map. Letting the caller choose the scope of the reuse preserves current semantics exactly.A batch method that keeps the "denied" convention inside
Manager— e.g.getDeniedActionsInWorkflow(ElementInterface $element, array $permissionTypes): array— would achieve the same and is an equally acceptable shape.Affected version
2026.x(present unchanged on12.3).