Bug Report: UserController::getAction() causes OOM when user is referenced by many objects
Versions
| Component |
Version |
| Pimcore Core |
v11.5.11 |
| pimcore/admin-ui-classic-bundle |
v1.7.16 |
| PHP |
8.2 |
Description
In Pimcore\Bundle\AdminBundle\Controller\Admin\UserController::getAction(), the following code loads all referencing objects fully into memory for every user detail request:
$userObjects = DataObject\Service::getObjectsReferencingUser($user->getId());
foreach ($userObjects as $o) {
if ($o->isAllowed('list')) {
$userObjectData[] = [
'path' => $o->getRealFullPath(),
'id' => $o->getId(),
'subtype' => $o->getClass()->getName(),
];
} else {
$hasHidden = true;
}
}
When a user is referenced by a large number of DataObjects (e.g. hundreds or thousands), this fully hydrates every object into PHP memory just to extract 3 fields (path, id, subtype) and check one permission (isAllowed('list')). This causes PHP to exceed memory_limit and throws a fatal OOM error, making it impossible to open that user's detail page in the admin UI.
Steps to Reproduce
- Create a user in Pimcore admin.
- Assign that user to a large number of DataObjects (e.g. via a User field in a DataObject class).
- Open the user's detail page in the admin UI (
/admin/user/get?id=X).
- Observe PHP fatal error:
Allowed memory size exhausted.
Expected Behavior
The endpoint should handle users with many object references gracefully without risking OOM.
Suggested Fix
Short-term: COUNT guard
Perform a cheap COUNT query first. If the count exceeds a safe threshold, skip full hydration and return hasHidden: true to signal the UI that dependencies exist but cannot be fully listed:
$db = \Pimcore\Db::get();
$count = (int) $db->fetchOne(
'SELECT COUNT(*) FROM dependencies WHERE targetid = ? AND targettype = "object"',
[$user->getId()]
);
$threshold = 200;
$hasHidden = false;
$userObjectData = [];
if ($count <= $threshold) {
$userObjects = DataObject\Service::getObjectsReferencingUser($user->getId());
foreach ($userObjects as $o) {
if ($o->isAllowed('list')) {
$userObjectData[] = [
'path' => $o->getRealFullPath(),
'id' => $o->getId(),
'subtype' => $o->getClass()->getName(),
];
} else {
$hasHidden = true;
}
}
} else {
$hasHidden = true;
}
Long-term (Recommended): Paginated dependencies endpoint
The proper fix is to move object dependency loading out of getAction() entirely and into a dedicated paginated endpoint, for example:
GET /admin/user/object-dependencies?id={userId}&page=1&limit=50
#[Route(path: '/object-dependencies', name: 'object_dependencies', methods: ['GET'])]
public function objectDependenciesAction(Request $request): JsonResponse
{
$userId = (int) $request->get('id');
$page = max(1, (int) $request->get('page', 1));
$limit = min(100, max(1, (int) $request->get('limit', 50)));
$offset = ($page - 1) * $limit;
$db = \Pimcore\Db::get();
$total = (int) $db->fetchOne(
'SELECT COUNT(*) FROM dependencies WHERE targetid = ? AND targettype = "object"',
[$userId]
);
$rows = $db->fetchAllAssociative(
'SELECT targetid as id FROM dependencies WHERE targetid = ? AND targettype = "object" LIMIT ? OFFSET ?',
[$userId, $limit, $offset]
);
$userObjectData = [];
$hasHidden = false;
foreach ($rows as $row) {
$o = DataObject::getById($row['id']);
if ($o) {
if ($o->isAllowed('list')) {
$userObjectData[] = [
'path' => $o->getRealFullPath(),
'id' => $o->getId(),
'subtype' => $o->getClass()->getName(),
];
} else {
$hasHidden = true;
}
}
}
return $this->adminJson([
'success' => true,
'total' => $total,
'page' => $page,
'limit' => $limit,
'hasHidden' => $hasHidden,
'dependencies' => $userObjectData,
]);
}
This approach:
- Eliminates OOM entirely — only a fixed batch of objects is hydrated per request
- Preserves full data visibility — all dependencies are accessible via pagination
- Keeps
getAction() fast and lightweight — no heavy loading on page open
- Scales to any number of references — no arbitrary threshold needed
The admin UI would then load dependencies lazily when the user scrolls or clicks a "Load dependencies" control, similar to how other paginated grids work in the Pimcore admin.
Impact
- Severity: High — affects production admin usability, entire user detail page becomes inaccessible
- Workaround: Full controller override that always returns an empty
objectDependencies payload — works but hides real dependencies for all users, not just the heavy ones
Additional Context
This was discovered in a production environment where certain users are referenced by thousands of DataObjects. The root cause is that getObjectsReferencingUser() returns fully hydrated objects, and the subsequent loop instantiates all of them simultaneously in memory. Only 3 fields and 1 permission check are actually needed per object, so full hydration is unnecessary and disproportionately expensive for large datasets.
Bug Report: UserController::getAction() causes OOM when user is referenced by many objects
Versions
Description
In
Pimcore\Bundle\AdminBundle\Controller\Admin\UserController::getAction(), the following code loads all referencing objects fully into memory for every user detail request:When a user is referenced by a large number of DataObjects (e.g. hundreds or thousands), this fully hydrates every object into PHP memory just to extract 3 fields (
path,id,subtype) and check one permission (isAllowed('list')). This causes PHP to exceedmemory_limitand throws a fatal OOM error, making it impossible to open that user's detail page in the admin UI.Steps to Reproduce
/admin/user/get?id=X).Allowed memory size exhausted.Expected Behavior
The endpoint should handle users with many object references gracefully without risking OOM.
Suggested Fix
Short-term: COUNT guard
Perform a cheap
COUNTquery first. If the count exceeds a safe threshold, skip full hydration and returnhasHidden: trueto signal the UI that dependencies exist but cannot be fully listed:Long-term (Recommended): Paginated dependencies endpoint
The proper fix is to move object dependency loading out of
getAction()entirely and into a dedicated paginated endpoint, for example:This approach:
getAction()fast and lightweight — no heavy loading on page openThe admin UI would then load dependencies lazily when the user scrolls or clicks a "Load dependencies" control, similar to how other paginated grids work in the Pimcore admin.
Impact
objectDependenciespayload — works but hides real dependencies for all users, not just the heavy onesAdditional Context
This was discovered in a production environment where certain users are referenced by thousands of DataObjects. The root cause is that
getObjectsReferencingUser()returns fully hydrated objects, and the subsequent loop instantiates all of them simultaneously in memory. Only 3 fields and 1 permission check are actually needed per object, so full hydration is unnecessary and disproportionately expensive for large datasets.