Skip to content

Releases: ovos/cms-doctrine

v8.5.1

17 Jun 18:00
v8.5.1
07b37ba

Choose a tag to compare

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).

Merges #1 and #2.

🧠 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 WeakReference entries, 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, so Doctrine_Connection::flush() still saves records that user code no longer references.
  • Doctrine_Transaction::rollback() now releases $_collections and $invalid. Previously both held strong references until the next successful commit, and stale invalid records could poison that commit with a spurious Doctrine_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_OVERRIDE cached per table with epoch invalidation; failed auto-accessor probes negative-cached.
  • Collection::add() — duplicate foreach replaced with strict in_array() (identity comparison at C speed). New internal addUnchecked() 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 checked add(). Back-reference alias resolved once per collection.
  • Hydrator_Graph row loop — hoisted invariant lookups; pre/postHydrate event dispatch skipped when neither the record listener nor the record class overrides the hook (reflection-detected once per component; Doctrine_Overloadable listeners always dispatched); relation collections fetched once per row.
  • Hydrator_RecordDriver — registered collections keyed by spl_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 $_identityMap directly would now see WeakReference values (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 sets indent_style = tab and trim_trailing_whitespace = false; [*.json] keeps 4-space indent (matching ovos/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 merged main.
  • New tests/Ticket/OV26TestCase.php covers: records collectible after drop, identity-map instance reuse while alive, flush() saving dereferenced new/dirty records, Doctrine_Overloadable record listeners during hydration, duplicate-row dedup for RawSql and diamond joins.

Pull requests: #1 (memory leaks + hot paths), #2 (editorconfig).