Skip to content

[Improvement]: Allow reading an element's workflow user permissions once instead of per permission type #360

Description

@cancan101

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Affected capability

    None yet

    Platform Version

    None yet

    Galaxy

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions