Skip to content

Narrow Cake\ORM\Association::find() return to SelectQuery<TargetEntity>#68

Open
dereuromark wants to merge 2 commits into
CakeDC:4.next-cake5from
dereuromark:feature/association-find-narrowing
Open

Narrow Cake\ORM\Association::find() return to SelectQuery<TargetEntity>#68
dereuromark wants to merge 2 commits into
CakeDC:4.next-cake5from
dereuromark:feature/association-find-narrowing

Conversation

@dereuromark
Copy link
Copy Markdown
Collaborator

Summary

Narrows Cake\ORM\Association::find() from SelectQuery<EntityInterface|array> to SelectQuery<TargetEntity>, eliminating the need for inline var annotations on every $this->Bar->Foos->find()->first() chain.

Problem

Cake core declares Association::find() as SelectQuery<EntityInterface|array>. The target entity type is lost the moment a query is built through an association property, so PHPStan reports property.notFound / property.nonObject on any subsequent ->id access:

$dance = $this->Events->Dances->find()->where(['slug' => $slug])->firstOrFail();
$dance->id; // ERROR: Access to an undefined property Cake\Datasource\EntityInterface::$id

Real codebases work around this by sprinkling inline var annotations on every callsite — tedious, noisy, and easy to forget.

Solution

AssociationFindDynamicReturnTypeExtension reads the association's target table type — already exposed as BelongsTo<UsersTable> etc. by the existing TableAssociationTypeNodeResolverExtension — derives the entity class via the standard CakePHP naming convention, and returns SelectQuery<TargetEntity> instead.

// Before: $dance is EntityInterface
// After:  $dance is App\Model\Entity\Dance (once Cake 5.3.6 lands)
$dance = $this->Events->Dances->find()->firstOrFail();

The entity-derivation logic that was inlined in RepositoryEntityDynamicReturnTypeExtension is extracted to a new EntityClassFromTableClassTrait so both extensions stay consistent.

Cake version interaction

Downstream narrowing of first() / firstOrFail() to Entity|null / Entity depends on Cake core's per-method return annotations from cakephp/cakephp#19439, merged for the 5.3.6 release. This extension is forward-compatible: on Cake < 5.3.6 it still narrows the SelectQuery generic correctly (verified by the new test), and the first() / firstOrFail() narrowing comes online automatically once consumers upgrade.

Real-world impact

Applied locally to a CakePHP 5.3 app: stripped 10+ inline var annotations across controllers and tables, all the call sites that go through association traversal. PHPStan stays clean at level: 7 with treatPhpDocTypesAsCertain: false.

Files

  • src/Type/AssociationFindDynamicReturnTypeExtension.php — new extension
  • src/Traits/EntityClassFromTableClassTrait.php — shared entity-class resolution
  • src/Type/RepositoryEntityDynamicReturnTypeExtension.php — uses the new trait, drops the duplicated method
  • extension.neon — registers the new extension
  • tests/TestCase/Type/Fake/AssociationFindCorrectUsage.php — fixture using assertType on the existing NotesTable / MyUsersTable test app
  • tests/TestCase/Type/AssociationFindDynamicReturnTypeExtensionTest.php — runs PHPStan against the fixture

Note on the suppressed PHPStan rule

One phpstan-ignore-next-line phpstanApi.instanceofType on instanceof GenericObjectType: there is no non-deprecated equivalent in PHPStan 2.x for reading template parameters from a typed type. Happy to switch to a different approach if the maintainers have a preferred pattern.

Cake core declares Association::find() as SelectQuery<EntityInterface|array>,
losing the target table's entity type. That forces consumers to write inline
var annotations on every $this->Bar->Foos->find()->first() chain just to
avoid property.notFound / property.nonObject errors.

This change adds AssociationFindDynamicReturnTypeExtension, which reads the
association's target table type — already exposed as
BelongsTo<UsersTable> etc. by TableAssociationTypeNodeResolverExtension —
derives the entity class via the standard CakePHP naming convention, and
returns SelectQuery<TargetEntity> instead.

The entity-derivation logic that was inlined in
RepositoryEntityDynamicReturnTypeExtension is extracted to a new
EntityClassFromTableClassTrait so both extensions stay consistent.

Downstream narrowing of first() / firstOrFail() to Entity|null / Entity
depends on Cake core's per-method return annotations (cakephp/cakephp#19439,
merged for the 5.3.6 release); this extension is forward-compatible and
contributes the SelectQuery template needed for that chain to resolve.

Includes a unit test that asserts the narrowed SelectQuery template on the
existing test_app fixture.
@dereuromark dereuromark added the enhancement New feature or request label May 20, 2026
@dereuromark dereuromark requested a review from rochamarcelo May 20, 2026 17:17
Comment thread src/Type/AssociationFindDynamicReturnTypeExtension.php Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants