Skip to content

Add PHPStan rule verifying #[DbQuery] contracts (MVP)#105

Open
koriym wants to merge 2 commits into
1.xfrom
claude/vigilant-thompson-2qrmjj
Open

Add PHPStan rule verifying #[DbQuery] contracts (MVP)#105
koriym wants to merge 2 commits into
1.xfrom
claude/vigilant-thompson-2qrmjj

Conversation

@koriym

@koriym koriym commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

Adds a self-contained PHPStan extension under tools/phpstan/ that statically verifies the #[DbQuery] contract of Ray.MediaQuery interfaces — the cross-artifact invariants PHPStan core cannot see. This is the MVP from the agreed plan (Phase 0 de-risking + Phase 1 rule), scoped to checks with near-zero false positives.

Checks (error identifiers)

Identifier Meaning
rayMediaQuery.sqlFileNotFound #[DbQuery('id')] has no {sqlDir}/id.sql in any configured directory.
rayMediaQuery.emptySqlFile The resolved id.sql exists but is empty.
rayMediaQuery.pagerReturnMismatch #[Pager] present but return type is not a PagesInterface.
rayMediaQuery.missingPagerAttribute Return type is a PagesInterface but #[Pager] is missing.
rayMediaQuery.invalidFactory factory: names a missing class or missing factory method.

Deliberately not implemented:

  • Invalid type: literal — PHPStan core already reports it via the 'row'|'row_list' constructor type (argument.type). Verified with a fixture.
  • Placeholder ↔ parameter matching — deferred to the SQL-lexer phase; needs #[Input] flattening and #[SqlTemplate] handling first to stay false-positive-free.

Phase 0 findings (de-risked before building)

  • InClassMethodNode does fire on bodiless interface methods (every #[DbQuery] consumer is an interface — proven with a PoC before relying on it).
  • RuleTestCase needs phpstan and phpunit in the same vendor. The repo's vendor-bin/tools has phpstan but no phpunit; the root has phpunit but no phpstan. So the extension carries its own composer.json/vendor with both — this also matches the eventual standalone ray/media-query-phpstan package.

Integration

  • phpstan.neon now includes tools/phpstan/extension.neon with rayMediaQuery.sqlDirectories pointed at the tutorial SQL set (the only #[DbQuery] consumers in the analysed paths, since tests/Fake/* is excluded). The rule runs against the tutorial's 12 #[DbQuery] methods and is green.
  • Rule source self-analyses at level max (tools/phpstan/phpstan.neon.dist) and follows PHPStan's API best practices (Type::getConstantStrings()).

Verification

  • composer sa (psalm + phpstan): green
  • composer test: 109 passed
  • tools/phpstan rule tests (RuleTestCase): 3 passed — each error case + the valid cases (exact-match, so no false positives)
  • Negative test: removing a tutorial .sql correctly raises rayMediaQuery.sqlFileNotFound; restoring returns to green.

Caveat documented

PHPStan's result cache is keyed on PHP files, so a .sql-only change won't invalidate it locally (CI is unaffected). tools/phpstan/README.md documents running composer clean after SQL edits.

Follow-ups (later phases, not in this PR)

  • Phase 2: NamedParameterExtractor SQL lexer (string/comment/::cast/:= aware).
  • Phase 3: placeholder ↔ parameter matching, limited to methods without #[Input]/#[SqlTemplate].
  • Phase 4: strict checks (e.g. void + SELECT, single-row nullability, factory return-type alignment).
  • CI: run the tools/phpstan rule tests in the pipeline (currently run locally via its own vendor).

https://claude.ai/code/session_01HWD3hygK9SGrLvZvehW28x


Generated by Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a PHPStan static analysis extension to validate database query interface contracts, including SQL file resolution, pager attribute verification, and factory method validation.
  • Documentation

    • Added documentation for the PHPStan extension, including configuration instructions and usage guidelines.
  • Tests

    • Added test coverage for the PHPStan validation rules.

Introduce a self-contained PHPStan extension under tools/phpstan/ that
statically checks the cross-artifact invariants of Ray.MediaQuery
#[DbQuery] interface methods that PHPStan core cannot see:

- rayMediaQuery.sqlFileNotFound / emptySqlFile: the {id}.sql backing a
  #[DbQuery('id')] exists in a configured directory and is non-empty.
- rayMediaQuery.pagerReturnMismatch / missingPagerAttribute: #[Pager] is
  present iff the return type is a PagesInterface.
- rayMediaQuery.invalidFactory: a declared factory: class exists and
  exposes the configured factory method (static or instance).

Invalid type: literals are intentionally left to PHPStan core, which
already reports them via the 'row'|'row_list' constructor type.

The extension ships its own phpstan + phpunit so RuleTestCase can run
(the repo's vendor-bin/tools has phpstan but no phpunit, the root has
phpunit but no phpstan). It is wired into the repo phpstan.neon against
the tutorial SQL set, and rule code self-analyses at level max.

https://claude.ai/code/session_01HWD3hygK9SGrLvZvehW28x
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@koriym, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 16 minutes and 11 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1c281d7-035e-4cef-b54d-9565332fcdfb

📥 Commits

Reviewing files that changed from the base of the PR and between c901091 and e0da9b1.

⛔ Files ignored due to path filters (1)
  • vendor-bin/media-query-phpstan/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • .gitignore
  • composer.json
  • phpstan.neon
  • vendor-bin/media-query-phpstan/README.md
  • vendor-bin/media-query-phpstan/bootstrap.php
  • vendor-bin/media-query-phpstan/composer.json
  • vendor-bin/media-query-phpstan/extension.neon
  • vendor-bin/media-query-phpstan/phpstan.neon.dist
  • vendor-bin/media-query-phpstan/phpunit.xml.dist
  • vendor-bin/media-query-phpstan/src/Rules/DbQueryContractRule.php
  • vendor-bin/media-query-phpstan/src/Support/AttributeFinder.php
  • vendor-bin/media-query-phpstan/src/Support/DbQueryAttribute.php
  • vendor-bin/media-query-phpstan/src/Support/SqlFileResolver.php
  • vendor-bin/media-query-phpstan/tests/Fixture/Data/FactoryInterface.php
  • vendor-bin/media-query-phpstan/tests/Fixture/Data/FactoryWithoutFactoryMethod.php
  • vendor-bin/media-query-phpstan/tests/Fixture/Data/PagerInterface.php
  • vendor-bin/media-query-phpstan/tests/Fixture/Data/SqlFilesInterface.php
  • vendor-bin/media-query-phpstan/tests/Fixture/Data/ValidFactory.php
  • vendor-bin/media-query-phpstan/tests/Fixture/sql/empty_query.sql
  • vendor-bin/media-query-phpstan/tests/Fixture/sql/existing_query.sql
  • vendor-bin/media-query-phpstan/tests/Rules/DbQueryContractRuleTest.php
📝 Walkthrough

Walkthrough

This PR introduces a complete PHPStan extension package (ray/media-query-phpstan) that statically verifies #[DbQuery] attribute contract invariants for Ray MediaQuery. The extension validates SQL file existence and non-emptiness, enforces pager return type coherence, and checks factory class and method validity.

Changes

PHPStan DbQuery Contract Extension

Layer / File(s) Summary
Extension Package & Configuration
.gitignore, composer.json, phpstan.neon, tools/phpstan/README.md, tools/phpstan/composer.json, tools/phpstan/extension.neon, tools/phpstan/phpstan.neon.dist
Project-level and extension package configuration: gitignore rules for tools, root composer autoload-dev mapping, PHPStan includes and rayMediaQuery parameters, extension package manifest with PSR-4 autoloading, service wiring for SqlFileResolver and DbQueryContractRule, and comprehensive documentation covering MVP scope, configuration, caching caveats, and development setup.
Support Utilities
tools/phpstan/src/Support/AttributeFinder.php, tools/phpstan/src/Support/DbQueryAttribute.php, tools/phpstan/src/Support/SqlFileResolver.php
AttributeFinder searches AST method attributes by name with leading backslash normalization; DbQueryAttribute parses #[DbQuery] arguments (id, type, factory) into constant string values or null; SqlFileResolver maps query ids to {dir}/{id}.sql file paths across configured SQL directories.
Contract Validation Rule
tools/phpstan/src/Rules/DbQueryContractRule.php
DbQueryContractRule targets #[DbQuery]-annotated methods and validates three contract invariants: (1) SQL file resolution and non-emptiness via checkSqlFile(), (2) pager attribute presence matching PagesInterface return type via checkPager(), and (3) factory class existence and configured method presence via checkFactory(), emitting identifier-based diagnostic errors when violated.
Test Infrastructure & Fixtures
tools/phpstan/bootstrap.php, tools/phpstan/phpunit.xml.dist, tools/phpstan/tests/Fixture/Data/*.php, tools/phpstan/tests/Fixture/sql/*.sql
Bootstrap script loads package and conditional parent project autoloaders; PHPUnit configuration defines testsuite and cache settings; fixture interfaces (FactoryInterface, PagerInterface, SqlFilesInterface) and factory classes (ValidFactory, FactoryWithoutFactoryMethod) represent valid and invalid contract scenarios; SQL fixture files test existence, emptiness, and resolution logic.
Rule Test Cases
tools/phpstan/tests/Rules/DbQueryContractRuleTest.php
DbQueryContractRuleTest extends PHPStan's RuleTestCase and wires the rule with test SQL directory; three test methods verify contract violation detection for missing/empty SQL files (testSqlFileExistence()), pager/return-type mismatches (testPagerCoherence()), and missing/invalid factories (testFactoryExistence()).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • NaokiTsuchiya
  • jingu

Poem

A static guardian now stands tall,
Checking contracts one and all—
SQL files, pagers, factories bright,
PHPStan rules make MediaQuery right! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add PHPStan rule verifying #[DbQuery] contracts (MVP)' clearly and concisely describes the main change: adding a PHPStan extension that validates #[DbQuery] attribute contracts. It accurately summarizes the PR's primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vigilant-thompson-2qrmjj

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 and usage tips.

@koriym koriym marked this pull request as ready for review June 12, 2026 17:20
@koriym

koriym commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 3-4: The .gitignore currently references /tools/phpstan/vendor/
and /tools/phpstan/composer.lock which breaks the repo's tool-isolation
convention; move the PHPStan tool workspace into the isolated path
vendor-bin/tools/phpstan (i.e., install tool deps via
bamarni/composer-bin-plugin per composer.json) and update .gitignore entries to
match vendor-bin/tools/phpstan/vendor/ and
vendor-bin/tools/phpstan/composer.lock, or alternatively update the repository
tooling guideline in composer.json to accept /tools/phpstan/ if this PR
intentionally changes the standard.

In `@tools/phpstan/README.md`:
- Around line 85-98: The fenced code block that lists the tools/phpstan tree
(starting with the line containing "tools/phpstan/") is missing a language tag;
update the opening fence from ``` to ```text so the block becomes a fenced
"text" block (e.g., change the fence that precedes "tools/phpstan/" to ```text)
to satisfy markdownlint MD040 while leaving the inner filenames like
extension.neon, phpstan.neon.dist, src/Rules/DbQueryContractRule.php,
Support/AttributeFinder.php, tests/Rules/DbQueryContractRuleTest.php, and
Fixture/sql/*.sql unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e26d3027-0ba1-490a-8744-986f5c107171

📥 Commits

Reviewing files that changed from the base of the PR and between 001b3ce and c901091.

📒 Files selected for processing (21)
  • .gitignore
  • composer.json
  • phpstan.neon
  • tools/phpstan/README.md
  • tools/phpstan/bootstrap.php
  • tools/phpstan/composer.json
  • tools/phpstan/extension.neon
  • tools/phpstan/phpstan.neon.dist
  • tools/phpstan/phpunit.xml.dist
  • tools/phpstan/src/Rules/DbQueryContractRule.php
  • tools/phpstan/src/Support/AttributeFinder.php
  • tools/phpstan/src/Support/DbQueryAttribute.php
  • tools/phpstan/src/Support/SqlFileResolver.php
  • tools/phpstan/tests/Fixture/Data/FactoryInterface.php
  • tools/phpstan/tests/Fixture/Data/FactoryWithoutFactoryMethod.php
  • tools/phpstan/tests/Fixture/Data/PagerInterface.php
  • tools/phpstan/tests/Fixture/Data/SqlFilesInterface.php
  • tools/phpstan/tests/Fixture/Data/ValidFactory.php
  • tools/phpstan/tests/Fixture/sql/empty_query.sql
  • tools/phpstan/tests/Fixture/sql/existing_query.sql
  • tools/phpstan/tests/Rules/DbQueryContractRuleTest.php

Comment thread .gitignore Outdated
Comment on lines +3 to +4
/tools/phpstan/vendor/
/tools/phpstan/composer.lock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move PHPStan tool dependency isolation to vendor-bin/tools/.

Line 3 and Line 4 indicate a standalone dependency workspace under tools/phpstan/ (vendor/ and composer.lock), which conflicts with the repository’s declared tool-isolation convention. Please migrate this tooling to vendor-bin/tools/ (or update the guideline/policy in the same PR if this is intentionally changing standards). As per coding guidelines, composer.json: “Use bamarni/composer-bin-plugin for isolated tool dependencies in vendor-bin/tools/”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 3 - 4, The .gitignore currently references
/tools/phpstan/vendor/ and /tools/phpstan/composer.lock which breaks the repo's
tool-isolation convention; move the PHPStan tool workspace into the isolated
path vendor-bin/tools/phpstan (i.e., install tool deps via
bamarni/composer-bin-plugin per composer.json) and update .gitignore entries to
match vendor-bin/tools/phpstan/vendor/ and
vendor-bin/tools/phpstan/composer.lock, or alternatively update the repository
tooling guideline in composer.json to accept /tools/phpstan/ if this PR
intentionally changes the standard.

Source: Coding guidelines

Comment thread tools/phpstan/README.md Outdated
Comment on lines +85 to +98
```
tools/phpstan/
extension.neon # parameters schema + service registration
phpstan.neon.dist # self-analysis of the rule code
src/
Rules/DbQueryContractRule.php
Support/AttributeFinder.php
Support/DbQueryAttribute.php
Support/SqlFileResolver.php
tests/
Rules/DbQueryContractRuleTest.php
Fixture/Data/*.php # interfaces/classes analysed by the rule tests
Fixture/sql/*.sql # SQL fixtures
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language to the fenced block in Layout section.

The code fence starting at Line 85 has no language tag (MD040). Use something like text to satisfy markdownlint consistently.

Suggested patch
-```
+```text
 tools/phpstan/
   extension.neon              # parameters schema + service registration
   phpstan.neon.dist           # self-analysis of the rule code
   src/
@@
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 85-85: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/phpstan/README.md` around lines 85 - 98, The fenced code block that
lists the tools/phpstan tree (starting with the line containing
"tools/phpstan/") is missing a language tag; update the opening fence from ```
to ```text so the block becomes a fenced "text" block (e.g., change the fence
that precedes "tools/phpstan/" to ```text) to satisfy markdownlint MD040 while
leaving the inner filenames like extension.neon, phpstan.neon.dist,
src/Rules/DbQueryContractRule.php, Support/AttributeFinder.php,
tests/Rules/DbQueryContractRuleTest.php, and Fixture/sql/*.sql unchanged.

Source: Linters/SAST tools

Address review feedback: the extension's isolated dependency set conflicted
with the repo convention (tool deps live under vendor-bin/ via
bamarni/composer-bin-plugin). Relocate the whole package from tools/phpstan/
to vendor-bin/media-query-phpstan/ so it is installed as its own bamarni bin,
matching vendor-bin/tools/.

- Relocate package; root autoload-dev and phpstan.neon include now point at
  vendor-bin/media-query-phpstan/.
- .gitignore: drop the bespoke tools/phpstan/* entries; the existing
  /vendor-bin/*/vendor/ rule covers the vendor dir, and composer.lock is
  committed to match vendor-bin/tools/. Ignore /vendor-bin/*/.phpunit.cache/.
- bootstrap.php still resolves the parent autoloader (two levels up), so the
  root binaries can drive the extension's phpunit/phpstan configs:
    ./vendor/bin/phpunit  -c vendor-bin/media-query-phpstan
    ./vendor/bin/phpstan  analyse -c vendor-bin/media-query-phpstan/phpstan.neon.dist
- README: update paths/commands and add the missing fenced-block language tag
  (markdownlint MD040).

composer sa, composer test (109), the extension rule tests (3) and the rule
self-analysis all pass after the move.

https://claude.ai/code/session_01HWD3hygK9SGrLvZvehW28x
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.

2 participants