Skip to content

Harden FakeQuery release baseline#2

Open
koriym wants to merge 11 commits into
1.xfrom
codex/fake-query-release-hardening
Open

Harden FakeQuery release baseline#2
koriym wants to merge 11 commits into
1.xfrom
codex/fake-query-release-hardening

Conversation

@koriym

@koriym koriym commented May 15, 2026

Copy link
Copy Markdown
Member

Summary

  • Add release-hardening planning docs and CI for PHP 8.2-8.5.
  • Update package metadata and dependency declarations around Ray.MediaQuery 1.1.
  • Add factory hydration coverage for static and injected #[DbQuery(factory: ...)] factories.
  • Validate nested fixture query IDs recursively.
  • Add select-side constructor-based PostQueryInterface result wrapper support.

Scope

  • Keeps Ray.FakeQuery focused on select fixtures.
  • Does not emulate PDO or PostQueryContext for DML metadata.
  • Leaves AffectedRows / InsertedRow support for a separate explicit metadata fixture design if needed.

Tests

  • composer tests
  • composer crc
  • composer validate --strict

Summary by CodeRabbit

  • New Features

    • Factory-based result hydration and PostQueryInterface support; PDO-compatible fake DB wrapper for test mode
    • Recursive fixture discovery, normalized multi-dir fake handling, and JSONL windowing/param filtering
  • Documentation

    • README updated with fixture format, factory usage, and Ray.MediaQuery 1.1 guidance; design/planning docs rewritten for release hardening
  • Tests

    • New factory-backed/post-query tests, fixtures, and helpers; added negative tests for missing factories
  • Chores

    • CI workflow added; package metadata updated; .gitignore extended
  • Bug Fixes

    • Stricter fixture validation detecting unknown/nested fake files

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds factory-backed hydration and PostQueryInterface (Ray.MediaQuery ^1.1) select-result support, makes JsonHydrator DbQuery-aware, implements recursive fixture validation, updates CI/composer/README/.gitignore, and adds tests and fixtures covering static/injected factories and post-query selection hydration.

Changes

Factory hydration and PostQueryInterface result flow

Layer / File(s) Summary
Project metadata, CI, README
composer.json, README.md, .github/workflows/ci.yml, .gitignore
Composer metadata and platform php updated, README documents fixture formats and PostQuery support, new CI workflow added, and .phpunit.cache/ ignored.
Planning & design docs
.planning/findings.md, .planning/progress.md, .planning/task_plan.md, DESIGN.md
Rewrites findings, progress, task plan, and design notes in English focusing on release-hardening scope, fixture contracts, phases, and validation rules.
Module, config, and exception
src/FakeQueryModule.php, src/FakeQueryConfig.php, src/Exception/InvalidFactoryException.php
FakeQueryConfig trims trailing separators; FakeQueryModule wires FactoryMethod binding, changes interceptor binding, and validates fixtures recursively deriving normalized query ids; new InvalidFactoryException added.
Interceptor: dispatch and file loading
src/FakeQueryInterceptor.php
invoke() computes entityClass, detects PostQueryInterface return types, dispatches to selectPostQuery() for JSONL/post-query results, centralizes readContent/readJsonl file loading, and forwards DbQuery to hydrator.
Hydrator: factory-aware hydration
src/JsonHydrator.php
Becomes stateful with constructor for FactoryMethod and InjectorInterface; hydrate() accepts optional DbQuery; row/list paths resolve and invoke static or injected factory callables when configured.
FakeQueryPdo: PDO wrapper
src/FakeQueryPdo.php
Adds a PDO-backed wrapper providing fetch/yield/prepare helpers and stricter getAttribute/quoting behaviors used by post-query flows and tests.
Tests, fixtures, entities, and contracts
tests/Fake/..., tests/FakeQueryModuleTest.php, tests/FakeUnknownNested/*
Adds immutable FactoryTodoEntity, StaticTodoFactory, InjectedTodoFactory, MissingMethodTodoFactory, query interfaces with DbQuery(..., factory: ...), TodoSelection PostQuery result, JSON/JSONL fixtures (including nested paths and missing-factory cases), ThrowingSqlQuery test double, and expanded tests covering factory hydration, post-query selection, unknown nested fixtures, and config/module edge cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ray-di/Ray.FakeQuery#1: Establishes the initial FakeQueryInterceptor, FakeQueryModule, and JsonHydrator implementations that this PR extends with factory-driven hydration and PostQueryInterface result wrapper support.

Poem

A rabbit found fixtures in a nested heap,
Factories stitch entities while tests count sheep,
PostQuery selections hop out in a row,
CI hums, composer aligns — branches flow,
Hooray — the fake-data garden’s set to grow! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.02% 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 PR title clearly summarizes the main objective: hardening the Ray.FakeQuery release by establishing a baseline for 1.0.0 readiness with factory support, nested fixture validation, and CI coverage.
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 codex/fake-query-release-hardening

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 May 15, 2026 01:22
@koriym

koriym commented May 15, 2026

Copy link
Copy Markdown
Member Author

Downstream verification completed in koriym/BEAR.AppKata#2.

Verified usage:

  • Installed this branch as ray/fake-query in BEAR.AppKata.
  • First tested with a local Composer path repository symlink, then switched BEAR.AppKata to this GitHub VCS branch so CI can install it.
  • Replaced Admin read hard-coded fake classes with JSON/JSONL fixtures plus FakeQueryModule.
  • Confirmed #[DbQuery(factory: ...)] hydration, missing nullable row behavior, and constructor-based typed rowlist wrapper support.

BEAR.AppKata checks after push:

  • All tests: pass
  • Compile: pass
  • All Coverages: pass
  • apidoc: pass

Ray.FakeQuery checks are also passing for PHP 8.2, 8.3, 8.4, 8.5 and Quality.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
composer.json (1)

106-108: ⚡ Quick win

Consider using "8.2" instead of "8.2.0" for the platform PHP version.

The platform.php constraint is set to the exact version 8.2.0. While this is functionally valid, it's less common than omitting the patch version (e.g., "php": "8.2"). Using the minor version only provides more flexibility with patch releases while still constraining the major and minor versions for dependency resolution.

🤖 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 `@composer.json` around lines 106 - 108, The composer.json "platform" PHP
constraint currently pins the exact patch version ("php": "8.2.0"); update the
value to the minor-only constraint "8.2" (i.e., change platform -> php from
"8.2.0" to "8.2") so dependency resolution allows any 8.2.x patch release while
still constraining major/minor; locate the "platform" -> "php" entry in
composer.json to make this change.
src/FakeQueryModule.php (1)

51-51: 💤 Low value

Hard-coding 'factory' makes the factory method name a global, undocumented convention.

#[FactoryMethod] is bound to the literal string 'factory', which forces every DbQuery(factory: ...) target across the entire app to expose a method named exactly factory. This is fine as a v1.0 convention, but it's worth surfacing in the README/documentation and ideally driving it from a constant (e.g., JsonHydrator::DEFAULT_FACTORY_METHOD) so the string lives in exactly one place.

🤖 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 `@src/FakeQueryModule.php` at line 51, The binding currently hard-codes the
factory method name ('factory') via FactoryMethod::class in FakeQueryModule
(->toInstance('factory')), which makes the convention global and undocumented;
change the literal to a single shared constant (e.g. replace 'factory' with
JsonHydrator::DEFAULT_FACTORY_METHOD or another appropriately named constant on
the hydrator/DbQuery target) and update the README/docs to document the
#[FactoryMethod] convention so the string/value lives in one place and the
convention is explicit.
src/FakeQueryInterceptor.php (1)

113-126: 💤 Low value

readContent() ignores the nullable-return convention used elsewhere.

The standard query path (lines 74–80) honours isNullable($returnType) and returns null when a fixture file is missing on a nullable return type, but selectPostQuery()readContent() unconditionally throws FakeJsonNotFoundException. If a method declares ?SomePostQuery as its return type, behaviour will diverge from regular row/list queries. If this asymmetry is intentional (containers are always required), please document it on readContent()/selectPostQuery() so future maintainers don't "fix" it inconsistently.

🤖 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 `@src/FakeQueryInterceptor.php` around lines 113 - 126, readContent() currently
always throws FakeJsonNotFoundException when the fixture file is missing, which
diverges from the standard query path that uses isNullable($returnType) to
return null for nullable returns; update the code so selectPostQuery() (or
readContent()) checks the return type nullability via isNullable($returnType)
and returns null when the file is missing and the return type is nullable,
otherwise keep throwing FakeJsonNotFoundException; alternatively, if container
fixtures must always exist, add a clear docblock to readContent() and
selectPostQuery() stating that container post queries are required and will
always throw when missing to prevent future unintended changes.
🤖 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 @.planning/progress.md:
- Line 43: Add a blank line between the "Test Results" heading and the table row
starting with "| Date | Command | Result | Notes |" so the table is surrounded
by a blank line per markdownlint MD058; locate the "Test Results" heading and
insert an empty line immediately above the table (the line containing "| Date |
Command | Result | Notes |") to satisfy the lint rule.

In @.planning/task_plan.md:
- Line 120: Add a single blank line immediately before the "Errors Encountered"
markdown table header (the line starting with "| Error | Attempt | Resolution
|") so the table is separated from preceding content and satisfies markdownlint
MD058; locate the table header in .planning/task_plan.md and insert one empty
line above it.

In `@src/FakeQueryInterceptor.php`:
- Around line 99-105: selectPostQuery currently instantiates the post-query via
Reflection and constructor args which breaks Ray.MediaQuery v1.1's contract;
instead build a PostQueryContext using the hydrated rows returned by
$this->hydrator->hydrate($this->readJsonl($dbQuery), $entityClass, false,
$dbQuery) plus the upstream context (statement, PDO, bind values) and then call
the static factory $postQueryClass::fromContext($context) (referencing
PostQueryInterface::fromContext and PostQueryContext) rather than newInstance;
update selectPostQuery to create and pass that PostQueryContext to the static
fromContext method so implementations like TodoSelection and others follow the
documented contract.

In `@src/FakeQueryModule.php`:
- Around line 72-91: The recursive directory iteration in configure() using new
RecursiveDirectoryIterator($this->fakeDir, FilesystemIterator::SKIP_DOTS) will
throw UnexpectedValueException when $this->fakeDir is missing or unreadable;
change configure() to first check is_dir($this->fakeDir) (or is_readable) and
either return early (skip scanning) or throw a clear, actionable exception that
references $this->fakeDir, so the failure mode matches the previous glob()
behavior and avoids confusing DI-time stack traces from
RecursiveDirectoryIterator; update any tests or callers that expect
UnknownFakeJsonException to still be thrown for missing files inside a valid
directory.
- Around line 94-100: queryIdFromFile() breaks when $this->fakeDir has a
trailing slash; normalize the stored fakeDir once in the class constructor
(e.g., trim trailing DIRECTORY_SEPARATOR and '/' from the promoted property or
remove readonly and rtrim in __construct) so later calls to queryIdFromFile()
can safely use substr($file->getPathname(), strlen($this->fakeDir) + 1) or,
better, compute the offset from strlen($this->fakeDir) and only add 1 when the
stored fakeDir does not end with a separator; ensure you use DIRECTORY_SEPARATOR
(and '/' for cross-platform) when trimming and keep the existing
str_replace(DIRECTORY_SEPARATOR, '/', $queryId) for final normalization.

In `@src/JsonHydrator.php`:
- Around line 117-119: The current check in JsonHydrator that returns null when
a configured factory can't be resolved hides misconfigurations; update the logic
so that when DbQuery::factory is non-empty but the referenced class or method
does not exist (the branch checking class_exists($factoryClass) ||
method_exists($factoryClass, $factoryMethod)), you throw a clear exception
(e.g., InvalidArgumentException or a small custom FactoryResolutionException)
instead of returning null; include the offending $factoryClass and
$factoryMethod in the exception message and reference DbQuery::factory and the
JsonHydrator::factory (or the factory-resolving method) so callers can diagnose
typos/refactors immediately.

---

Nitpick comments:
In `@composer.json`:
- Around line 106-108: The composer.json "platform" PHP constraint currently
pins the exact patch version ("php": "8.2.0"); update the value to the
minor-only constraint "8.2" (i.e., change platform -> php from "8.2.0" to "8.2")
so dependency resolution allows any 8.2.x patch release while still constraining
major/minor; locate the "platform" -> "php" entry in composer.json to make this
change.

In `@src/FakeQueryInterceptor.php`:
- Around line 113-126: readContent() currently always throws
FakeJsonNotFoundException when the fixture file is missing, which diverges from
the standard query path that uses isNullable($returnType) to return null for
nullable returns; update the code so selectPostQuery() (or readContent()) checks
the return type nullability via isNullable($returnType) and returns null when
the file is missing and the return type is nullable, otherwise keep throwing
FakeJsonNotFoundException; alternatively, if container fixtures must always
exist, add a clear docblock to readContent() and selectPostQuery() stating that
container post queries are required and will always throw when missing to
prevent future unintended changes.

In `@src/FakeQueryModule.php`:
- Line 51: The binding currently hard-codes the factory method name ('factory')
via FactoryMethod::class in FakeQueryModule (->toInstance('factory')), which
makes the convention global and undocumented; change the literal to a single
shared constant (e.g. replace 'factory' with
JsonHydrator::DEFAULT_FACTORY_METHOD or another appropriately named constant on
the hydrator/DbQuery target) and update the README/docs to document the
#[FactoryMethod] convention so the string/value lives in one place and the
convention is explicit.
🪄 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: 3bf05c88-ccf7-4d1a-ab58-8b31a8d19dcf

📥 Commits

Reviewing files that changed from the base of the PR and between e9bf110 and c1bab11.

⛔ Files ignored due to path filters (3)
  • composer.lock is excluded by !**/*.lock
  • vendor-bin/require-checker/composer.lock is excluded by !**/*.lock
  • vendor-bin/tools/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • .github/workflows/ci.yml
  • .gitignore
  • .planning/findings.md
  • .planning/progress.md
  • .planning/task_plan.md
  • README.md
  • composer.json
  • src/FakeQueryInterceptor.php
  • src/FakeQueryModule.php
  • src/JsonHydrator.php
  • tests/Fake/Entity/FactoryTodoEntity.php
  • tests/Fake/Factory/InjectedTodoFactory.php
  • tests/Fake/Factory/StaticTodoFactory.php
  • tests/Fake/Query/FactoryTodoQueryInterface.php
  • tests/Fake/Query/TodoSelectionQueryInterface.php
  • tests/Fake/Result/TodoSelection.php
  • tests/Fake/factory_injected_item.json
  • tests/Fake/factory_static_item.json
  • tests/Fake/factory_static_list.jsonl
  • tests/Fake/nested/factory_static_item.json
  • tests/FakeQueryModuleTest.php
  • tests/FakeUnknownNested/nested/stray_query.json

Comment thread .planning/progress.md
Comment thread .planning/task_plan.md
Comment thread src/FakeQueryInterceptor.php Outdated
Comment thread src/FakeQueryModule.php Outdated
Comment thread src/FakeQueryModule.php Outdated
Comment thread src/JsonHydrator.php Outdated
@koriym

koriym commented May 15, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Follow-up fix added after downstream use in BEAR.AppKata exposed the important case: installing FakeQueryModule as an override while the application already has MediaQueryModule.

Root cause: Ray.Aop keys annotated pointcuts by annotation class, so the existing #[DbQuery] MediaQuery pointcut could shadow the FakeQuery pointcut.

Fix: FakeQuery now binds a priority interceptor using a non-deduplicated annotated matcher, so fake fixtures win in override contexts and the SQL interceptor is never reached.

Regression coverage added in Ray.FakeQuery: a module with both MediaQueryModule and FakeQueryModule plus a throwing SqlQueryInterface; the query must be served from JSON fixtures.

Re-verified:

  • Ray.FakeQuery CI: PHP 8.2, 8.3, 8.4, 8.5 and Quality all pass.
  • BEAR.AppKata#2 now uses direct install(new FakeQueryModule(...)) again, no nested injector workaround, and all checks pass.

@coderabbitai

@koriym

koriym commented May 15, 2026

Copy link
Copy Markdown
Member Author

Design correction: FakeQuery now replaces the Ray\MediaQuery\DbQueryInterceptor binding with FakeQueryInterceptor instead of trying to win annotated pointcut priority. This keeps existing MediaQuery #[DbQuery] pointcuts installed while DI resolves them to the fake interceptor. A regression test covers MediaQueryModule + throwing SqlQueryInterface + FakeQueryModule override. CI is passing for Quality and Unit PHP 8.2-8.5.

@koriym

koriym commented May 15, 2026

Copy link
Copy Markdown
Member Author

PR #2 レビュー: Harden FakeQuery release baseline

概要

リリース前のベースライン強化 PR。CI(PHP 8.2–8.5)、composer.json メタデータ整備、#[DbQuery(factory:)] 経由のファクトリ・ハイドレーション、ネストしたクエリID(nested/foo)対応、コンストラクタベースの PostQueryInterface 結果ラッパ対応など、機能追加と再設計が混在。


良い点

  • DbQueryInterceptor の alias 差し替え戦略 (FakeQueryModule.php:62-66)
    $this->bindInterceptor(..., [DbQueryInterceptor::class]);
    $this->bind(DbQueryInterceptor::class)->to(FakeQueryInterceptor::class)->in(Scope::SINGLETON);
    MediaQueryModuleoverride するだけで Fake 側が勝つ設計は良い。テスト testFakeQueryOverridesExistingMediaQueryInterceptors で動作検証されている。
  • ネストID対応 (FakeQueryModule.php:75-101): glob から RecursiveDirectoryIterator への置換は妥当。testUnknownNestedFakeJsonFileThrows で例外パスもカバー。
  • CI matrix (.github/workflows/ci.yml): 8.2–8.5 を unit/quality に分離。quality 側だけ composer tests + crc を回すのは健全。
  • composer.json: phpdocumentor/reflection-docblockray/aop を直接依存に明示。require-checker 通過のため必須。

指摘事項

重要: JsonHydrator::factory()is_callable チェック (src/JsonHydrator.php:113-125)

\$staticFactory = [\$factoryClass, \$factoryMethod];
if (is_callable(\$staticFactory)) {
    return static fn (array \$row): mixed => \$staticFactory(...array_values(\$row));
}

PHP 8 では is_callable(['Class', 'instanceMethod'])構文的に呼び出し可能なら true を返すInjectedTodoFactory::factory() は public な非 static メソッドなので、ここで true を返し、static 呼び出しを試みて non-static method called statically の致命的エラーとなる可能性が高い。

実際に CI が緑なのか、テスト経路を要確認:

  • testInjectedFactoryHydrationInjectedTodoFactory (非 static) を経由する
  • もし通っているなら、PHP のバージョン依存挙動が紛れている可能性あり

→ 推奨は (new ReflectionMethod(\$factoryClass, \$factoryMethod))->isStatic() で明示判定。

JsonHydrator::factory() のサイレント失敗 (src/JsonHydrator.php:118-120)

if (! class_exists(\$factoryClass) || ! method_exists(\$factoryClass, \$factoryMethod)) {
    return null;
}

#[DbQuery(factory: Typo::class)] 等で誤指定すると factory が無視され通常のエンティティ・ハイドレーションへフォールバック する。InvalidFactoryException 的な専用例外を投げるべき(汎用例外禁止 → 専用ドメイン例外の方針にも沿う)。

selectPostQuery.jsonl 固定 (src/FakeQueryInterceptor.php:91-99)

private function selectPostQuery(...): PostQueryInterface
{
    \$rows = \$this->hydrator->hydrate(\$this->readJsonl(\$dbQuery), \$entityClass, false, \$dbQuery);
    ...
    return (new ReflectionClass(\$postQueryClass))->newInstance(\$rows);
}
  • 単行の selection ラッパや、.json 配列形式を持つ fixture をサポートしない。README に「constructor-based select result wrappers」と限定明記されているので機能上は意図通りだが、PostQueryInterface 実装で .json を置いた場合は FakeJsonNotFoundException になり、ユーザにとってエラーメッセージから理由が読み取りにくい。
  • Ray.MediaQuery 本家は PostQueryInterface::fromContext(PostQueryContext) 規約。FakeQuery は コンストラクタ第1引数=rows 配列 を強制(newInstance(\$rows))。TodoSelection::fromContext を実装してもこちらは無視される。README に明記しておくこと(現状の "Constructor-based" 表記は読めるが、fromContext を期待した開発者には罠)。

FakeQueryInterceptor::invoke の Reflection 連打 (src/FakeQueryInterceptor.php:57-62)

if (\$returnType instanceof ReflectionNamedType) {
    \$typeName = \$returnType->getName();
    if (class_exists(\$typeName) && is_subclass_of(\$typeName, PostQueryInterface::class)) {
        return \$this->selectPostQuery(\$typeName, \$dbQuery, \$entityClass);
    }
}

メソッド呼び出しごとに class_exists + is_subclass_of。Reflection 結果は Module 構成時にキャッシュできる(Queries::fromDir の延長で post-query クラスを事前判定)が、現状のテスト規模では実害なし。最適化は不要だが、メソッド ID → strategy のメモ化を将来検討

軽微

  • FakeQueryModule.php:97: substr(\$file->getPathname(), strlen(\$this->fakeDir) + 1)\$this->fakeDir 末尾 /rtrim で正規化してから使うとロバスト。コンストラクタで一度 trim すれば足りる。
  • JsonHydrator::__construct#[FactoryMethod] を文字列としてインジェクトしているが、bind()->annotatedWith(FactoryMethod::class)->toInstance('factory') のハードコードは MediaQuery 本家との整合を要確認。Ray.MediaQuery 側の既定値とずれていないか。
  • assert(\$file instanceof SplFileInfo) (FakeQueryModule.php:81): 不要(常に SplFileInfo)。Psalm 抑止が目的なら /** @var SplFileInfo \$file */ で十分。
  • composer.jsonplatform.php: 8.2.0 固定は CI の 8.5 ジョブで意図せずダウンレベル解決を招く可能性。matrix CI 側で --ignore-platform-req=php 不要にするためなら明示理由をコメント。

ドキュメント

  • README に factory の挙動 (#[DbQuery(factory: ...)] 使用時の JSON フィールド → 引数マッピング)が未記載。array_values(\$row)JSON のキー順序依存 で位置引数として渡されるという仕様は重要なので明記すべき(snake_case → camelCase 変換が "factory パスでは効かない" 点も)。

テストカバレッジ

ファクトリ static/injected/list/nested、PostQuery、override、nested unknown まで網羅されており良好。ただし以下は未カバー:

  • factory 指定だが クラス不在/メソッド不在 のフォールバック経路 → 上記の「サイレント失敗」を例外化すれば自然にテスト追加可能
  • PostQueryInterface 実装クラスの コンストラクタが rows を受け取らない ケースのエラーメッセージ

まとめ

設計の方向性(DbQueryInterceptor alias 差し替え、ネストID、PostQuery 対応)は良い。最優先対応推奨は is_callable の static/instance 判定。次点でファクトリ誤指定時の専用例外化。README の factory フィールドマッピング仕様の追記も合わせて。

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll look at the current implementation to review the fix properly.


🏁 Script executed:

#!/bin/bash
cat src/FakeQueryModule.php
echo "---"
cat src/FakeQueryInterceptor.php
echo "---"
# Find the regression test for MediaQuery override
rg -n "testFakeQueryOverrides\|ThrowingSql\|MediaQueryModule" tests/ -A5 -B2

Length of output: 9469


🏁 Script executed:

#!/bin/bash
cat src/JsonHydrator.php
echo "==="
rg -n "testFakeQueryOverrides\|ThrowingSql\|MediaQueryModule\|FakeQueryOverride" tests/ -A10 -B3
echo "==="
cat tests/Fake/ThrowingSqlQuery.php 2>/dev/null || echo "NOT FOUND"

Length of output: 7912


🏁 Script executed:

#!/bin/bash
# Find the test that uses ThrowingSqlQuery or covers the override scenario
rg -n "ThrowingSqlQuery\|override\|Override\|FakeQueryOverride\|MediaQuery" tests/FakeQueryModuleTest.php -A8 -B3
echo "==="
# Check for any test file that tests the override behavior
fd -e php . tests/ --exec grep -ln "ThrowingSqlQuery\|MediaQueryModule\|override"

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Show the full test file to find the override regression test
cat tests/FakeQueryModuleTest.php

Length of output: 11137


@koriym: Thank you for the follow-up — the implementation looks solid. Here's a review of the current state:


Override mechanism ✅

The final design (as implemented) is the clean alias approach:

$this->bindInterceptor(
    $this->matcher->any(),
    $this->matcher->annotatedWith(DbQuery::class),
    [DbQueryInterceptor::class],
);
$this->bind(DbQueryInterceptor::class)->to(FakeQueryInterceptor::class)->in(Scope::SINGLETON);

This is better than the intermediate "priority interceptor" approach because it doesn't race against existing pointcuts at all — Ray.Di's singleton binding resolution just redirects DbQueryInterceptor to FakeQueryInterceptor transparently. The existing MediaQueryModule pointcuts remain wired, but DI resolves them to the fake.

testFakeQueryOverridesExistingMediaQueryInterceptors properly validates this scenario end-to-end with ThrowingSqlQuery as a sentinel — if real SQL were ever reached, the test would fail hard with a RuntimeException.


Two concerns from the earlier review — already resolved ✅

is_callable static/instance ambiguity — fixed correctly via ReflectionMethod::isStatic():

$method = new ReflectionMethod($factoryClass, $factoryMethod);
if ($method->isStatic()) {
    return static fn (array $row): mixed => $method->invokeArgs(null, array_values($row));
}
$factory = $this->injector->getInstance($factoryClass);
return static fn (array $row): mixed => $method->invokeArgs($factory, array_values($row));

This correctly separates static dispatch from injected-instance dispatch without relying on PHP's context-sensitive is_callable behavior.

Factory silent failure — fixed with InvalidFactoryException:

if (! class_exists($factoryClass) || ! method_exists($factoryClass, $factoryMethod)) {
    throw new InvalidFactoryException($factoryClass, $factoryMethod);
}
if (! $method->isPublic()) {
    throw new InvalidFactoryException($factoryClass, $factoryMethod);
}

testMissingFactoryClassThrows and testMissingFactoryMethodThrows cover both paths.


Remaining open items

These are lower priority but worth tracking:

  1. selectPostQuery is .jsonl-only and ignores PostQueryInterface::fromContext() — the newInstance($rows) constructor path is intentional, but developers who implement fromContext() and place a .json file will get a FakeJsonNotFoundException with no hint about why. The README's "Constructor-based" note covers it, but a clarifying sentence about fromContext() not being used here would help.

  2. fakeDir trailing-slash robustness in queryIdFromFile()substr($file->getPathname(), strlen($this->fakeDir) + 1) is fragile if $fakeDir is passed with a trailing /. A single rtrim($this->fakeDir, '/') in the constructor would harden this.

  3. README: factory field-mapping contract — the array_values($row) positional dispatch (JSON key order → factory argument order, no snake→camelCase conversion on the factory path) is a meaningful contract worth documenting explicitly.

  4. assert($file instanceof SplFileInfo) in validateFakeFiles() — always true given RecursiveIteratorIterator; a /** @var SplFileInfo $file */ docblock would be sufficient for static analysis without the runtime assertion.

Would you like me to open issues for any of the remaining items?

🐇✨

@koriym

koriym commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai No outstanding blockers. Please resolve all open conversations and approve this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@src/Exception/InvalidFactoryException.php`:
- Line 7: InvalidFactoryException currently extends LogicException which
resolves to Ray\FakeQuery\Exception\LogicException in this namespace; change the
parent to the global SPL LogicException by prefixing it with a leading backslash
or importing it via use \LogicException so that class InvalidFactoryException
extends \LogicException (or uses a use statement) and avoids a fatal class
resolution error.

In `@src/FakeQueryConfig.php`:
- Line 21: The constructor currently sets $this->fakeDir = rtrim($fakeDir,
DIRECTORY_SEPARATOR) which only strips the platform-specific separator; change
the trimming to remove both forward and back slashes so trailing '/' or '\' are
normalized across platforms (use rtrim on $fakeDir with both slash characters,
then keep the rest of FakeQueryConfig behavior unchanged). Reference: the
$this->fakeDir assignment and use of rtrim and DIRECTORY_SEPARATOR in
FakeQueryConfig.
🪄 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: a046a39d-87ed-4982-ad43-8982ee7cd9f2

📥 Commits

Reviewing files that changed from the base of the PR and between 5d77462 and 500d29d.

📒 Files selected for processing (13)
  • .planning/progress.md
  • .planning/task_plan.md
  • DESIGN.md
  • README.md
  • src/Exception/InvalidFactoryException.php
  • src/FakeQueryConfig.php
  • src/FakeQueryModule.php
  • src/JsonHydrator.php
  • tests/Fake/Factory/MissingMethodTodoFactory.php
  • tests/Fake/Query/FactoryTodoQueryInterface.php
  • tests/Fake/factory_missing_class_item.json
  • tests/Fake/factory_missing_method_item.json
  • tests/FakeQueryModuleTest.php
✅ Files skipped from review due to trivial changes (6)
  • tests/Fake/factory_missing_method_item.json
  • tests/Fake/Factory/MissingMethodTodoFactory.php
  • tests/Fake/factory_missing_class_item.json
  • README.md
  • .planning/progress.md
  • .planning/task_plan.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/FakeQueryModule.php
  • src/JsonHydrator.php

Comment thread src/Exception/InvalidFactoryException.php
Comment thread src/FakeQueryConfig.php Outdated
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

@koriym: Understood — no outstanding blockers. Resolving all open conversations and approving the PR now. 🐇🎉


🧠 Learnings used
Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Applies to src/FakeQueryModule.php : `FakeQueryModule` must scan `interfaceDir` for interfaces with `#[DbQuery]` attribute and bind them using Ray.Di style, mirroring the behavior of `MediaQuerySqlModule`

Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Applies to src/Interceptor/FakeQueryInterceptor.php : `FakeQueryInterceptor` must map `#[DbQuery]` attributes to JSON files using the pattern `{fakeDir}/{queryId}.json`

Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Structure the package with FakeQueryModule, FakeQueryConfig, Interceptor, and Hydrator classes as defined in DESIGN.md target architecture

Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Applies to **/*.php : Namespace production code as `Ray\FakeQuery\` with autoload path `src/`, test code as `Ray\FakeQuery\` with autoload paths `tests/` and `tests/Fake/`
✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

@koriym

koriym commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai No outstanding blockers. Please resolve all open conversations and approve this PR.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

@koriym: Resolving all open conversations and approving the PR now. 🐇🎉


🧠 Learnings used
Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Applies to src/FakeQueryModule.php : `FakeQueryModule` must scan `interfaceDir` for interfaces with `#[DbQuery]` attribute and bind them using Ray.Di style, mirroring the behavior of `MediaQuerySqlModule`

Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Structure the package with FakeQueryModule, FakeQueryConfig, Interceptor, and Hydrator classes as defined in DESIGN.md target architecture

Learnt from: koriym
Repo: ray-di/Ray.FakeQuery PR: 2
File: src/FakeQueryInterceptor.php:99-105
Timestamp: 2026-06-08T11:23:12.717Z
Learning: Applies to src/FakeQueryInterceptor.php in ray-di/Ray.FakeQuery: FakeQuery intentionally implements select fixture wrappers as constructor-based `PostQueryInterface` result objects and does not call `PostQueryInterface::fromContext()`; DML `PostQueryContext` and PDO emulation are out of scope unless a separate explicit metadata fixture design is introduced.

Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Applies to src/Interceptor/FakeQueryInterceptor.php : `FakeQueryInterceptor` must map `#[DbQuery]` attributes to JSON files using the pattern `{fakeDir}/{queryId}.json`

Learnt from: CR
Repo: ray-di/Ray.FakeQuery PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T15:23:05.849Z
Learning: Applies to **/*.php : Namespace production code as `Ray\FakeQuery\` with autoload path `src/`, test code as `Ray\FakeQuery\` with autoload paths `tests/` and `tests/Fake/`
✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/FakeQueryModule.php (1)

14-16: Update dependency check for ray/input-query imports
src/FakeQueryModule.php imports Ray\InputQuery\ToArray / Ray\InputQuery\ToArrayInterface, but ray/input-query is not declared directly in composer.json; it is present in composer.lock (v1.1.0) as a transitive dependency required by ray/media-query. Optional: add ray/input-query explicitly to composer.json for clarity and to decouple from upstream dependency changes.

🤖 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 `@src/FakeQueryModule.php` around lines 14 - 16, The code imports
Ray\InputQuery\ToArray and Ray\InputQuery\ToArrayInterface but composer.json
does not declare ray/input-query (it's only a transitive dependency via
ray/media-query); update composer.json to explicitly require "ray/input-query"
(pin to a compatible version such as the lockfile's 1.1.0 or use ^1.1) so those
imports are guaranteed, then run composer update/install to refresh
composer.lock; keep references to the existing imports (ToArray,
ToArrayInterface) and ensure no other code changes are needed.
src/FakeQueryPdo.php (1)

410-421: ⚡ Quick win

Consider throwing for unsupported value types instead of returning empty string.

When quoteValueToString() encounters a value that is not scalar, null, or array (e.g., objects, resources), it silently returns an empty string at Line 417. This could hide bugs where invalid data is passed to quote().

Throwing an exception would make debugging easier and prevent silent data corruption.

🛡️ Proposed fix
     private function quoteValueToString(mixed $value): string
     {
         if (is_scalar($value) || $value === null) {
             return (string) $value;
         }
 
         if (! is_array($value)) {
-            return '';
+            throw new LogicException('Cannot quote non-scalar, non-array value for SQL.');
         }
 
         return implode(',', array_map($this->quoteValueToString(...), $value));
     }
🤖 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 `@src/FakeQueryPdo.php` around lines 410 - 421, The method quoteValueToString
currently returns an empty string for unsupported types, which hides errors;
change it to throw an InvalidArgumentException (or DomainException) when the
input is neither scalar, null, nor array, including the actual type in the
exception message to aid debugging; keep existing behavior for scalar/null (cast
to string) and array (implode with recursive calls to quoteValueToString), and
ensure callers like quote() will surface the exception instead of silently
receiving an empty string.
🤖 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 `@src/FakeQueryPdo.php`:
- Around line 145-156: fetchObjects currently calls
$this->perform(...)->fetchAll(PDO::FETCH_CLASS, $class) but ignores the $args
parameter, so classes with required constructor args won't be instantiated;
update the fetchAll call in the fetchObjects method to pass $args as the third
argument (i.e., fetchAll(PDO::FETCH_CLASS, $class, $args)) so constructor
arguments are forwarded just like fetchObject($class, $args).

---

Nitpick comments:
In `@src/FakeQueryModule.php`:
- Around line 14-16: The code imports Ray\InputQuery\ToArray and
Ray\InputQuery\ToArrayInterface but composer.json does not declare
ray/input-query (it's only a transitive dependency via ray/media-query); update
composer.json to explicitly require "ray/input-query" (pin to a compatible
version such as the lockfile's 1.1.0 or use ^1.1) so those imports are
guaranteed, then run composer update/install to refresh composer.lock; keep
references to the existing imports (ToArray, ToArrayInterface) and ensure no
other code changes are needed.

In `@src/FakeQueryPdo.php`:
- Around line 410-421: The method quoteValueToString currently returns an empty
string for unsupported types, which hides errors; change it to throw an
InvalidArgumentException (or DomainException) when the input is neither scalar,
null, nor array, including the actual type in the exception message to aid
debugging; keep existing behavior for scalar/null (cast to string) and array
(implode with recursive calls to quoteValueToString), and ensure callers like
quote() will surface the exception instead of silently receiving an empty
string.
🪄 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: 8a3e2b2c-5268-46a4-93f1-5ae8a53df58e

📥 Commits

Reviewing files that changed from the base of the PR and between 7639a89 and eaf8d88.

📒 Files selected for processing (8)
  • src/FakeQueryConfig.php
  • src/FakeQueryInterceptor.php
  • src/FakeQueryModule.php
  • src/FakeQueryPdo.php
  • tests/Fake/Query/TodoQueryInterface.php
  • tests/Fake/Result/TodoSelection.php
  • tests/FakeQueryModuleTest.php
  • tests/FakeSecondary/todo_by_status.jsonl
✅ Files skipped from review due to trivial changes (1)
  • tests/FakeSecondary/todo_by_status.jsonl
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/FakeQueryInterceptor.php
  • tests/FakeQueryModuleTest.php

Comment thread src/FakeQueryPdo.php Outdated
Comment on lines +145 to +156
public function fetchObjects(
string $statement,
array $values = [],
string $class = stdClass::class,
array $args = [],
): array {
if (! class_exists($class)) {
return [];
}

return $this->perform($statement, $values)->fetchAll(PDO::FETCH_CLASS, $class);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pass constructor arguments to fetchAll() for FETCH_CLASS mode.

The $args parameter is declared but not passed to fetchAll(). When using PDO::FETCH_CLASS, constructor arguments must be provided as the third parameter. Compare to fetchObject() at Line 135, which correctly passes $args to fetchObject($class, $args).

Entities with required constructor parameters will fail to instantiate without this fix.

🐛 Proposed fix
-        return $this->perform($statement, $values)->fetchAll(PDO::FETCH_CLASS, $class);
+        return $this->perform($statement, $values)->fetchAll(PDO::FETCH_CLASS, $class, $args);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function fetchObjects(
string $statement,
array $values = [],
string $class = stdClass::class,
array $args = [],
): array {
if (! class_exists($class)) {
return [];
}
return $this->perform($statement, $values)->fetchAll(PDO::FETCH_CLASS, $class);
}
public function fetchObjects(
string $statement,
array $values = [],
string $class = stdClass::class,
array $args = [],
): array {
if (! class_exists($class)) {
return [];
}
return $this->perform($statement, $values)->fetchAll(PDO::FETCH_CLASS, $class, $args);
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 149-149: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

🤖 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 `@src/FakeQueryPdo.php` around lines 145 - 156, fetchObjects currently calls
$this->perform(...)->fetchAll(PDO::FETCH_CLASS, $class) but ignores the $args
parameter, so classes with required constructor args won't be instantiated;
update the fetchAll call in the fetchObjects method to pass $args as the third
argument (i.e., fetchAll(PDO::FETCH_CLASS, $class, $args)) so constructor
arguments are forwarded just like fetchObject($class, $args).

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.

1 participant