Add RefersToMorphed relation for cyclic morphed references#570
Merged
Conversation
BelongsToMorphed inherits the hard "parent before child" dependency of BelongsTo and therefore deadlocks the pool when persisting a closed cycle (A > A or A > B > A) in a single transaction. There was no morphed counterpart of RefersTo to break such cycles. Add Relation::REFERS_TO_MORPHED and the RefersToMorphed relation, which mirrors BelongsToMorphed on top of RefersTo: it stores the outer key and the target role (morph key) on the owner but resolves the outer key in a deferred, "soft" way, allowing self-linked and cyclic morphed references to be persisted within one transaction. The morph key is kept in sync with the related role and cleared when the relation is set to null; the BelongsToMorphedLoader is reused. Tests cover A > A and A > B > A (read, single-transaction create, detach) on all four drivers, plus the previously uncovered interaction of both morphed belongs-to/refers-to relations with Options::$ignoreUninitializedRelations.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 2.x #570 +/- ##
============================================
- Coverage 91.63% 91.61% -0.02%
- Complexity 2014 2027 +13
============================================
Files 131 132 +1
Lines 5235 5274 +39
============================================
+ Hits 4797 4832 +35
- Misses 438 442 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Verify the promise reference resolves with the expected number of read queries: a self-reference is taken from the heap (0 queries), a parent that is not yet loaded costs exactly one query, and the back-reference across an A > B > A cycle is served from the heap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover one-level eager loading via Select::load() and batch loading via BulkLoader for the morphed refers-to relation (reusing BelongsToMorphedLoader). Both resolve the parent up front, so accessing it afterwards issues no queries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔍 What was changed
Added a new morphed relation type
Relation::REFERS_TO_MORPHEDtogether with theRefersToMorphedrelation class.RefersToMorphedis toBelongsToMorphedwhatRefersTois toBelongsTo: it stores both the outer key and the target role (morph key) on the owner entity, but resolves the outer key in a deferred / "soft" way instead of as a hard "parent before child" dependency.src/Relation.php— newREFERS_TO_MORPHEDconstant.src/Relation/Morphed/RefersToMorphed.php— new relation extendingRefersTo; adds morph-key handling (reads the target role from the morph column ininitReference, keeps the morph key in sync with the related entity role, and clears it when the relation is set tonull).src/Config/RelationConfig.php— registers the type, reusing the existingBelongsToMorphedLoader(read semantics are identical).Note
The
#[RefersToMorphed]attribute lives in the separatecycle/annotatedpackage and is intentionally not part of this PR.🤔 Why?
BelongsToMorphedinherits the hard ordering dependency ofBelongsTo, so persisting a closed cycle through a morphed relation in a single transaction deadlocks the pool:This affects both self-references (
A > A) and two-entity cycles (A > B > A). For non-morphed relations this is solved byRefersTo, but until now there was no morphed counterpart.RefersToMorphedallows such cyclic / self-linked morphed references to be saved within one transaction (INSERT + deferred UPDATE), exactly likeHasOne/RefersTocycles.📝 Checklist
Functional tests added for
A > AandA > B > A(read, single-transaction create, detach tonull) across all four drivers (SQLite, MySQL, Postgres, SQL Server). Additionally added coverage — previously missing — for the interaction of both morphedbelongs-to/refers-torelations withOptions::$ignoreUninitializedRelations(unset relation property is ignored when the option istrue, and clears both the outer key and the morph key whenfalse).Full SQLite suite, unit tests, Psalm and PHP-CS-Fixer are green.
📃 Documentation
No documentation change shipped in this repo. A follow-up in
cycle/annotated(the#[RefersToMorphed]attribute) and the docs site would complete the feature for end users.