Conversation
Joining the same association through different paths in one query (e.g. `Creator.Contacts` and `Modifier.Contacts`) collides on the join alias. contain() degrades the duplicates to the select strategy, while matching()/joinWith()/notMatching() overwrite the first join and now assert on the conflict. When `Database.deepAssociations` is enabled (or `EagerLoader::setDeepAssociations(true)` is called), associations joined below the top level are aliased by their full path with dots replaced by underscores (`Creator_Contacts`, `Articles_Authors_Profiles`). This applies to contain, matching, joinWith and notMatching, including nested belongsToMany junctions. Conditions and fields written against the target alias inside contain callbacks, association conditions, finders and beforeFind listeners are rewritten to the path alias. `_matchingData` keys use the path alias as well. The option defaults to off, so existing queries are unchanged. Refs cakephp#18929, cakephp#17679 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CufZXaq7Vyk9J4MUcX2otP
Use `self` for the `_matchingLoader()` return type and declare the void return type on the beforeFind closure in the deep associations test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CufZXaq7Vyk9J4MUcX2otP
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.
Summary
Explores upstream cakephp#18929 (related: cakephp#17679, cakephp#18932), which upstream closed because rewriting join aliases is a major-version change. This puts the fix behind an opt-in option so it can be evaluated without affecting existing queries.
Problem: joining the same association through different paths in one query (e.g.
Creator.ContactsandModifier.Contacts) collides on the join alias.contain()silently degrades the duplicates to theselectstrategy;matching(),innerJoinWith(),leftJoinWith()andnotMatching()overwrite the first join (5.3 now asserts on it).Fix: when
Configure::read('Database.deepAssociations')is true, orEagerLoader::setDeepAssociations(true)is called on a query's loader, every association joined below the top level is aliased by its full path with dots replaced by underscores:UsersUsers(top level unchanged)Users.ProfilesUsers_ProfilesArticles.Authors.ProfilesArticles_Authors_ProfilesArticles.Tags(belongsToMany junction)Articles_ArticlesTagsPath aliases are used everywhere when the option is on, not only when a conflict exists.
Behaviour when enabled
contain,matching,joinWithandnotMatching, including nested belongsToMany.conditions, finders andbeforeFindlisteners (incl. TranslateBehavior) are rewritten to the path alias automatically.where(['Creator_Contacts.name' => ...])._matchingDatakeys use the path alias, e.g._matchingData['Articles_Authors'].contain()paths are joined instead of downgraded to separateselectqueries.Changes
EagerLoader: option handling, path alias computation in_normalizeContain(), joins keyed by query alias,deepAlias()helper, external loads keyed by the parent's query alias,_fixStrategies()skipped in deep mode.EagerLoadable: newqueryAlias()/sourceAlias().Association::attachTo(): honoursalias/sourceAliasoptions; rewrites identifiers in the surrogate query's where and select clauses (sub-queries left untouched);transformRow()/defaultRowValue()accept a source alias.BelongsToMany: junction alias and join conditions follow the path alias.ResultSetFactory,CommonQueryTrait::addDefaultTypes(),HasMany,HasOne,BelongsTo: plumbing for source alias and column types.tests/TestCase/ORM/DeepAssociationsTest.php(18 tests).phpstan-baseline.neon: one count adjusted.Verification
Authors.created,count(tags.id)) or_matchingData['Authors']-style keys. Left untouched since the option defaults to off.Open questions for review
_was chosen because__is the select-key separator. An association name containing_could theoretically produce an ambiguous alias.EagerLoader::setDeepAssociations()should stay public, or the Configure key should be the only switch.🤖 Generated with Claude Code
https://claude.ai/code/session_01CufZXaq7Vyk9J4MUcX2otP