Skip to content

Initial implementation of Ray.FakeQuery#1

Merged
koriym merged 9 commits into
1.xfrom
initial-implementation
Mar 2, 2026
Merged

Initial implementation of Ray.FakeQuery#1
koriym merged 9 commits into
1.xfrom
initial-implementation

Conversation

@koriym

@koriym koriym commented Mar 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Implement FakeQueryModule that scans interfaces with #[DbQuery] and binds them to FakeQueryInterceptor
  • FakeQueryInterceptor loads JSON/JSONL fixture files instead of executing SQL, with entity hydration (snake_case → camelCase)
  • Support void return (no-op), single entity (?Entity), entity list (array<Entity>), and raw array returns
  • Throw FakeJsonNotFoundException with descriptive message when fixture file is missing
  • Validate fake directory contents against interface definitions via FakeQueryConfig::assertValidFakeDir()
  • 100% code coverage with 10 tests / 24 assertions

Architecture

src/
├── FakeQueryModule.php          # Ray.Di module — scans interfaceDir, binds #[DbQuery] interfaces
├── FakeQueryConfig.php          # Value object with fake dir validation
├── Interceptor/
│   └── FakeQueryInterceptor.php # Intercepts #[DbQuery] calls, loads JSON/JSONL
├── Hydrator/
│   └── FakeJsonHydrator.php     # JSON → Entity hydration (snake_case → camelCase)
└── Exception/
    ├── FakeJsonNotFoundException.php
    ├── LogicException.php
    └── RuntimeException.php

Test plan

  • ✅ PHPUnit: 10 tests, 24 assertions, 100% coverage
  • ✅ PHPStan level max: no errors
  • ✅ Psalm: no errors
  • ✅ PHPCS: no violations

Summary by Sourcery

Introduce a Ray.Di module and interceptor for serving DbQuery interfaces from JSON/JSONL fixtures instead of a database, with type-aware hydration and validation of fake data files.

New Features:

  • Add FakeQueryModule to scan DbQuery-annotated interfaces, wire them for interception, and validate corresponding fake JSON files.
  • Add FakeQueryInterceptor to resolve DbQuery calls from JSON/JSONL fixtures with support for void, nullable, single-entity, collection, and raw array returns.
  • Introduce JsonHydrator and supporting type definitions to hydrate entities from JSON data using property and constructor mapping, including snake_case to camelCase handling.
  • Add configuration and domain-specific exceptions for missing or unknown fake JSON files.

Enhancements:

  • Relax RuntimeException to be extensible so domain-specific exceptions can inherit from it.
  • Update JSON conventions to use JSONL files for collections and document the rationale for this format.

Build:

  • Adjust composer test and coverage scripts to invoke the project-local phpunit binary instead of the global command.

Documentation:

  • Update README and design documentation to reflect JSON/JSONL fixture conventions, void handling, and JSONL usage rationale while removing outdated integration guidance.

Tests:

  • Replace the previous FakeQuery tests with a new FakeQueryModuleTest suite that covers entity and constructor hydration, JSONL collections, void commands, nullable behavior, and error conditions for missing or unknown fake files.

Summary by CodeRabbit

  • New Features

    • Collections switch to JSON Lines (jsonl); automatic hydration of entities from JSON/JSONL fixtures; fake-query module wiring; new "add" query method added.
  • Bug Fixes

    • Missing fixture handling: nullable returns now yield null; non-nullable calls raise an error.
  • Documentation

    • README and design docs updated with JSONL examples, fixture conventions, and usage guidance.
  • Tests

    • New tests for hydration, JSONL lists, missing-file scenarios, and invalid fixture directories.
  • Chores

    • Updated test runner scripts and static analysis configuration.

koriym added 6 commits March 2, 2026 12:54
- FakeQueryModule: Ray.Di module that scans interfaceDir and binds
  #[DbQuery] interfaces to JSON-backed fake implementations
- FakeQueryInterceptor: intercepts #[DbQuery] method calls, loads JSON,
  hydrates to return type; void methods are no-ops
- JsonHydrator: hydrates JSON data to entities (camelCase and snake_case
  key support, constructor and property-based hydration)
- FakeQueryConfig: value object holding fakeDir path
- FakeJsonNotFoundException: thrown when JSON fixture file not found
- FakeQueryModuleTest: 6 tests covering entity hydration, list hydration,
  explicit row type, void no-op, and missing JSON exception
- Test fixtures: TodoQueryInterface, TodoCommandInterface, TodoEntity
- JSON fixtures: todo_item.json, todo_list.json (camelCase keys)
- Fix: explicit JsonHydrator binding in FakeQueryModule
- Fix: exclude tests/tmp from PHPStan analysis
- Fix: migrate phpunit.xml.dist schema
- Support .jsonl (JSON Lines) for collection queries, .json for single rows
- Add UnknownFakeJsonException: validate fakeDir files against #[DbQuery] IDs
- Nullable return type with missing file returns null
- Fix exception hierarchy: RuntimeException is no longer final
- Fix nullable union type (TodoEntity|null) handling in isNullable()
- Extract resolveArgument() to eliminate else (coding rule compliance)
- Add domain types (Types.php): JsonRow, JsonRowList, DbQueryIdSet
- Remove skeleton FakeQuery class and test
- Fix composer scripts to use ./vendor/bin/phpunit
Add UserEntity with constructor hydration and UserQueryInterface with
union type nullable return. Test camelCase key matching and optional
parameter defaults in resolveArgument(). Mark unreachable union type
branch in isNullable() and defensive edge cases with codeCoverageIgnore.
@sourcery-ai

sourcery-ai Bot commented Mar 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces the initial Ray.FakeQuery implementation that wires Ray.Di DbQuery interfaces to a fake JSON/JSONL-backed interceptor, adds JSON hydrator and configuration, validates fixture files against query IDs, updates docs and composer scripts, loosens RuntimeException inheritance, and replaces old FakeQuery entrypoint/tests with a module-based design and comprehensive PHPUnit coverage.

Sequence diagram for FakeQueryInterceptor handling a DbQuery method call

sequenceDiagram
    actor Client
    participant DbQueryInterface
    participant FakeQueryInterceptor
    participant FakeQueryConfig
    participant Filesystem
    participant ReturnEntity
    participant JsonHydrator

    Client->>DbQueryInterface: callMethod()
    DbQueryInterface->>FakeQueryInterceptor: invoke(invocation)

    FakeQueryInterceptor->>FakeQueryInterceptor: getMethod() and DbQuery annotation
    FakeQueryInterceptor->>FakeQueryInterceptor: inspect return type
    alt return type is void
        FakeQueryInterceptor-->>DbQueryInterface: return null
        DbQueryInterface-->>Client: null
    else non void
        FakeQueryInterceptor->>FakeQueryConfig: read fakeDir
        FakeQueryInterceptor->>FakeQueryInterceptor: decide extension .json or .jsonl
        FakeQueryInterceptor->>Filesystem: file_exists(fakeDir/id.ext)
        alt file missing and nullable
            FakeQueryInterceptor-->>DbQueryInterface: return null
            DbQueryInterface-->>Client: null
        else file missing and not nullable
            FakeQueryInterceptor-->>Client: throw FakeJsonNotFoundException
        else file exists
            FakeQueryInterceptor->>Filesystem: file_get_contents(path)
            Filesystem-->>FakeQueryInterceptor: content
            alt row (single entity or scalar)
                FakeQueryInterceptor->>FakeQueryInterceptor: json_decode(content)
                FakeQueryInterceptor->>ReturnEntity: __invoke(method)
                ReturnEntity-->>FakeQueryInterceptor: entityClass or null
                FakeQueryInterceptor->>JsonHydrator: hydrate(data, entityClass, true)
            else row list (collection)
                FakeQueryInterceptor->>FakeQueryInterceptor: parseJsonl(content)
                FakeQueryInterceptor->>ReturnEntity: __invoke(method)
                ReturnEntity-->>FakeQueryInterceptor: entityClass
                FakeQueryInterceptor->>JsonHydrator: hydrate(dataList, entityClass, false)
            end
            JsonHydrator-->>FakeQueryInterceptor: hydratedResult
            FakeQueryInterceptor-->>DbQueryInterface: hydratedResult
            DbQueryInterface-->>Client: hydratedResult
        end
    end
Loading

Class diagram for initial Ray.FakeQuery module-based design

classDiagram
    direction LR

    class FakeQueryModule {
        -string fakeDir
        -string interfaceDir
        +__construct(string fakeDir, string interfaceDir, AbstractModule module)
        +configure() void
        -validateFakeFiles(array classes) void
        -collectDbQueryIds(array classes) array
    }

    class FakeQueryConfig {
        +string fakeDir
        +__construct(string fakeDir)
    }

    class JsonHydrator {
        +hydrate(mixed data, string entityClass, bool isRow) mixed
        -hydrateRow(mixed data, string entityClass) mixed
        -hydrateRowList(mixed data, string entityClass) mixed
        -hydrateOne(array row, string entityClass) object
        -hydrateProperties(ReflectionClass refClass, array row) object
        -hydrateConstructor(ReflectionClass refClass, array row) object
        -resolveArgument(ReflectionParameter param, array row) mixed
    }

    class FakeQueryInterceptor {
        -FakeQueryConfig config
        -JsonHydrator hydrator
        -ReturnEntityInterface returnEntity
        +__construct(FakeQueryConfig config, JsonHydrator hydrator, ReturnEntityInterface returnEntity)
        +invoke(MethodInvocation invocation) mixed
        -parseJsonl(string content) array
        -isNullable(ReflectionType returnType) bool
    }

    class Types {
        <<final>>
        %% @psalm-type JsonRow = array
        %% @psalm-type JsonRowList = list
        %% @psalm-type DbQueryIdSet = array
    }

    class RuntimeException {
    }

    class FakeJsonNotFoundException {
        +__construct(string filename, string fakeDir)
    }

    class UnknownFakeJsonException {
        +__construct(string filename, string fakeDir)
    }

    class AbstractModule {
        <<external>>
        +configure() void
    }

    class MethodInterceptor {
        <<external>>
        +invoke(MethodInvocation invocation) mixed
    }

    class MethodInvocation {
        <<external>>
        +getMethod() ReflectionMethod
    }

    class ReturnEntityInterface {
        <<external>>
        +__invoke(ReflectionMethod method) string
    }

    class ReturnEntity {
        <<external>>
    }

    class Queries {
        <<external>>
        +fromDir(string interfaceDir) Queries
        +classes list~class-string~
    }

    class DbQuery {
        <<external annotation>>
        +string id
        +string type
    }

    class DocBlockFactoryInterface {
        <<external>>
    }

    class DocBlockFactory {
        <<external>>
        +createInstance() DocBlockFactoryInterface
    }

    %% Inheritance and implementation
    FakeQueryModule --|> AbstractModule
    FakeQueryInterceptor ..|> MethodInterceptor
    FakeJsonNotFoundException --|> RuntimeException
    UnknownFakeJsonException --|> RuntimeException

    ReturnEntity ..|> ReturnEntityInterface

    %% Composition and usage
    FakeQueryModule --> FakeQueryConfig
    FakeQueryModule --> JsonHydrator
    FakeQueryModule --> FakeQueryInterceptor
    FakeQueryModule --> DocBlockFactoryInterface
    FakeQueryModule --> ReturnEntityInterface
    FakeQueryModule ..> Queries
    FakeQueryModule ..> DbQuery
    FakeQueryModule ..> ReflectionClass

    FakeQueryInterceptor --> FakeQueryConfig
    FakeQueryInterceptor --> JsonHydrator
    FakeQueryInterceptor --> ReturnEntityInterface
    FakeQueryInterceptor ..> DbQuery

    JsonHydrator ..> ReflectionClass
    JsonHydrator ..> ReflectionParameter

    FakeJsonNotFoundException --> RuntimeException
    UnknownFakeJsonException --> RuntimeException

    FakeQueryModule ..> UnknownFakeJsonException
    FakeQueryInterceptor ..> FakeJsonNotFoundException
Loading

File-Level Changes

Change Details Files
Add FakeQueryModule to wire DbQuery interfaces to FakeQueryInterceptor and validate fake JSON files against known query IDs.
  • Introduce a Ray.Di module that binds FakeQueryConfig, JsonHydrator, DocBlockFactoryInterface, and ReturnEntityInterface dependencies.
  • Scan an interface directory via Ray\MediaQuery\Queries::fromDir, bind each discovered interface, and attach FakeQueryInterceptor to methods annotated with #[DbQuery].
  • Validate the fake JSON directory by collecting all DbQuery ids from interfaces and throwing an exception when unknown .json/.jsonl files are present.
src/FakeQueryModule.php
Implement FakeQueryInterceptor to load JSON/JSONL fixtures instead of executing SQL and delegate entity hydration.
  • Determine whether the query expects a single row or list based on DbQuery metadata and reflection of the return type, selecting .json or .jsonl accordingly.
  • Return null for void methods or nullable return types when the backing fixture is missing; otherwise throw FakeJsonNotFoundException with filename and directory context.
  • Parse JSON files via json_decode and JSON Lines collections via line-by-line decoding, then resolve entity class with ReturnEntityInterface and hydrate using JsonHydrator.
src/FakeQueryInterceptor.php
Add JsonHydrator to map decoded JSON data into entities, supporting both property and constructor-based hydration with snake_case to camelCase mapping and defaults.
  • Provide hydrate, hydrateRow, and hydrateRowList methods to handle single rows vs collections and raw array vs entity-class cases.
  • Use ReflectionClass and ReflectionParameter to either set public properties on a no-constructor entity or build constructor argument lists based on parameter names in camelCase and snake_case.
  • Leverage Ray\MediaQuery\StringCase to convert JSON keys to camelCase property names and fill in optional constructor parameters with default values when missing.
src/JsonHydrator.php
Define configuration, shared types, and domain-specific exceptions used by the fake query infrastructure.
  • Introduce FakeQueryConfig as a simple value object capturing the fake directory path.
  • Add Types class with Psalm type aliases for JsonRow, JsonRowList, and DbQueryIdSet to improve static analysis.
  • Create FakeJsonNotFoundException and UnknownFakeJsonException extending RuntimeException with descriptive error messages, and relax RuntimeException from final to allow extension.
src/FakeQueryConfig.php
src/Types.php
src/Exception/FakeJsonNotFoundException.php
src/Exception/UnknownFakeJsonException.php
src/Exception/RuntimeException.php
Add integration-style tests that exercise FakeQueryModule end-to-end with example entities, query interfaces, and fixture files, and remove the previous FakeQuery entrypoint and tests.
  • Create TodoEntity and UserEntity fixtures plus TodoQueryInterface, UserQueryInterface, and TodoCommandInterface annotated with #[DbQuery] to cover entity, collection, raw array, void, and nullable cases.
  • Add JSON and JSONL fixture files for todos and users, plus a stray file in a separate directory to verify UnknownFakeJsonException behavior.
  • Write FakeQueryModuleTest to bootstrap an Injector with FakeQueryModule, assert correct hydration for single entities and lists, validate raw array path, ensure void commands are no-ops, test nullable handling for missing files, and expect appropriate exceptions for non-nullable and unknown fake JSON files.
  • Delete the old FakeQuery class and its corresponding test, switching to the module-based API.
tests/FakeQueryModuleTest.php
tests/Fake/Entity/TodoEntity.php
tests/Fake/Entity/UserEntity.php
tests/Fake/Query/TodoQueryInterface.php
tests/Fake/Query/UserQueryInterface.php
tests/Fake/Query/TodoCommandInterface.php
tests/Fake/todo_item.json
tests/Fake/todo_list.json
tests/Fake/todo_list.jsonl
tests/Fake/user_item.json
tests/Fake/user_list.jsonl
tests/FakeUnknown/stray_query.json
src/FakeQuery.php
tests/FakeQueryTest.php
Update documentation and tooling configuration to describe JSONL conventions and use local PHPUnit binaries.
  • Adjust README and DESIGN docs to describe new JSON vs JSONL conventions, nullable behavior, and remove Be Framework integration section.
  • Change composer.json test-related scripts to invoke the project-local vendor/bin/phpunit instead of the global phpunit binary.
  • Update phpunit.xml.dist and phpstan.neon as part of aligning tooling with the new layout and tests.
README.md
DESIGN.md
composer.json
phpunit.xml.dist
phpstan.neon

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@koriym

koriym commented Mar 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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 commented Mar 2, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2746b2 and 2d5a879.

📒 Files selected for processing (3)
  • README.md
  • src/Exception/InvalidFakeDirException.php
  • src/FakeQueryConfig.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Exception/InvalidFakeDirException.php

📝 Walkthrough

Walkthrough

Adds a fake-query system that intercepts DbQuery-annotated methods to load JSON/JSONL fixtures, hydrate results into entities, introduce config and module wiring, new exceptions, and updated tests/docs; nullable return types return null when fixtures are missing, non-nullable throw.

Changes

Cohort / File(s) Summary
Interceptor & runtime
src/FakeQueryInterceptor.php, src/FakeQueryModule.php, src/FakeQueryConfig.php, src/JsonHydrator.php
New interceptor reads .json (single) or .jsonl (collections) from configured fake dir, parses/hydrates results, handles nullable vs non-nullable missing-file behavior, and the DI module validates fixture IDs and installs the interceptor.
Exceptions
src/Exception/FakeJsonNotFoundException.php, src/Exception/UnknownFakeJsonException.php, src/Exception/InvalidFakeDirException.php, src/Exception/RuntimeException.php, src/Exception/LogicException.php
Adds specific exception types for missing/unknown fixtures and invalid fake dir; makes RuntimeException and LogicException non-final to allow subclassing.
Types & removal
src/Types.php, src/FakeQuery.php
Adds Psalm type aliases container; removes empty placeholder src/FakeQuery.php.
Interfaces & test entities
tests/Fake/Query/TodoQueryInterface.php, tests/Fake/Query/TodoCommandInterface.php, tests/Fake/Query/UserQueryInterface.php, tests/Fake/Entity/TodoEntity.php, tests/Fake/Entity/UserEntity.php
Introduces test query/command interfaces (including new add(string $todoId, string $title): void command) with #[DbQuery] attributes and test entity classes (property-based and constructor-promoted).
Fixtures
tests/Fake/todo_item.json, tests/Fake/todo_list.json, tests/Fake/todo_list.jsonl, tests/Fake/user_item.json, tests/Fake/user_list.jsonl, tests/FakeUnknown/stray_query.json
Adds JSON and JSONL test fixtures; includes both single-object .json and per-entity .jsonl collection files; adds stray unknown fixture for validation tests.
Tests
tests/FakeQueryModuleTest.php, tests/FakeQueryTest.php
Adds comprehensive FakeQueryModuleTest exercising hydration, lists, nullable/missing-file behavior, invalid dirs, and unknown fixtures; removes obsolete FakeQueryTest.
Config & docs
composer.json, phpunit.xml.dist, phpstan.neon, DESIGN.md, README.md
Updates composer test scripts to vendor phpunit, modernizes phpunit config (schema/cache/source), excludes tests/tmp/* from phpstan, and documents JSONL collections and missing-file behavior changes.
Test scaffolding
tests/... (new/updated test data and interfaces)
Adds test scaffolding and fixtures to validate new formats, hydration, and error cases.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Interceptor as FakeQueryInterceptor
    participant Config as FakeQueryConfig
    participant FS as Filesystem
    participant Hydrator as JsonHydrator
    participant ReturnEntity as ReturnEntityInterface

    Client->>Interceptor: invoke(method)
    Interceptor->>Interceptor: read DbQuery attribute, inspect return type
    Interceptor->>Config: get fakeDir
    Interceptor->>FS: read {fakeDir}/{queryId}.{json|jsonl}
    alt file exists
        FS-->>Interceptor: content
        Interceptor->>Interceptor: parse (.json -> object, .jsonl -> lines)
        Interceptor->>ReturnEntity: determine entity class/type
        Interceptor->>Hydrator: hydrate(data, entityClass, isRow)
        Hydrator-->>Client: entity or list
    else file missing & nullable
        Interceptor-->>Client: return null
    else file missing & non-nullable
        Interceptor-->>Client: throw FakeJsonNotFoundException
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through fixtures, file by file,

JSON or JSONL, each record in style.
Interceptors hum, hydrators compose,
Missing files whisper null — or an error that grows.
Tests cheer softly as fake queries compile.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Initial implementation of Ray.FakeQuery' accurately summarizes the main purpose of this PR, which introduces the foundational FakeQueryModule, interceptor, hydrator, and supporting infrastructure for a fake query system.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch initial-implementation

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.

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues, and left some high level feedback:

  • In FakeQueryModule::validateFakeFiles, a failed glob (e.g., unreadable or non-existent directory) is treated the same as "no files", which can hide misconfiguration; consider detecting this case and surfacing a configuration error instead of silently skipping validation.
  • In FakeQueryInterceptor::parseJsonl, json_decode results are used without checking for decode errors, so malformed JSONL lines will yield null rows that then flow into hydration; you may want to validate each line (e.g., via json_last_error() or strict decoding) and throw a dedicated exception with filename and line number.
  • In JsonHydrator::resolveArgument, falling back to null for missing non-optional constructor parameters can lead to a less clear TypeError when instantiating entities; consider throwing a domain-specific exception when a required parameter has no matching JSON key instead of silently passing null.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `FakeQueryModule::validateFakeFiles`, a failed `glob` (e.g., unreadable or non-existent directory) is treated the same as "no files", which can hide misconfiguration; consider detecting this case and surfacing a configuration error instead of silently skipping validation.
- In `FakeQueryInterceptor::parseJsonl`, `json_decode` results are used without checking for decode errors, so malformed JSONL lines will yield `null` rows that then flow into hydration; you may want to validate each line (e.g., via `json_last_error()` or strict decoding) and throw a dedicated exception with filename and line number.
- In `JsonHydrator::resolveArgument`, falling back to `null` for missing non-optional constructor parameters can lead to a less clear `TypeError` when instantiating entities; consider throwing a domain-specific exception when a required parameter has no matching JSON key instead of silently passing `null`.

## Individual Comments

### Comment 1
<location path="src/FakeQueryInterceptor.php" line_range="65" />
<code_context>
+            throw new FakeJsonNotFoundException($dbQuery->id . $ext, $this->config->fakeDir);
+        }
+
+        $content = (string) file_get_contents($jsonFile);
+        /** @psalm-suppress MixedAssignment */
+        $data = $isRow
</code_context>
<issue_to_address>
**issue (bug_risk):** Casting `file_get_contents` result to string hides read failures and can lead to confusing downstream behavior.

`file_get_contents` returns `false` on failure, which becomes `''` when cast to `string`. That makes an unreadable file indistinguishable from a legitimately empty file, and then `json_decode('')` / `parseJsonl('')` silently yields `null`/empty data. Please check explicitly for `false` and either throw a dedicated exception or reuse `FakeJsonNotFoundException` (or similar) so IO failures are not masked as empty content.
</issue_to_address>

### Comment 2
<location path="src/FakeQueryInterceptor.php" line_range="67-69" />
<code_context>
+
+        $content = (string) file_get_contents($jsonFile);
+        /** @psalm-suppress MixedAssignment */
+        $data = $isRow
+            ? json_decode($content, true)
+            : $this->parseJsonl($content);
+
+        $entityClass = ($this->returnEntity)($method);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Lack of JSON error handling means malformed JSON will fail late and non-obviously.

Both the row and JSONL paths call `json_decode(..., true)` without checking for failures. On malformed input this returns `null`, which then either triggers `JsonHydrator`’s `assert(is_array($data))` or produces `null` entries in the JSONL list. Consider using `JSON_THROW_ON_ERROR` and mapping the exception to a domain-specific error, or checking `json_last_error()` / `json_last_error_msg()` and throwing a clear "invalid fixture JSON" exception at this point.

Suggested implementation:

```
        /** @psalm-suppress MixedAssignment */
        $data = $isRow
            ? json_decode($content, true, 512, JSON_THROW_ON_ERROR)
            : $this->parseJsonl($content);

```

To fully implement the suggestion consistently across both row and JSONL paths, you should also:
1. Update all `json_decode` calls inside `parseJsonl()` to use `JSON_THROW_ON_ERROR`, for example:
   - Change `json_decode($line, true)` to `json_decode($line, true, 512, JSON_THROW_ON_ERROR)`.
2. Optionally, if you want a domain-specific error:
   - Wrap the call site above in a `try/catch (\JsonException $e)` block and rethrow a custom exception (e.g. `FakeInvalidJsonFixtureException`) with a clear message including `$jsonFile`.
   - Implement that custom exception class (likely under `src/Exception/` or wherever your other domain exceptions live).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/FakeQueryInterceptor.php Outdated
Comment thread src/FakeQueryInterceptor.php

@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: 5

🧹 Nitpick comments (2)
phpunit.xml.dist (1)

3-7: Optional: Restrict discovery to test files only.

If helper/fixture PHP files exist under tests/, adding a suffix filter avoids accidental test pickup.

Suggested tweak
-      <directory>tests</directory>
+      <directory suffix="Test.php">tests</directory>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@phpunit.xml.dist` around lines 3 - 7, Restrict PHPUnit test discovery by
adding a suffix filter on the <directory> element so only test files are picked
up (e.g., set the directory's suffix to "Test.php" or another project
convention) inside the existing <testsuite name="Ray.FakeQuery test suite">
block; update the <directory> element in phpunit.xml.dist to include the suffix
attribute to avoid picking up helper/fixture files under tests/.
README.md (1)

70-72: Align the later mapping explanation with the new JSONL convention.

Since Line 71 introduces .jsonl for collections, update the “How It Works” mapping text (Line 97) so it doesn’t imply .json is always used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 70 - 72, Update the “How It Works” mapping text so it
reflects the new JSONL convention: change any wording that implies collections
use “.json” to state that single-entity files use “.json” (e.g., todo_item.json)
while collections use “.jsonl” (e.g., todo_list.jsonl), and adjust examples and
phrasing in the mapping paragraph to mention both “.json” and “.jsonl” as
appropriate rather than always “.json”.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DESIGN.md`:
- Around line 144-147: Update the earlier flow/behavior sections to match the
nullable-file rule described in the storage format block: for "Single entity
(`?Entity`): `{queryId}.json`", "Collection (`array<Entity>`):
`{queryId}.jsonl`", "Nullable" and "void methods" semantics, change any
statements that say missing files always throw to instead state that missing
files return null for nullable return types (and only throw for non-nullable
returns), and clarify that void methods require no file; ensure the wording in
those earlier sections mirrors the exact behavior described in the storage
format lines so implementers are not given conflicting rules.

In `@phpstan.neon`:
- Line 8: Remove the accidental "(?)" from the excludePaths pattern: locate the
excludePaths entry that currently reads with "tests/tmp (?)" and replace it with
a valid pattern such as "tests/tmp" (to exclude the directory) or "tests/tmp/*"
(to exclude all contents). Ensure the change is made in the phpstan
configuration where the excludePaths array is defined so PHPStan's fnmatch
semantics are respected.

In `@README.md`:
- Around line 76-89: The README contains JSON/JSONL code fences that include
JavaScript-style `//` comments, making the examples invalid for copy-paste; edit
the JSON and JSONL example blocks (the single-object example and the multi-line
JSONL example) to remove the `//` comment lines and instead place the
descriptive text outside the fenced blocks (e.g., a plain sentence like
"`var/fake/todo_item.json` (single JSON object):" before the ```json``` fence
and "`var/fake/todo_list.jsonl` (one JSON object per line):" before the
```jsonl``` fence) so the fenced content contains only valid JSON/JSONL.

In `@src/FakeQueryConfig.php`:
- Around line 9-12: FakeQueryConfig's constructor currently accepts a fakeDir
without validating it, so add a fail-fast check in FakeQueryConfig::__construct
(or FakeQueryModule::__construct if config isn't constructed there) that
verifies the path exists and is readable (use is_dir($fakeDir) and
is_readable($fakeDir) or equivalent) and throw an InvalidArgumentException (or
RuntimeException used elsewhere) with a clear message if the check fails; this
ensures FakeQueryInterceptor and FakeQueryModule::validateFakeFiles will never
attempt to read from a non-existent/unreadable directory.

In `@src/FakeQueryInterceptor.php`:
- Around line 65-69: The code currently casts file_get_contents to string and
calls json_decode without checking for read or parse errors; update the block
around $content/$isRow/$data so that you first check
file_get_contents($jsonFile) !== false (or use is_readable) and throw a clear
exception if reading fails, then decode using json_decode with error handling
(either JSON_THROW_ON_ERROR in a try/catch or check json_last_error() after
decode) and throw a descriptive exception if decoding fails; also update
parseJsonl($content) to validate each line's json_decode and surface parse
errors similarly so both the $isRow branch and the parseJsonl path fail fast
with clear messages.

---

Nitpick comments:
In `@phpunit.xml.dist`:
- Around line 3-7: Restrict PHPUnit test discovery by adding a suffix filter on
the <directory> element so only test files are picked up (e.g., set the
directory's suffix to "Test.php" or another project convention) inside the
existing <testsuite name="Ray.FakeQuery test suite"> block; update the
<directory> element in phpunit.xml.dist to include the suffix attribute to avoid
picking up helper/fixture files under tests/.

In `@README.md`:
- Around line 70-72: Update the “How It Works” mapping text so it reflects the
new JSONL convention: change any wording that implies collections use “.json” to
state that single-entity files use “.json” (e.g., todo_item.json) while
collections use “.jsonl” (e.g., todo_list.jsonl), and adjust examples and
phrasing in the mapping paragraph to mention both “.json” and “.jsonl” as
appropriate rather than always “.json”.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53b98c1 and c741f93.

📒 Files selected for processing (27)
  • DESIGN.md
  • README.md
  • composer.json
  • phpstan.neon
  • phpunit.xml.dist
  • src/Exception/FakeJsonNotFoundException.php
  • src/Exception/RuntimeException.php
  • src/Exception/UnknownFakeJsonException.php
  • src/FakeQuery.php
  • src/FakeQueryConfig.php
  • src/FakeQueryInterceptor.php
  • src/FakeQueryModule.php
  • src/JsonHydrator.php
  • src/Types.php
  • tests/Fake/Entity/TodoEntity.php
  • tests/Fake/Entity/UserEntity.php
  • tests/Fake/Query/TodoCommandInterface.php
  • tests/Fake/Query/TodoQueryInterface.php
  • tests/Fake/Query/UserQueryInterface.php
  • tests/Fake/todo_item.json
  • tests/Fake/todo_list.json
  • tests/Fake/todo_list.jsonl
  • tests/Fake/user_item.json
  • tests/Fake/user_list.jsonl
  • tests/FakeQueryModuleTest.php
  • tests/FakeQueryTest.php
  • tests/FakeUnknown/stray_query.json
💤 Files with no reviewable changes (2)
  • tests/FakeQueryTest.php
  • src/FakeQuery.php

Comment thread DESIGN.md
Comment thread phpstan.neon Outdated
Comment thread README.md Outdated
Comment thread src/FakeQueryConfig.php
Comment thread src/FakeQueryInterceptor.php Outdated
- FakeQueryInterceptor: check file_get_contents for false, add JSON_THROW_ON_ERROR
- FakeQueryConfig: validate fakeDir exists with InvalidFakeDirException
- phpstan.neon: fix excludePaths pattern (tests/tmp/*)
- DESIGN.md: align missing-file behavior for nullable vs non-nullable
- README.md: remove comments from JSON blocks, simplify
@koriym

koriym commented Mar 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai resolve

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
DESIGN.md (1)

90-97: ⚠️ Potential issue | 🟡 Minor

Update the flow step to reflect .json vs .jsonl selection.

The flow still implies a fixed .json load path, but the conventions now specify .jsonl for collections. Please make the flow wording match the new extension rules.

Suggested doc patch
-4. Load `{fakeDir}/{queryId}.json`
+4. Resolve fixture file by return shape:
+   - single row/entity → `{fakeDir}/{queryId}.json`
+   - collection (`array<Entity>`) → `{fakeDir}/{queryId}.jsonl`

Also applies to: 146-149

♻️ Duplicate comments (1)
src/FakeQueryConfig.php (1)

16-18: ⚠️ Potential issue | 🟡 Minor

Also validate directory readability at construction time.

Current check catches non-directories, but unreadable directories still pass and later surface as misleading fixture-not-found failures.

Proposed fail-fast check
 use function is_dir;
+use function is_readable;
@@
-        if (! is_dir($fakeDir)) {
+        if (! is_dir($fakeDir) || ! is_readable($fakeDir)) {
             throw new InvalidFakeDirException($fakeDir);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FakeQueryConfig.php` around lines 16 - 18, The constructor check that
currently only verifies is_dir($fakeDir) should also assert the directory is
readable; update the validation in FakeQueryConfig (the block that throws
InvalidFakeDirException($fakeDir)) to fail-fast when either the path is not a
directory or is not readable (use is_readable on $fakeDir) and throw
InvalidFakeDirException with the same identifying $fakeDir so unreadable
directories are rejected at construction time.
🧹 Nitpick comments (2)
src/Exception/InvalidFakeDirException.php (1)

11-11: Make the exception message more generic to avoid false diagnostics.

$fakeDir can fail validation for reasons beyond non-existence (e.g., path exists but is not a directory), so the message is slightly misleading.

Proposed message tweak
-        parent::__construct("Fake directory does not exist: {$fakeDir}");
+        parent::__construct("Invalid fake directory: {$fakeDir}");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Exception/InvalidFakeDirException.php` at line 11, The exception message
in InvalidFakeDirException is too specific ("Fake directory does not exist:
{$fakeDir}") and can mislead when other validation failures occur; update the
message passed to parent::__construct in the InvalidFakeDirException constructor
to a more generic wording such as "Invalid fake directory: {$fakeDir}" or
"Invalid fake directory provided" so it covers non-existence and other invalid
states while preserving the $fakeDir value for context.
tests/FakeQueryModuleTest.php (1)

112-120: Optional cleanup: extract repeated injector wiring into a helper.

The repeated anonymous module setup appears in several tests; consolidating it into a small factory/helper will make failure-focused tests easier to read and maintain.

Also applies to: 132-140, 155-163, 183-191

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/FakeQueryModuleTest.php` around lines 112 - 120, Extract the repeated
anonymous Injector wiring into a helper method (e.g., createInjector or
makeInjector) on the test class so tests reuse it instead of duplicating the
anonymous class; the helper should accept the two path arguments and return new
Injector(new class extends AbstractModule { protected function configure(): void
{ $this->install(new FakeQueryModule($basePath, $queryPath)); } }, $tmpDir) (use
the actual parameter names you prefer), then replace the repeated blocks in
tests that construct Injector directly (the occurrences using Injector,
AbstractModule, and FakeQueryModule around lines 112-120, 132-140, 155-163,
183-191) with calls to that helper for clearer, DRY test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 5-8: The fenced code block containing the lines "Ray.MediaQuery  
SQL files  → DB execution → Entity" and "Ray.FakeQuery    JSON files → hydration
→ Entity (same interface)" lacks a language identifier and triggers markdownlint
MD040; update that opening fence from ``` to include a language identifier
(e.g., use ```text) so the block is explicitly labeled and the linter passes,
keeping the block content unchanged.

---

Duplicate comments:
In `@src/FakeQueryConfig.php`:
- Around line 16-18: The constructor check that currently only verifies
is_dir($fakeDir) should also assert the directory is readable; update the
validation in FakeQueryConfig (the block that throws
InvalidFakeDirException($fakeDir)) to fail-fast when either the path is not a
directory or is not readable (use is_readable on $fakeDir) and throw
InvalidFakeDirException with the same identifying $fakeDir so unreadable
directories are rejected at construction time.

---

Nitpick comments:
In `@src/Exception/InvalidFakeDirException.php`:
- Line 11: The exception message in InvalidFakeDirException is too specific
("Fake directory does not exist: {$fakeDir}") and can mislead when other
validation failures occur; update the message passed to parent::__construct in
the InvalidFakeDirException constructor to a more generic wording such as
"Invalid fake directory: {$fakeDir}" or "Invalid fake directory provided" so it
covers non-existence and other invalid states while preserving the $fakeDir
value for context.

In `@tests/FakeQueryModuleTest.php`:
- Around line 112-120: Extract the repeated anonymous Injector wiring into a
helper method (e.g., createInjector or makeInjector) on the test class so tests
reuse it instead of duplicating the anonymous class; the helper should accept
the two path arguments and return new Injector(new class extends AbstractModule
{ protected function configure(): void { $this->install(new
FakeQueryModule($basePath, $queryPath)); } }, $tmpDir) (use the actual parameter
names you prefer), then replace the repeated blocks in tests that construct
Injector directly (the occurrences using Injector, AbstractModule, and
FakeQueryModule around lines 112-120, 132-140, 155-163, 183-191) with calls to
that helper for clearer, DRY test setup.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c741f93 and a2746b2.

📒 Files selected for processing (9)
  • DESIGN.md
  • README.md
  • phpstan.neon
  • src/Exception/InvalidFakeDirException.php
  • src/Exception/LogicException.php
  • src/FakeQueryConfig.php
  • src/FakeQueryInterceptor.php
  • tests/FakeEmpty/.gitkeep
  • tests/FakeQueryModuleTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • phpstan.neon

Comment thread README.md Outdated
@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Comments resolved. Auto-approval is disabled; enable reviews.request_changes_workflow to approve automatically.

- FakeQueryConfig: also check is_readable($fakeDir)
- InvalidFakeDirException: generalize message for non-existence and unreadable
- README.md: add text language identifier to code fence
@koriym

koriym commented Mar 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai approve

@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Comments resolved. Auto-approval is disabled; enable reviews.request_changes_workflow to approve automatically.

@koriym koriym merged commit e3ca8a0 into 1.x Mar 2, 2026
1 check passed
@koriym koriym deleted the initial-implementation branch March 2, 2026 15:22
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