Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

on:
pull_request:
push:
branches:
- 1.x
- '*.x'
- 'codex/**'

jobs:
unit:
name: Unit PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php:
- '8.2'
- '8.3'
- '8.4'
- '8.5'

steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, pdo, dom, xml, xmlwriter
tools: composer:v2
coverage: none

- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-scripts

- name: Run PHPUnit
run: vendor/bin/phpunit

quality:
name: Quality
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
extensions: mbstring, pdo, dom, xml, xmlwriter
tools: composer:v2
coverage: none

- name: Install dependencies and tools
run: composer install --no-interaction --prefer-dist

- name: Run style, static analysis, and tests
run: composer tests

- name: Check declared dependencies
run: composer crc
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/build/
/tests/tmp/
.phpcs-cache
.phpunit.cache/
.phpunit.result.cache
phpstan-baseline.neon
psalm-baseline.xml
213 changes: 66 additions & 147 deletions .planning/findings.md
Original file line number Diff line number Diff line change
@@ -1,147 +1,66 @@
# Findings — Ray.FakeQuery 実装調査

## Ray.MediaQuery ソースから判明した事実

### DbQuery アトリビュート
```php
// Ray\MediaQuery\Annotation\DbQuery
#[Attribute(Attribute::TARGET_METHOD)]
final class DbQuery {
public function __construct(
public string $id,
public string $type = 'row_list', // 'row' | 'row_list'
public string $factory = '',
) {}
}
```
- `$id` でクエリIDを取得
- `$type` で single/list を明示できる(デフォルト row_list)

### Ray.Aop getAnnotation API
`DbQueryInterceptor` では:
```php
$dbQuery = $method->getAnnotation(DbQuery::class); // Ray\Aop\ReflectionMethod API
```
`getAttributes()` ではなく `getAnnotation()` を使う(Ray.Aop固有メソッド)。

### Interface スキャン(Queries::fromDir)
```php
// Queries::fromDir($dir) → ClassesInDirectories::list($dir)
// - PHPファイルをトークン解析してclass/interfaceを取得
// - class_exists() / interface_exists() でオートロード確認
// - Generator で class-string を yield
```
`ray/media-query` の `Queries::fromDir()` をそのまま使える。

### Interface バインディングパターン
```php
// MediaQueryBaseModule::configure()
foreach ($this->queries->classes as $class) {
$this->bind($class)->toNull(); // Null object にバインド
}
// → InterceptorがNull objectのメソッド呼び出しをインターセプト
```

### Interceptor バインディング
```php
// MediaQueryDbModule::configure()
$this->bindInterceptor(
$this->matcher->any(),
$this->matcher->annotatedWith(DbQuery::class),
[DbQueryInterceptor::class],
);
```
`any()` クラス × `#[DbQuery]` メソッド にインターセプトをバインド。

### ReturnEntity(PHPDoc解析)
```php
// ReturnEntityInterface::__invoke(ReflectionMethod) → ?class-string
// - 戻り値型がクラスなら そのFQCN を返す
// - array<Entity> (PHPDoc) なら Entity のFQCN を返す
// - void / null / raw array → null を返す
// phpdocumentor/reflection-docblock に依存(ray/media-queryが持つ)
```
`ReturnEntityInterface` は public → そのままバインドして再利用できる。

### row/row_list 判定ロジック(DbQueryInterceptor より)
```php
$isRow = $dbQuery->type === 'row'
|| $returnType instanceof ReflectionUnionType
|| ($returnType instanceof ReflectionNamedType && $returnType->getName() !== 'array');
```
FakeQuery でも同じロジックを採用する。

### エンティティ種別と PDO fetch との対応

| 条件 | PDO fetch | FakeQuery hydration |
|------|-----------|---------------------|
| entity = null(raw array) | FETCH_ASSOC | JSON data をそのまま返す |
| entity あり、__construct なし | FETCH_CLASS(プロパティ直設定) | `new $entity()` → public property を set |
| entity あり、__construct あり | FETCH_FUNC(位置引数) | Reflection で param名→JSON値 マッピング → `new $entity(...)` |

### snake_case → camelCase 変換
```php
// Ray\MediaQuery\StringCase::camel('todo_id') → 'todoId'
// ray/media-query に含まれるため再利用可能
```

### void メソッド(Command)の確認
```php
// TodoAddInterface
#[DbQuery('todo_add')]
public function __invoke(string $id, string $title): void;
// → JSON ファイル不要、no-op
```

## 依存関係

### 現在の composer.json(Ray.FakeQuery)
```json
"require": { "php": "^8.1" }
```
**追加が必要:**
- `ray/di: ^2.18`(AbstractModule, bindInterceptor等)
- `ray/media-query: ^1.0`(Queries, ReturnEntity, DbQuery, StringCase等)

transitively 取得できるもの:
- `ray/aop`(MethodInterceptor, MethodInvocation)
- `phpdocumentor/reflection-docblock`(ReturnEntity内部で使用)

### Ray.MediaQuery の PHP 要件
`ray/media-query` は `php: ^8.2` を要求している。
→ Ray.FakeQuery の `require.php` も `^8.2` に上げる必要あり。

## テスト用 Fake データ構造

DESIGN.md のテスト例:
```
tests/
├── FakeQueryModuleTest.php
└── Fake/
├── Interface/ ← interfaceDir として渡す
│ └── TodoQueryInterface.php
├── Entity/
│ └── TodoEntity.php
├── todo_item.json ← fakeDir として渡す
└── todo_list.json
```

## 未解決事項(設計判断が必要)

### Q1: エンティティ constructor 引数マッピング
`FetchNewInstance` は PDO の列順に `new $entity(...$args)` を呼ぶ。
JSON は名前付きキーなので、**constructor パラメータ名でマッピング**が必要。
- snake_case JSON キー → camelCase → コンストラクタパラメータ名 でマッピング?

### Q2: DbQuery::type の扱い
`$type = 'row'` の場合に single fetch を強制するか?
→ MediaQuery と同じロジックで yes(`$type === 'row'` → row)

### Q3: nullable の JSON ファイルが存在しない場合
`?Entity` 戻り値型で JSON ファイルが存在しない場合:
- DESIGN.md: 「Missing files: throw FakeJsonNotFoundException」
- ただし nullable なら null を返すべきか? or 明示的に `null` を JSON に書く?

### Q4: `array<Entity>` で要素が Entity かどうかのチェック
`ReturnEntity` が null を返す(PHPDoc なし or raw array)かつ戻り型が `array` なら raw array として扱う。
これは MediaQuery と同じ動作で OK?
# Findings — Ray.FakeQuery Release Hardening

## Current Package State
- Repository: `/Users/akihito/git/Ray.FakeQuery`
- Branch at start: `1.x`
- Baseline commit: `e9bf11042a41c03d879319b2c94b7d4ccdb1a8db`
- No release tags were present in the remote tag listing.
- Packagist/composer metadata exposes `1.x-dev` and no stable tag.

## Current Implementation
- `FakeQueryModule` scans query interfaces with `Ray\MediaQuery\Queries::fromDir()`.
- Query interfaces are bound to Ray.Di null objects and intercepted on
`#[DbQuery]` methods.
- `FakeQueryInterceptor` maps:
- row return paths to `<query_id>.json`
- row-list return paths to `<query_id>.jsonl`
- `void` methods to no-op.
- `JsonHydrator` supports:
- raw arrays when no entity type is resolved,
- public-property entities,
- constructor entities with camelCase / snake_case key matching.
- Current tests cover basic row, row-list, explicit `type: 'row'`, constructor
hydration, void no-op, unknown fixture files, invalid fake dir, and nullable
missing-file behavior.

## Baseline Commands
- `composer tests` passed on PHP 8.5.
- `composer coverage` passed with line coverage 96.55%.
- `composer crc` failed because currently used symbols from `phpdocumentor` and
`ray/aop` are not declared as direct dependencies.

## Documentation Drift
- `.planning` was older than the implementation.
- The old plan expected row-list `.json`; implementation and README now use
`.jsonl`.
- JSONL is the right direction for row-list fixtures because diffs are stable and
each line is a canonical row example.
- The old plan expected missing nullable fixture files to throw, while current
tests return `null` for nullable methods. For release compatibility and
Ray.MediaQuery "no row" parity, the current default remains `null`; stricter
fixture-completeness validation can be added separately.

## BEAR.AppKata / MyVendor.Cms Release Criteria
- Fake fixtures must be reusable as shared domain vocabulary, not just local
mocks.
- Select-side BDR result support matters:
- custom `PostQueryInterface` wrappers for SELECT row lists
- typed selection result objects such as `ArticleSelection`
- Pager support matters for `PagesInterface` / `#[Pager]`.
- `#[DbQuery(factory: ...)]` matters because MyVendor.Cms uses factories for
Article entity hydration.
- Nested query IDs matter because BEAR.AppKata uses ids such as
`admins/admin_selection_list`; fake file validation must recurse and preserve
query id paths.
- Parameter-aware resolver support is likely a post-1.0 extension unless the
first stable release aims to replace MyVendor.Cms' stateful `FakeSqlQuery`.

## Open Technical Questions
- Whether `AffectedRows` and `InsertedRow` belong in Ray.FakeQuery 1.0. If they
do, they should be built directly from metadata fixtures, not by creating fake
PDO objects.
- Exact fake fixture shape for `PagesInterface`.
- Whether custom SELECT wrappers should remain constructor-based only or support
`fromContext()` through a public Ray.MediaQuery helper in the future.
- Whether 1.0.0 must include a stateful resolver layer for write/read round trips
or whether static fixtures plus BDR metadata fixtures are sufficient.
95 changes: 71 additions & 24 deletions .planning/progress.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,76 @@
# Progress — Ray.FakeQuery 実装
# Progress — Ray.FakeQuery Release Hardening

## セッション: 2026-03-02
## Session: 2026-05-15

### 完了
- [x] Ray.MediaQuery ソース調査
- DbQuery アトリビュート構造(id, type, factory)
- Interceptor バインディングパターン(any() × annotatedWith(DbQuery))
- Interface スキャン(Queries::fromDir → ClassesInDirectories)
- ReturnEntity(phpdocumentor による PHPDoc 解析)
- StringCase::camel(snake → camel 変換)
- エンティティ種別判定(constructor あり / なし)
- row/row_list 判定ロジック
### Completed
- Reviewed Ray.FakeQuery public README, composer metadata, source, tests, and
`.planning` files.
- Verified the current implementation uses `.json` for row and `.jsonl` for
row-list fixtures.
- Verified `composer tests` passes locally on PHP 8.5.
- Verified `composer coverage` passes locally with 96.55% line coverage.
- Verified `composer crc` currently fails on undeclared direct dependencies.
- Created branch `codex/fake-query-release-hardening`.
- Replaced obsolete `.planning` implementation notes with a release-hardening
plan driven by BEAR.AppKata / MyVendor.Cms use cases.
- Spawned sub-agents for:
- Ray.MediaQuery 1.1 / BDR feature gap analysis.
- composer / CI / release hygiene review.
- BEAR.AppKata and MyVendor.Cms acceptance criteria extraction.
- Received sub-agent findings and incorporated the high-priority criteria:
nested query ids, factory hydration, select-side result wrappers,
pager support, and parameter-aware resolver as an extension point.
- Fixed the first release-hygiene blocker:
- direct `ray/aop` and `phpdocumentor/reflection-docblock` dependencies,
- `ray/media-query:^1.1` baseline,
- package metadata,
- GitHub Actions workflow,
- `composer crc` passing.
- Added support and tests for:
- static `#[DbQuery(factory: ...)]` hydration,
- injected factory hydration,
- factory row-list hydration,
- nested query id fixture loading,
- recursive unknown fixture validation,
- constructor-based SELECT result wrappers.
- Removed the fake PDO direction from the current scope. DML metadata results
can be added later as direct metadata fixture handling if needed.

### 確定(Phase 0 完了)
- [x] Phase 0: 設計判断の確定
- Constructor 引数: 名前ベースマッピング(snake_case→camelCase)
- nullable + ファイル不在: FakeJsonNotFoundException スロー
- PHP バージョン: ^8.2 に変更
- DbQuery::type: 尊重する(MediaQuery 互換)
### In Progress
- Implement Ray.MediaQuery 1.1 result support in priority order.

### 未着手
- [ ] Phase 1: 依存関係セットアップ(composer.json 更新)
- [ ] Phase 2: コア実装(5ファイル)
- [ ] Phase 3: テスト実装
- [ ] Phase 4: 品質チェック
## Test Results

## エラーログ
(なし)
| Date | Command | Result | Notes |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
|------|---------|--------|-------|
| 2026-05-15 | `composer tests` | pass | phpcs, phpstan, psalm, phpunit passed. |
| 2026-05-15 | `composer coverage` | pass | Line coverage 96.55%. |
| 2026-05-15 | `composer crc` | fail | Missing direct dependency declarations for phpdocumentor and ray/aop symbols. |
| 2026-05-15 | `composer validate --strict && composer crc && vendor/bin/phpunit` | pass | Composer metadata, direct dependency scan, and unit tests passed. |
| 2026-05-15 | `composer tests && composer crc && composer validate --strict` | pass | phpcs, phpstan, psalm, phpunit, CRC, and composer validation passed. |

## Files Modified
- `.planning/task_plan.md`
- `.planning/findings.md`
- `.planning/progress.md`
- `.github/workflows/ci.yml`
- `README.md`
- `composer.json`
- `composer.lock`
- `vendor-bin/require-checker/composer.lock`
- `vendor-bin/tools/composer.lock`
- `src/FakeQueryModule.php`
- `src/FakeQueryInterceptor.php`
- `src/JsonHydrator.php`
- `tests/FakeQueryModuleTest.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_static_item.json`
- `tests/Fake/factory_injected_item.json`
- `tests/Fake/factory_static_list.jsonl`
- `tests/Fake/nested/factory_static_item.json`
- `tests/FakeUnknownNested/nested/stray_query.json`
Loading
Loading