Repository navigation
getCustomLayoutDefinitionForGridColumnConfig() mutates the supplied class definition #497
Description
Activity
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.
WithService::cloneDefinition($class)passed in, the supplied class definition keeps its Block children. The returnedlayoutDefinitionstill 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
packagingcontainingpackedValueandpackagingUnitId, 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]thatgeneric-data-index-bundle'sBlockAdapter::normalize()performs without a check raisesUndefined array key "packedValue". Block::getDataForResource()serializes such data asa: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-objectExportDataCollectionHandlercallsColumnConfigurationService::getAvailableDataObjectColumnConfiguration($classId, 1, $user)before it loads the objects to export. That path goes through the grid column collectors (FieldDefinitionCollector,AdvancedColumnCollector,ObjectBrickCollector) andGrid\Service\ClassDefinitionServiceinto this method. In production we get theBlockAdaptererror with a stack trace through exactly this handler (ExportDataCollectionHandler→GridService::getGridValuesForElement()→getGridDataForElement()→ … →BlockAdapter::normalize()).- Loading an object logs
Metadata
Metadata
Assignees
Labels
Type
Fields
Platform Version
Affected capability
Galaxy
Affected Version
2025.4 - LTS
Affected capability
Data Objects
Steps to reproduce
Create a data object class containing an object-brick field, for example
attributes.Obtain a class definition instance and retain it for reuse.
Configure a non-admin user and folder/custom-layout context in which
attributesis excluded.Call:
Inspect
$classDefinition->getLayoutDefinitions().Reuse the same class definition for a user or folder context in which
attributesis 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 usingClassDefinition::getLayoutDefinitions()and passes it todoFilterCustomGridFieldDefinitions():https://github.com/pimcore/pimcore/blob/v12.3.12.1/models/DataObject/Service.php#L1211-L1283
The recursive filter calls
setChildren()andsetNoteditable()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:
Workaround
Deep-copying the class layout before calling the method prevents the shared definition from being modified.