Skip to content
This repository was archived by the owner on Aug 13, 2026. It is now read-only.
This repository was archived by the owner on Aug 13, 2026. It is now read-only.

[Bug]: UserController::getAction() causes OOM fatal error when user has many object references #1106

Description

@gresa-neziri

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

  1. Create a user in Pimcore admin.
  2. Assign that user to a large number of DataObjects (e.g. via a User field in a DataObject class).
  3. Open the user's detail page in the admin UI (/admin/user/get?id=X).
  4. 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.

Activity

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

Metadata

Metadata

Assignees

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