Releases: ovos/cms-doctrine
v8.5.1
Memory & performance release for the PHP 8.3–8.5 line. No public API signatures changed. Full suite passes: 450 cases / 4373 assertions, 0 failures (PHP 8.3/8.4/8.5, Ubuntu + Windows).
🧠 Memory leaks fixed (#1)
Every record ever hydrated or created was previously pinned for the lifetime of the connection by two strong-reference roots: Doctrine_Table::$_identityMap (keyed by PK) and Doctrine_Table_Repository (keyed by OID). This forced batch jobs (newsletter processing in leadersnet / westbahn-website) to work around Doctrine with manual ->free() calls and chunked memory limits.
- Weak identity map + repository — both now store
WeakReferenceentries, so records become collectible the moment user code drops them. Dead entries are compacted by an amortized sweep. Identity-map semantics are preserved: while a record is alive, the same PK returns the same instance. flush()semantics preserved via pinning — records with unsaved changes (TDIRTY/DIRTY) are strongly pinned in the repository and unpinned on save/delete/evict/state-change, soDoctrine_Connection::flush()still saves records that user code no longer references.Doctrine_Transaction::rollback()now releases$_collectionsand$invalid. Previously both held strong references until the next successful commit, and stale invalid records could poison that commit with a spuriousDoctrine_Validator_Exception.
Steady-state memory in hydrate-and-drop loops is now flat (previously grew unbounded):
| Scenario | before | after |
|---|---|---|
new Record + save() ×2000 |
4013 KB leaked | 67 KB (bounded bookkeeping) |
Table::find() ×2000 |
856 KB | 29 KB |
| join query loop ×50 | 284 KB | flat |
Manual ->free() calls and ATTR_AUTO_FREE_QUERY_OBJECTS continue to work unchanged — they are just no longer necessary for steady-state memory.
⚡ Performance (#1)
| Operation | before | after |
|---|---|---|
| Record hydration, 1000-row join | 19.1 ms | 12.3 ms (−35%) |
| Array hydration, same query | 3.2 ms | 2.5 ms (−20%) |
Magic field read $record->name |
0.55 µs | 0.32 µs (−43%) |
set() |
1.21 µs | 0.76 µs (−37%) |
Record::get()/set()— one static-array lookup for registered accessors/mutators;ATTR_AUTO_ACCESSOR_OVERRIDEcached per table with epoch invalidation; failed auto-accessor probes negative-cached.Collection::add()— duplicateforeachreplaced with strictin_array()(identity comparison at C speed). New internaladdUnchecked()lets the hydrator skip the O(n) scan only where its identifier map already guarantees uniqueness; simple queries and diamond-join relation collections keep the checkedadd(). Back-reference alias resolved once per collection.Hydrator_Graphrow loop — hoisted invariant lookups;pre/postHydrateevent dispatch skipped when neither the record listener nor the record class overrides the hook (reflection-detected once per component;Doctrine_Overloadablelisteners always dispatched); relation collections fetched once per row.Hydrator_RecordDriver— registered collections keyed byspl_object_id, snapshotted once per query instead of once per row.
Behavior notes (intentional, observable only in edge cases)
Repository::count()/ iteration now reflect only live records.- A rolled-back transaction no longer snapshots its collections at the next unrelated commit and no longer reports stale
getInvalid()after rollback. - An unreferenced, clean, unreachable record can no longer be resurrected through the identity map — a fresh instance is hydrated from the DB (same data, new object).
- Subclasses that touch the protected
$_identityMapdirectly would now seeWeakReferencevalues (neither consumer project does this).
🧹 Tooling — .editorconfig (#2)
The library is tab-indented and, by convention, blank lines carry the surrounding block's tab indentation, but .editorconfig declared indent_style = space for PHP and trim_trailing_whitespace = true — so an editorconfig-aware editor would strip those tabs on save.
[*.php]now setsindent_style = tabandtrim_trailing_whitespace = false;[*.json]keeps 4-space indent (matchingovos/cms-zf).- Blank-line indentation normalized across
lib/,tools/,tests/(345 files). Whitespace-only change — string/heredoc/inline-HTML bodies verified byte-identical to the prior tree via the PHP tokenizer.
✅ Verification
php tests/run.php— 450 cases / 4373 assertions, 0 failures on the mergedmain.- New
tests/Ticket/OV26TestCase.phpcovers: records collectible after drop, identity-map instance reuse while alive,flush()saving dereferenced new/dirty records,Doctrine_Overloadablerecord listeners during hydration, duplicate-row dedup for RawSql and diamond joins.
Pull requests: #1 (memory leaks + hot paths), #2 (editorconfig).