Skip to content

Add lazy-load guard to detect N+1 relation queries - #591

Open
KalimeroMK wants to merge 7 commits into
yiisoft:masterfrom
KalimeroMK:lazy-load-guard
Open

KalimeroMK wants to merge 7 commits into
yiisoft:masterfrom
KalimeroMK:lazy-load-guard

Conversation

@KalimeroMK

@KalimeroMK KalimeroMK commented Sep 20, 2026 •

Copy link
Copy Markdown

Closes #590

What — LazyLoadGuardTrait overrides retrieveRelation() and reports every lazy load to LazyLoadGuard, which has Log (default) / Strict modes and is configured statically: LazyLoadGuard::set(LazyLoadGuardMode::Strict) or LazyLoadGuard::set(LazyLoadGuardMode::Log, $logger). No events and no dispatcher configuration; models without the trait are not affected. Meant for development and testing.

How — retrieveRelation() is the single lazy-load path; with() populates $related up front so relation() returns before reaching it. Works together with EventsTrait, which doesn't override retrieveRelation(). psr/log added to require-dev + suggest and registered in composer-dependency-analyser.php alongside the other opt-in packages.

Tests — tests/LazyLoadGuardTest.php, 9 cases across all five drivers: counting lazy loads, not counting after with(), each mode, combination with EventsTrait, and reset().

Sqlite suite green (467 tests, 2022 assertions). Psalm, Rector, PHP CS Fixer and composer-dependency-analyser clean.

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 25fd72be-b89a-49e4-afe1-4d5e07eca461

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.69%. Comparing base (f82660c) to head (c06c21e).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##             master     #591   +/-   ##
=========================================
  Coverage     99.69%   99.69%           
- Complexity      662      668    +6     
=========================================
  Files            43       45    +2     
  Lines          1622     1648   +26     
=========================================
+ Hits           1617     1643   +26     
  Misses            5        5           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@KalimeroMK
KalimeroMK marked this pull request as ready for review September 21, 2026 05:23

@Tigrov Tigrov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two questions:

  1. Is this only for development or debugging / profiling?
  2. Why is LazyLoadGuard not designed as a class attribute #[Attribute(Attribute::TARGET_CLASS)]? It will not require properties $only and $except if apply the attribute to a specified AR class.

@KalimeroMK

KalimeroMK commented Sep 23, 2026 •

Copy link
Copy Markdown
Author

@Tigrov

  1. Yes — it's meant for dev/test: Strict in the test suite/CI to fail on N+1, Log on staging. Nobody should enable it in production.
  2. I considered an attribute, but the mode is an environment decision rather than a model one: #[LazyLoadGuard(Strict)] on the class would also throw in production, and a PSR-3 logger can't be passed through attribute arguments.

You're right that only/except are redundant, since the dispatcher is already per class. Off is redundant too — just don't register the listener. Dropped all three in 174a71d.

If you'd prefer, I can reduce this PR to just the BeforeLazyRelationLoad event and move the guard to the docs as an example listener.

@Tigrov Tigrov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The solution looks a bit complicated. Perhaps better to move it to a separate trait where the counter will be enabled inside the method retrieveRelation() without using events. Then it will allow to use only this trait without difficult configurations.

Comment thread src/Trait/EventsTrait.php Outdated
Comment thread docs/traits/events.md Outdated
@KalimeroMK

Copy link
Copy Markdown
Author

Thanks, agreed, the event-based setup was too heavy. Reworked it:

  • Moved to a separate LazyLoadGuardTrait that overrides retrieveRelation() and counts lazy loads directly, without events. BeforeLazyRelationLoad and the EventsTrait changes are removed.
  • LazyLoadGuard is configured statically, like ConnectionProvider:
LazyLoadGuard::set(LazyLoadGuardMode::Strict);
// or
LazyLoadGuard::set(LazyLoadGuardMode::Log, $logger);
  • Using it is just use LazyLoadGuardTrait; in the model. It works together with EventsTrait, since that doesn't override retrieveRelation().

Docs moved to docs/traits/lazy-load-guard.md.

Comment thread src/LazyLoadGuard.php
@Tigrov
Tigrov requested a review from a team September 23, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect N+1 queries: dispatch an event on lazy relation load

2 participants