Skip to content

getCustomLayoutDefinitionForGridColumnConfig() mutates the supplied class definition #497

Description

@movingthings

Affected Version

2025.4 - LTS

Affected capability

Data Objects

Steps to reproduce

  1. Create a data object class containing an object-brick field, for example attributes.

  2. Obtain a class definition instance and retain it for reuse.

  3. Configure a non-admin user and folder/custom-layout context in which attributes is excluded.

  4. Call:

    DataObject\Service::getCustomLayoutDefinitionForGridColumnConfig(
        $classDefinition,
        $restrictedFolderId,
        $restrictedUser,
    );
  5. Inspect $classDefinition->getLayoutDefinitions().

  6. Reuse the same class definition for a user or folder context in which attributes is permitted.

Actual Behavior

The first call removes excluded children directly from the layout tree owned by the supplied class definition.

The original class definition is consequently changed, and the later resolution cannot restore or find the removed field. Downstream grid-column resolution can then fail because the base layout no longer contains the object-brick field.

Expected Behavior

Permission filtering should return a filtered layout without changing the supplied class definition or its layout tree.

Each call should start from the complete class layout, independently of earlier users, folders, or permission contexts.

Technical details

getCustomLayoutDefinitionForGridColumnConfig() obtains the original layout using ClassDefinition::getLayoutDefinitions() and passes it to doFilterCustomGridFieldDefinitions():

https://github.com/pimcore/pimcore/blob/v12.3.12.1/models/DataObject/Service.php#L1211-L1283

The recursive filter calls setChildren() and setNoteditable() on that layout tree. Because the tree is not copied first, these changes also modify the layout retained by the supplied class definition.

Studio Backend exposes the problem by obtaining a potentially cached class definition and passing it directly to this Core method:

https://github.com/pimcore/studio-backend-bundle/blob/v2025.4.14/src/Grid/Service/ClassDefinitionService.php#L41-L94

The mutating implementation is also still present in Pimcore Core v2026.2.13:

https://github.com/pimcore/pimcore/blob/v2026.2.13/models/DataObject/Service.php#L934-L1006

Requested change

Please make getCustomLayoutDefinitionForGridColumnConfig() operate on an independent copy of the layout tree, or otherwise guarantee that the supplied class definition and its layout remain unchanged.

Suggested regression coverage

Add a regression test that:

  1. Creates one class definition containing a nested or object-brick field.
  2. Retains the original layout tree.
  3. Resolves a restricted grid layout that excludes the field.
  4. Verifies that the returned layout is filtered.
  5. Verifies that the original class definition still contains the field and retains its original flags.
  6. Resolves the same class definition in a permitted context and verifies that the field is still available.

Workaround

Deep-copying the class layout before calling the method prevents the shared definition from being modified.

Activity

  1. added theissue type on Sep 16, 2026
  2. updated Affected capability Data Objects and Platform Version 2025.4 - LTS on Sep 16, 2026
  3. added this to the 2026.4 milestone on Sep 17, 2026
  4. aKugler commented on Oct 2, 2026

    @aKugler

    Hi!
    Additional findings from our setup (Pimcore 2026.2.12 / Studio Backend 2026.2.10) that go beyond the object-brick case:

    1. It is not limited to restricted contexts. Any non-admin user triggers it, and fields inside a Block are always removed.
    For a non-admin user and an existing object, getCustomGridFieldDefinitions() returns the class's top-level field definitions (the caller adds the localized fields), also when no custom layout is assigned. doFilterCustomGridFieldDefinitions() recurses into nested containers and removes every child that is not in that map. Fields inside a Block are never top-level, so they are always removed, even when nothing should be excluded.

    2. The deep-copy workaround stops the mutation, but the result is still wrong for Blocks.
    With Service::cloneDefinition($class) passed in, the supplied class definition keeps its Block children. The returned layoutDefinition still contains the Block without any fields (getFieldDefinitions() returns []). So the filter itself needs to handle fields nested in a Block, not only work on a copy.

    Reproduction (class with a Block field packaging containing packedValue and packagingUnitId, a non-admin user without custom layouts, any existing object ID):

    $class = ClassDefinition::getByName('Product');
    $block = $class->getFieldDefinition('packaging');
    var_dump((bool) $block->getFieldDefinition('packedValue')); // true
    
    DataObject\Service::getCustomLayoutDefinitionForGridColumnConfig($class, $anyObjectId, $nonAdminUser);
    
    $block = ClassDefinition::getByName('Product')->getFieldDefinition('packaging'); // same instance
    var_dump((bool) $block->getFieldDefinition('packedValue')); // false
    var_dump(array_keys($block->getFieldDefinitions()));      // []

    3. Consequences of the mutation for Block data. These last until the runtime cache is cleared (end of the request, or the next message in a messenger worker). Reproduced locally:

    • Loading an object logs class definition seems to have changed, element name: … for every Block child (Block::getDataFromResource()), and the Block items come back empty.
    • For Block data that was already loaded before the mutation, the lookup $fieldDefinitions[$key] that generic-data-index-bundle's BlockAdapter::normalize() performs without a check raises Undefined array key "packedValue".
    • Block::getDataForResource() serializes such data as a:1:{i:0;a:0:{}} instead of the two fields, because it skips elements without a field definition. A save in that state would therefore write empty Block items. We have not run an actual save.

    4. Exports by non-admin users trigger it.
    The data-object ExportDataCollectionHandler calls ColumnConfigurationService::getAvailableDataObjectColumnConfiguration($classId, 1, $user) before it loads the objects to export. That path goes through the grid column collectors (FieldDefinitionCollector, AdvancedColumnCollector, ObjectBrickCollector) and Grid\Service\ClassDefinitionService into this method. In production we get the BlockAdapter error with a stack trace through exactly this handler (ExportDataCollectionHandler → GridService::getGridValuesForElement() → getGridDataForElement() → … → BlockAdapter::normalize()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    Platform Version

    2025.4 - LTS

    Affected capability

    Data Objects

    Galaxy

    None yet

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions