# doctrine-orm-0002 — ClassMetadata::addSubClass: O(n²) subclass dedup during metadata loading **Status:** OPEN **Severity:** MEDIUM **Component:** `doctrine/orm` — `src/Doctrine/ORM/Mapping/ClassMetadata.php` **Affects:** Application startup / metadata cache warming; entity class hierarchies with many subclasses --- ## Root Cause `ClassMetadata.php:2313` — `addSubClass()`: ```php // DEFECTIVE — O(subclasses) linear scan per addSubClass call public function addSubClass(string $className): void { if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) { $this->subClasses[] = $className; } } ``` `$this->subClasses` is a plain PHP array (`public array $subClasses = []`, line 314). `in_array` scans the entire array for every call. `addSubClass()` is called inside loops in `ClassMetadataFactory`: - `ClassMetadataFactory.php:152`: `$class->addSubClasses($parent->subClasses)` — bulk add loop - `ClassMetadataFactory.php:386`: inside `foreach ($parentClasses as $parentClass)` loop inside `findAbstractEntityClassesNotListedInDiscriminatorMap()` The second call site is the worst case: for each discriminator map entry, `getParentClasses()` returns all ancestor classes, and `addSubClass` is called for each. With S subclasses and D depth, total comparisons: O(S × D × S) = O(S²×D). --- ## Complexity Analysis | Metric | Defective | Fixed | |--------|-----------|-------| | Single addSubClass | O(current subclass count) | O(1) | | Full hierarchy load (S subclasses) | O(S²) | O(S) | | Growth pattern | Quadratic | Linear | --- ## Fix Replace `$this->subClasses` with a parallel indexed structure, or use a `Set`-equivalent (array keyed by class name): ```php // Keep public array $subClasses for backward compat, add private set for O(1) lookup private array $subClassSet = []; public function addSubClass(string $className): void { if (is_subclass_of($className, $this->name) && ! isset($this->subClassSet[$className])) { $this->subClassSet[$className] = true; $this->subClasses[] = $className; } } ``` Alternatively, key `$subClasses` by class name directly (breaking change to indexed access). --- ## Call Sites | File | Line | Context | |------|------|---------| | `src/Mapping/ClassMetadata.php` | 2313 | `addSubClass()` | | `src/Mapping/ClassMetadataFactory.php` | 152 | bulk inherit from parent | | `src/Mapping/ClassMetadataFactory.php` | 386 | discriminator map abstract class discovery | --- ## Speedup Estimate Metadata loading for a hierarchy with 50 subclasses: ~50× reduction in membership check work. Impact is startup/warm-up only (metadata is cached after first load). Applications without metadata caching (e.g., dev mode with `auto_generate_proxy_classes`) see this on every request.