java-topology/defects/doctrine-orm/patch/doctrine-orm-0001-subclasses-list-inarray.md
russell@unturf.com ac82cff865 php-cluster: doctrine-orm-0001 + composer-0003 CWE-407 defects; count 668→670
doctrine-orm-0001: ClassMetadata::addSubClass in_array O(S²) subclasses list HIGH
  - src/Mapping/ClassMetadata.php addSubClass() scans plain $subClasses list
  - addSubClasses($parent->subClasses) called in doLoadMetadata: O(S²) per class
  - Fix: parallel $subClassesSet hash for O(1) membership; 500x at S=1000

composer-0003: InstalledRepository::getDependents in_array O(P²) needles list MEDIUM
  - needles array grows during foreach($packages) loop, in_array scan 3 sites
  - $packagesFoundSet already exists for cycle-detection but needles is separate
  - Fix: add $needlesSet = array_fill_keys($needles, true), mirror growth; 1000x at P=1000

cayley: CLEAN (confirmed -- map-based seen/pathMap throughout)
tinkerpop: CLEAN (confirmed -- existing CLEAN.md valid, sort ArrayList is query-plan-time only)
2026-03-29 19:51:31 -04:00

3 KiB
Raw Blame History

UNDF: UNDF-2026-000000002

UNDF: (pending)

doctrine-orm-0001: ClassMetadata::addSubClass — O(N²) in_array on subClasses list

CWE-407 — Algorithmic Complexity

Field Value
ID doctrine-orm-0001
Severity HIGH
Ecosystem doctrine-orm
Package doctrine/orm
File src/Mapping/ClassMetadata.php
Lines 23082315
Complexity O(S²) per hierarchy, O(N·S²) total metadata load
Hot path EntityManager::getMetadataFactory()->getMetadataFor() — called on every entity operation

Defect

// BEFORE — O(S²): in_array() is O(S) inside addSubClasses() which iterates S entries
// Called from ClassMetadataFactory::doLoadMetadata() during metadata bootstrap.
// addSubClasses($parent->subClasses) copies parent's S subclasses into child, each
// call doing an O(S) scan, yielding O(S²) per class and O(N·S²) across N entities.

public function addSubClass(string $className): void
{
    if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
        $this->subClasses[] = $className;   // O(S) scan each insertion
    }
}

Called from ClassMetadataFactory::doLoadMetadata (line 152):

$class->addSubClasses($parent->subClasses);  // iterates S entries × O(S) each = O(S²)

And from the parent-class scan loop (line 386):

foreach ($parentClasses as $parentClass) {
    $rootEntityClass->addSubClass($parentClass);  // O(S) scan per parent
}

subClasses is declared as a plain list<class-string> with no parallel hash index:

// src/Mapping/ClassMetadata.php line 314
public array $subClasses = [];

Fix

// AFTER — O(1) lookup: maintain a parallel hash-keyed set alongside the list.
// The list is kept for ordered serialization/BC; the set provides O(1) membership.

// Add alongside $subClasses (line ~314):
/** @phpstan-var array<class-string, true> */
public array $subClassesSet = [];

public function addSubClass(string $className): void
{
    if (is_subclass_of($className, $this->name) && !isset($this->subClassesSet[$className])) {
        $this->subClassesSet[$className] = true;   // O(1) hash insert
        $this->subClasses[] = $className;           // preserve ordered list for BC
    }
}

$subClassesSet must also be hydrated on deserialization (in __wakeup / __clone):

// Rebuild set after deserialization:
$this->subClassesSet = array_fill_keys($this->subClasses, true);

Speedup

S (subclasses) Before (ops) After (ops) Speedup
50 1,275 50 25×
100 5,050 100 50×
500 125,250 500 250×
1,000 500,500 1,000 500×

A Doctrine application with a 100-subclass STI (Single-Table Inheritance) hierarchy pays 5,050 membership scans on every cold metadata bootstrap instead of 100. With warm OPcache this runs once per process, but in serverless / high-churn deployments it hits on every cold start.