diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dd89cdb --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 99b67a0..751527f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /build/ /tests/tmp/ .phpcs-cache +.phpunit.cache/ .phpunit.result.cache phpstan-baseline.neon psalm-baseline.xml diff --git a/.planning/findings.md b/.planning/findings.md index e351ffd..51cd99e 100644 --- a/.planning/findings.md +++ b/.planning/findings.md @@ -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 (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 かどうかのチェック -`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 `.json` + - row-list return paths to `.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. diff --git a/.planning/progress.md b/.planning/progress.md index 9792142..f6f352b 100644 --- a/.planning/progress.md +++ b/.planning/progress.md @@ -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 | +|------|---------|--------|-------| +| 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` diff --git a/.planning/task_plan.md b/.planning/task_plan.md index 38e8f1b..c05ca5f 100644 --- a/.planning/task_plan.md +++ b/.planning/task_plan.md @@ -1,281 +1,124 @@ -# Task Plan — Ray.FakeQuery 実装 +# Task Plan — Ray.FakeQuery Release Hardening ## Goal -`FakeQueryModule` をインストールすると、`#[DbQuery]` アノテーション付きインターフェースのメソッドが -SQLではなくJSONファイルからデータを返すようになるパッケージを実装する。 - -## ステータス凡例 +Use the BEAR.AppKata / MyVendor.Cms modernization use cases as the release +engine for Ray.FakeQuery, then prepare a stable package release. + +Ray.FakeQuery should let tests and frontend development replace +Ray.MediaQuery SQL execution with executable fixture vocabulary: + +- `#[DbQuery]` interface remains the public contract. +- row fixtures use `.json`. +- row-list fixtures use `.jsonl`, one object per line. +- fixture names follow the Ray.MediaQuery query id. +- fake data hydrates through the same Entity / result contracts the application uses. + +## Release Judgment +- `1.x-dev` is useful today for experiments. +- A narrow `0.1.0` tag would be acceptable for current row / row-list support. +- The target for this work is **1.0.0 readiness**, because BEAR.AppKata needs + Ray.MediaQuery 1.1 result semantics, not only simple entity hydration. + +## Source Use Cases +- `/Users/akihito/git/bear-app` + - Admin read fixtures as shared domain vocabulary. + - BDR samples: typed selection result wrappers and smoke tests. +- `/Users/akihito/git/MyVendor.Cms` + - Existing `FakeSqlQuery` behavior. + - `ArticleSelection`, `PagesInterface`, and current DML fake behavior. + - MediaQuery smoke tests and fake pager examples. + +## Decisions +- JSONL is the canonical row-list fixture format. Older `.planning` references to + row-list `.json` are obsolete. +- Nullable row methods keep Ray.MediaQuery "no row" parity: a missing row fixture + returns `null`. Non-nullable rows and row lists still throw when the fixture is + absent. To make a no-row example explicit, a `.json` file containing `null` + remains valid; empty `.jsonl` files represent empty lists. +- Fake classes remain useful only for behavior outside Ray.FakeQuery scope. + Direct app-local fake query classes should shrink as Ray.FakeQuery covers the + shared query/result contract. +- Keep 1.0.0 scope select-side first. DML metadata results such as + `AffectedRows` and `InsertedRow` should not require fake PDO machinery; if + they are added, they should be built directly from explicit metadata fixtures. + +## Status Legend - [ ] pending -- [~] in_progress +- [~] in progress - [x] complete -- [!] blocked / 要確認 - ---- - -## Phase 0: 設計判断(確定済み)[x] - -1. **Constructor エンティティの引数マッピング** → **名前ベース** - - Reflection でパラメータ名を取得し、JSON キー(snake_case→camelCase変換)でマッピング - -2. **nullable + JSONファイル不在の挙動** → **FakeJsonNotFoundException をスロー** - - null を返したければ明示的に JSON ファイルに `null` と書く - -3. **PHP バージョン要件** → **`^8.2` に変更** - -4. **`DbQuery::$type` の扱い** → **尊重する(MediaQuery と同じ挙動)** - - `type === 'row'` → single fetch 強制 - ---- - -## Phase 1: 依存関係セットアップ [ ] - -### 1-1. composer.json 更新 -```json -"require": { - "php": "^8.2", - "ray/di": "^2.18", - "ray/media-query": "^1.0" -} -``` - -### 1-2. composer update -```bash -composer update -``` - -### 検証 -- `vendor/ray/media-query/src/` が存在する -- `vendor/ray/di/` が存在する - ---- - -## Phase 2: コア実装 [ ] - -### 2-1. `src/FakeQueryConfig.php` -```php -final class FakeQueryConfig { - public function __construct( - public readonly string $fakeDir, - ) {} -} -``` - -### 2-2. `src/Exception/FakeJsonNotFoundException.php` -`src/Exception/RuntimeException.php` を継承。 -```php -final class FakeJsonNotFoundException extends RuntimeException { - public function __construct(string $queryId, string $fakeDir) { - parent::__construct("Fake JSON file not found: {$queryId}.json in {$fakeDir}"); - } -} -``` - -### 2-3. `src/Hydrator/FakeJsonHydrator.php` -責務: JSON データ → エンティティオブジェクト or 配列 - -入力: -- `array|null $data` (JSONデコード済みデータ) -- `string|null $entityClass` (エンティティFQCN、nullならraw array) -- `bool $isRow` (single か list か) - -ロジック: -``` -$isRow = true: - $entityClass = null → return $data as-is (raw assoc) - $entityClass あり: - $data = null → return null - → hydrateOne($data, $entityClass) - -$isRow = false: - $entityClass = null → return $data as-is (raw assoc array) - $entityClass あり: - → array_map(fn($row) => hydrateOne($row, $entityClass), $data) - -hydrateOne($row, $entity): - method_exists($entity, '__construct') なし - → new $entity(), public property を camelCase 変換後に set - あり - → Reflection でコンストラクタパラメータを取得 - → param名(またはsnake_case)で $row からマッピング - → new $entity(...$args) -``` - -### 2-4. `src/Interceptor/FakeQueryInterceptor.php` -```php -final class FakeQueryInterceptor implements MethodInterceptor { - public function __construct( - private readonly FakeQueryConfig $config, - private readonly FakeJsonHydrator $hydrator, - private readonly ReturnEntityInterface $returnEntity, - ) {} - - public function invoke(MethodInvocation $invocation): mixed { - $method = $invocation->getMethod(); - $dbQuery = $method->getAnnotation(DbQuery::class); // Ray.Aop API - - // void → no-op - $returnType = $method->getReturnType(); - if ($returnType instanceof ReflectionNamedType && $returnType->getName() === 'void') { - return null; - } - - // row か row_list か判定(MediaQueryと同ロジック) - $isRow = $dbQuery->type === 'row' - || $returnType instanceof ReflectionUnionType - || ($returnType instanceof ReflectionNamedType && $returnType->getName() !== 'array'); - - // JSON ファイル読込 - $jsonFile = $this->config->fakeDir . '/' . $dbQuery->id . '.json'; - if (! file_exists($jsonFile)) { - throw new FakeJsonNotFoundException($dbQuery->id, $this->config->fakeDir); - } - $data = json_decode((string) file_get_contents($jsonFile), true); - - // エンティティクラス取得(PHPDoc含む) - $entityClass = ($this->returnEntity)($method); - - return $this->hydrator->hydrate($data, $entityClass, $isRow); - } -} -``` - -### 2-5. `src/FakeQueryModule.php` -```php -final class FakeQueryModule extends AbstractModule { - public function __construct( - private readonly string $fakeDir, - private readonly string $interfaceDir, - AbstractModule|null $module = null, - ) { - parent::__construct($module); - } - - protected function configure(): void { - // 1. FakeQueryConfig をバインド - $this->bind(FakeQueryConfig::class)->toInstance(new FakeQueryConfig($this->fakeDir)); - - // 2. ReturnEntityInterface をバインド(ray/media-queryから再利用) - $this->bind(DocBlockFactoryInterface::class)->toInstance(DocBlockFactory::createInstance()); - $this->bind(ReturnEntityInterface::class)->to(ReturnEntity::class); - - // 3. interfaceDir からインターフェースをスキャン、toNull() でバインド - $queries = Queries::fromDir($this->interfaceDir); - foreach ($queries->classes as $class) { - $this->bind($class)->toNull(); - } - - // 4. #[DbQuery] メソッドへインターセプトをバインド - $this->bindInterceptor( - $this->matcher->any(), - $this->matcher->annotatedWith(DbQuery::class), - [FakeQueryInterceptor::class], - ); - } -} -``` - ---- - -## Phase 3: テスト実装 [ ] - -### 3-1. テスト用 Fake データ作成 - -**`tests/Fake/Interface/TodoQueryInterface.php`** -```php -interface TodoQueryInterface { - #[DbQuery('todo_item')] - public function item(string $todoId): ?TodoEntity; - - #[DbQuery('todo_list')] - /** @return array */ - public function list(): array; -} -``` - -**`tests/Fake/Entity/TodoEntity.php`** -```php -final class TodoEntity { - public string $todoId; - public string $todoTitle; - public bool $isCompleted; -} -``` - -**`tests/Fake/todo_item.json`** -```json -{ - "todo_id": "01HVXXXXXX0008", - "todo_title": "Beフレームワーク", - "is_completed": false -} -``` - -**`tests/Fake/todo_list.json`** -```json -[ - {"todo_id": "01HVXXXXXX0008", "todo_title": "Beフレームワーク", "is_completed": false}, - {"todo_id": "01HVXXXXXX0007", "todo_title": "ALPS設計", "is_completed": true} -] -``` - -**Command 用**: -```php -interface TodoCommandInterface { - #[DbQuery('todo_add')] - public function add(string $todoId, string $title): void; -} -``` - -### 3-2. `tests/FakeQueryModuleTest.php` - -テストケース: -1. `testItemQuery` — `?Entity` 戻り値、single JSON → エンティティ取得 -2. `testListQuery` — `array` PHPDoc、JSON配列 → エンティティ配列 -3. `testCommandIsNoOp` — `void` 戻り値 → 例外なし、null返却 -4. `testMissingJsonThrows` — JSON ファイルなし → `FakeJsonNotFoundException` -5. `testRawArrayQuery` — PHPDoc なし `array` 戻り値 → raw array - ---- - -## Phase 4: 品質チェック [ ] - -```bash -composer cs-fix # コードスタイル修正 -composer phpstan # PHPStan level max -composer psalm # Psalm -composer test # PHPUnit -composer tests # 全チェック -``` - ---- - -## リスクと注意点 - -| リスク | 対策 | -|--------|------| -| `ReturnEntity` が internal 変更される | `ReturnEntityInterface` (public) を通してのみ使う | -| Constructor エンティティのパラメータ名マッピング漏れ | Reflection で getName() を使い、camelCase/snake_case 両方試みる | -| PHPStan level max で型エラー | `@param`, `@return` を丁寧に書く | -| `ray/media-query ^1.0` が `^8.2` 要求 | PHP 要件を `^8.2` に更新 | - ---- - -## 依存グラフ(実装順) - -``` -Phase 1: composer.json - ↓ -FakeQueryConfig (単純 VO) - ↓ -FakeJsonNotFoundException (単純例外) - ↓ -FakeJsonHydrator (hydration ロジック) - ↓ -FakeQueryInterceptor (FakeQueryConfig + Hydrator + ReturnEntity) - ↓ -FakeQueryModule (全体を束ねる) - ↓ -Tests - ↓ -Quality checks -``` +- [!] blocked / needs decision + +## Phase 0: Baseline Assessment [x] +- [x] Confirm current branch and HEAD. +- [x] Run current `composer tests`. +- [x] Run coverage check. +- [x] Run `composer crc` and record blocker. +- [x] Inspect current README and `.planning` drift. + +## Phase 1: Planning Synchronization [x] +- [x] Replace obsolete implementation plan with release-hardening plan. +- [x] Record JSONL as the canonical list fixture format. +- [x] Record BEAR.AppKata / MyVendor.Cms as release acceptance sources. +- [x] Incorporate sub-agent findings into this plan. + +## Phase 2: Release Hygiene [~] +- [x] Fix direct dependency declarations so `composer crc` passes. +- [x] Add package description, keywords, support metadata, and CI badges only if + they reflect real checks. +- [x] Add GitHub Actions for PHP 8.2, 8.3, 8.4, and 8.5 where available. +- [ ] Run highest and lowest dependency test jobs, or document why lowest is not + currently practical. +- [x] Update README release scope and semantics. + +## Phase 3: Fixture Contract And Diagnostics [ ] +- [x] Support nested query ids by loading and validating subdirectory fixtures. +- [ ] Keep nullable missing row fixtures returning `null`; add an optional strict + validation path only if callers need every query id to be materialized. +- [ ] Make missing non-nullable row and row-list fixture files throw a clear exception. +- [ ] Keep void `#[DbQuery]` methods as no-op without a fixture file. +- [ ] Support explicit `null` in row `.json` fixtures for `?Entity` misses. +- [ ] Keep empty `.jsonl` valid for empty row-list results. +- [ ] Add invalid JSON / invalid JSONL diagnostics. +- [ ] Replace `assert()`-dependent runtime validation with explicit exceptions + for fixture shape and hydration errors. + +## Phase 4: Ray.MediaQuery 1.1 Select Result Support [~] +- [x] Add `#[DbQuery(factory: ...)]` hydration parity for static and injected + factory classes. +- [x] Add support for constructor-based custom `PostQueryInterface` wrappers + over hydrated SELECT rows, such as MyVendor.Cms `ArticleSelection`. +- [ ] Preserve `#[DbQuery(factory: ...)]` semantics for row and row-list + hydration where possible. +- [ ] Add tests that mirror BEAR.AppKata / MyVendor.Cms BDR samples. +- [ ] Keep DML metadata (`AffectedRows`, `InsertedRow`) as a separate optional + phase; do not introduce fake PDO just to satisfy `PostQueryContext`. + +## Phase 5: Pager Support [ ] +- [ ] Add fake `PagesInterface` support for methods annotated with `#[Pager]`. +- [ ] Decide fixture shape for paged row lists without making DB dumps. +- [ ] Add tests for count, page access, per-page argument, and empty pages. +- [ ] Compare behavior against MyVendor.Cms `FakePages`. + +## Phase 6: Reference Integration Checks [ ] +- [ ] Try Ray.FakeQuery in a BEAR.AppKata hermetic test context for Admin read + fixtures. +- [ ] Compare with MyVendor.Cms fake query smoke expectations. +- [ ] Record any missing upstream behavior as Ray.FakeQuery issues or local + narrow adapters. +- [ ] Keep app fixtures as domain vocabulary, not raw mock assertions. + +## Phase 7: Release Preparation [ ] +- [ ] Ensure `composer tests`, `composer coverage`, and `composer crc` pass. +- [ ] Ensure GitHub Actions are green. +- [ ] Update CHANGELOG or release notes. +- [ ] Decide tag: + - `0.1.0` if only simple row / row-list support is guaranteed. + - `1.0.0` if Phases 2-6 pass against BEAR.AppKata / MyVendor.Cms use cases. +- [ ] Create release PR. + +## Errors Encountered + +| Error | Attempt | Resolution | +|-------|---------|------------| +| `composer crc` reports `phpDocumentor\Reflection\DocBlockFactory*` and `Ray\Aop\Method*` as unknown symbols | Baseline release check | Add direct dependencies or adjust code so the package declares what it uses. | +| Existing fake file validation only scanned top-level fixtures | BEAR.AppKata query ids use `admins/...` paths | Replaced top-level glob with recursive validation preserving `/` query ids. | diff --git a/DESIGN.md b/DESIGN.md index 33c2b97..45a4413 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,220 +1,85 @@ -# Ray.FakeQuery — Design Document for Implementation +# Ray.FakeQuery Design Notes -## Goal +Ray.FakeQuery is a fixture adapter for Ray.MediaQuery query interfaces. It keeps +the application-facing query interface unchanged while replacing SQL execution +with JSON or JSONL fixture data. -Implement `Ray.FakeQuery` as a companion package to `Ray.MediaQuery`. +## Scope -When `FakeQueryModule` is installed instead of `MediaQuerySqlModule`, all `#[DbQuery]` annotated interface methods return data from JSON fixture files instead of executing SQL. +The 1.0 scope is intentionally limited to select-style fake responses: -## Reference +- row and nullable row responses from `.json` +- row list responses from `.jsonl` +- `void` command methods as no-op methods +- constructor-based `PostQueryInterface` result wrappers +- static and injected factory hydration via `#[DbQuery(factory: ...)]` -- Ray.MediaQuery source: `/Users/akihito/git/Ray.MediaQuery` -- BEAR.FakeJson (same concept for BEAR.Sunday resources): `https://github.com/bearsunday/BEAR.FakeJson` -- README: `README.md` in this repo +DML metadata result objects such as `AffectedRows` and `InsertedRow` are tracked +separately for a later release. They should remain fixture-driven and must not +turn FakeQuery into a fake PDO or mutable in-memory database. -## Package Info +## Module Replacement -```json -{ - "name": "ray/fake-query", - "description": "Replace Ray.MediaQuery SQL execution with JSON fixtures", - "require": { - "php": "^8.1", - "ray/di": "^2.18", - "ray/media-query": "^1.0" - } -} -``` - -## Directory Structure - -``` -src/ -├── FakeQueryModule.php -├── FakeQueryConfig.php -├── Interceptor/ -│ └── FakeQueryInterceptor.php -└── Hydrator/ - └── FakeJsonHydrator.php -tests/ -├── FakeQueryModuleTest.php -└── Fake/ - ├── todo_item.json - └── todo_list.json -var/fake/ (convention, not in src) -``` - -## Core Classes - -### FakeQueryConfig - -Simple value object holding configuration: - -```php -final class FakeQueryConfig -{ - public function __construct( - public readonly string $fakeDir, // path to JSON fixture directory - ) {} -} -``` - -### FakeQueryModule - -Ray.Di module. Binds all interfaces that have `#[DbQuery]` methods to a proxy that intercepts calls and returns JSON data. - -Should work similarly to how `MediaQuerySqlModule` binds interfaces — scan the interface directory, find all interfaces with `#[DbQuery]` methods, and bind them to fake implementations. - -```php -final class FakeQueryModule extends AbstractModule -{ - public function __construct( - private readonly string $fakeDir, - private readonly string $interfaceDir, // same as MediaQuerySqlModule - ) {} - - protected function configure(): void - { - // bind FakeQueryConfig - // scan interfaceDir for interfaces with #[DbQuery] methods - // for each interface, bind to a generated fake implementation - // the fake implementation uses FakeQueryInterceptor - } -} -``` - -### FakeQueryInterceptor +`FakeQueryModule` binds all Ray.MediaQuery query interfaces from the configured +interface directory to null-object proxies, the same proxy surface used by +Ray.MediaQuery. -Intercepts method calls on `#[DbQuery]` annotated methods: - -1. Get the query ID from `#[DbQuery('query_id')]` -2. Determine return type from method signature -3. If return type is `void` → do nothing, return null -4. Load `{fakeDir}/{queryId}.json` -5. If file not found: - - nullable return type → return `null` - - non-nullable return type → throw `FakeJsonNotFoundException` -6. Hydrate JSON to return type and return +For override use, FakeQuery does not compete with MediaQuery pointcuts by +priority. It binds the MediaQuery interceptor token itself: ```php -final class FakeQueryInterceptor implements MethodInterceptor -{ - public function __construct( - private readonly FakeQueryConfig $config, - private readonly FakeJsonHydrator $hydrator, - ) {} - - public function invoke(MethodInvocation $invocation): mixed - { - $method = $invocation->getMethod(); - $dbQuery = $method->getAttributes(DbQuery::class)[0]->newInstance(); - $queryId = $dbQuery->id; // check actual property name in Ray.MediaQuery - - $returnType = $method->getReturnType(); - - // void → no-op - if ($returnType instanceof \ReflectionNamedType && $returnType->getName() === 'void') { - return null; - } - - $jsonFile = $this->config->fakeDir . '/' . $queryId . '.json'; - - if (! file_exists($jsonFile)) { - throw new FakeJsonNotFoundException($queryId, $this->config->fakeDir); - } - - $data = json_decode(file_get_contents($jsonFile), true); - - return $this->hydrator->hydrate($data, $method); - } -} +$this->bindInterceptor( + $this->matcher->any(), + $this->matcher->annotatedWith(DbQuery::class), + [DbQueryInterceptor::class], +); +$this->bind(DbQueryInterceptor::class)->to(FakeQueryInterceptor::class); ``` -### FakeJsonHydrator - -Hydrates JSON array to the declared return type: +If an application already installed `MediaQueryModule`, the existing `#[DbQuery]` +pointcut may remain active. Because `DbQueryInterceptor` resolves to +`FakeQueryInterceptor`, SQL execution is still replaced by fixtures. -- `?SomeEntity` → hydrate single object or return null if JSON is null -- `array` (from PHPDoc `@return`) → hydrate array of objects -- `array` → return raw array -- snake_case keys → camelCase properties (same as Ray.MediaQuery) +## Fixture Vocabulary -Look at how Ray.MediaQuery handles hydration (`/Users/akihito/git/Ray.MediaQuery/src/`) and reuse or replicate the same logic. +Fixture filenames are derived from the `#[DbQuery]` id: -## JSON File Conventions +| Query shape | Fixture | Result | +|-------------|---------|--------| +| row / nullable row | `.json` | one object, raw row, or `null` | +| row list | `.jsonl` | one JSON object per line | +| `void` command | no fixture | no-op success | -- Single entity (`?Entity`): `{queryId}.json` — single JSON object -- Collection (`array`): `{queryId}.jsonl` — JSON Lines, one object per line -- Nullable: missing file or `null` content returns null for nullable return types -- void methods: no file needed +Nested query ids are represented by nested paths, for example +`#[DbQuery('admin/profile')]` maps to `admin/profile.json`. -### Why JSONL for collections? +Unknown `.json` or `.jsonl` files fail fast during module configuration. This +keeps fixture directories as a shared vocabulary rather than a loose set of +unused mock files. -```jsonl -{"todoId": "01HVXXXXXX0007", "todoTitle": "ALPSプロファイルを設計する", "isCompleted": true} -{"todoId": "01HVXXXXXX0008", "todoTitle": "Beフレームワークのチュートリアルを書く", "isCompleted": false} -``` - -- Adding a record = adding a line (no array syntax, no trailing comma issues) -- Git diffs are clean -- Each line is independently valid JSON - -## Exception +## Hydration -```php -final class FakeJsonNotFoundException extends \RuntimeException -{ - public function __construct(string $queryId, string $fakeDir) - { - parent::__construct( - "Fake JSON file not found: {$queryId}.json in {$fakeDir}" - ); - } -} -``` +Default entity hydration follows Ray.MediaQuery conventions: -## Key Behaviors +- JSON keys may use `snake_case` +- constructor parameters and object properties use `camelCase` +- constructor defaults are respected +- raw `array` return types receive raw fixture rows -1. **Commands are no-ops**: `void` return type → silently succeed -2. **Missing file handling**: Nullable returns `null`; non-nullable throws with queryId and directory -3. **snake_case → camelCase**: Automatic key conversion on hydration -4. **Nullable respected**: `?Entity` with null JSON returns null -5. **Array PHPDoc respected**: `@return array` triggers array hydration +When `#[DbQuery(factory: SomeFactory::class)]` is used, FakeQuery calls the +configured Ray.MediaQuery factory method name, currently `factory` by default. +Static methods are invoked statically. Non-static methods are resolved through +the injector. Fixture fields are passed as positional arguments in JSON object +order, matching PDO `FETCH_FUNC` semantics. -## Test Strategy - -Write tests that: -1. Install `FakeQueryModule` with a test fake directory -2. Get interface instance from injector -3. Assert that method returns hydrated entity from JSON - -```php -final class FakeQueryModuleTest extends TestCase -{ - public function testItemQuery(): void - { - $injector = new Injector(new class extends AbstractModule { - protected function configure(): void - { - $this->install(new FakeQueryModule( - fakeDir: __DIR__ . '/Fake', - interfaceDir: __DIR__ . '/Fake/Interface', - )); - } - }); - - $query = $injector->getInstance(TodoQueryInterface::class); - $todo = $query->item('todo-123'); - - $this->assertInstanceOf(TodoEntity::class, $todo); - $this->assertSame('Beフレームワークのチュートリアルを書く', $todo->todoTitle); - } -} -``` +Invalid factory classes, missing factory methods, or non-public factory methods +throw `InvalidFactoryException`; they never silently fall back to default entity +hydration. -## Implementation Notes +## Non Goals -- Study how `MediaQuerySqlModule` scans interfaces and binds them — replicate the same pattern for fake binding -- The `DbQuery` attribute class lives in `Ray\MediaQuery\Annotation\DbQuery` — import from there -- Use `ray/di` interceptor binding: `$this->bindInterceptor(...)` for methods with `#[DbQuery]` -- PHP 8.1+ only — use readonly properties, enums where appropriate +- No fake PDO layer. +- No mutable in-memory database. +- No inferred insert/update/delete side effects. +- No automatic DML metadata until the dedicated `AffectedRows` / `InsertedRow` + fixture design is implemented. diff --git a/README.md b/README.md index 034d84d..06c1580 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,11 @@ Ray.FakeQuery JSON files → hydration → Entity (same interface) ## Installation ```bash -composer require ray/fake-query 1.x-dev --dev +composer require ray/fake-query:^1.0 --dev ``` +This package targets Ray.MediaQuery `^1.1`. + ## Usage Define query interfaces with `#[DbQuery]` as usual: @@ -63,3 +65,27 @@ Create JSON files matching the query ID in `#[DbQuery]`: `void` methods require no file — they succeed silently as no-ops. JSON keys use `snake_case`; entity properties use `camelCase`. Conversion is automatic, matching Ray.MediaQuery behavior. + +## Fixture Format + +| Query shape | File | Meaning | +|-------------|------|---------| +| row / nullable row | `.json` | One JSON object, raw row, or `null`. | +| row list | `.jsonl` | One JSON object per line. Empty files represent empty lists. | +| void command | no file | The command succeeds as a no-op. | + +Constructor-based select result wrappers are supported for return types that +implement `Ray\MediaQuery\Result\PostQueryInterface`, such as a typed selection +object wrapping hydrated rows. FakeQuery passes the hydrated row list to the +wrapper constructor; it does not call `PostQueryInterface::fromContext()`. + +When `#[DbQuery(factory: SomeFactory::class)]` is used, FakeQuery calls the +factory method configured by Ray.MediaQuery (the default method name is +`factory`). Static factory methods are called statically; non-static factory +methods are resolved through the injector. Fixture fields are passed as +positional arguments in JSON object order, matching PDO `FETCH_FUNC` semantics, +so factory fixtures should keep field order aligned with the factory method +parameters. + +DML metadata results such as `AffectedRows` and `InsertedRow` are outside the +current select-fixture scope. diff --git a/composer.json b/composer.json index 359fb27..7d983ef 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,15 @@ { "name": "ray/fake-query", - "description": "", + "description": "JSON and JSONL fixture adapter for Ray.MediaQuery query interfaces", + "type": "library", "license": "MIT", + "keywords": [ + "fixture", + "json", + "media-query", + "ray", + "testing" + ], "authors": [ { "name": "Akihito Koriyama", @@ -9,9 +17,12 @@ } ], "require": { - "php": "^8.2", - "ray/di": "^2.18", - "ray/media-query": "^1.0" + "php": "^8.4", + "phpdocumentor/reflection-docblock": "^6.0", + "ray/aop": "^2.19", + "ray/di": "^2.19", + "ray/media-query": "^1.1", + "aura/sql": "^6.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8", @@ -30,8 +41,11 @@ ] } }, + "support": { + "issues": "https://github.com/ray-di/Ray.FakeQuery/issues", + "source": "https://github.com/ray-di/Ray.FakeQuery/tree/1.x" + }, "scripts": { - "bin": "echo 'bin not installed'", "test": "./vendor/bin/phpunit", "coverage": "php -dzend_extension=xdebug.so -dxdebug.mode=coverage ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage", "phpdbg": "phpdbg -qrr ./vendor/bin/phpunit --coverage-text --coverage-html ./build/coverage --coverage-clover=build/coverage.xml", @@ -69,7 +83,6 @@ "post-update-cmd": "@composer bin all update --ansi" }, "scripts-descriptions": { - "bin": "bamarni/composer-bin-plugin command", "test": "Run unit tests", "coverage": "Generate test coverage report", "phpdbg": "Generate test coverage report (phpdbg)", @@ -90,6 +103,9 @@ "config": { "allow-plugins": { "bamarni/composer-bin-plugin": true + }, + "platform": { + "php": "8.4.0" } }, "extra": { diff --git a/composer.lock b/composer.lock index 80a1342..eb45388 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b33ff6acd16fe81e7492c2f75e5f27fd", + "content-hash": "647f31370c0a037c3d68d1c70032b6da", "packages": [ { "name": "aura/sql", @@ -433,16 +433,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -492,9 +492,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-01T18:43:49+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -726,16 +726,16 @@ }, { "name": "ray/aura-sql-module", - "version": "1.17.0", + "version": "1.17.1", "source": { "type": "git", "url": "https://github.com/ray-di/Ray.AuraSqlModule.git", - "reference": "9ce6181e7637078fcf7cebcf30b5eea8adb13f98" + "reference": "814531f584bfaa759ee527192667b839228848a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ray-di/Ray.AuraSqlModule/zipball/9ce6181e7637078fcf7cebcf30b5eea8adb13f98", - "reference": "9ce6181e7637078fcf7cebcf30b5eea8adb13f98", + "url": "https://api.github.com/repos/ray-di/Ray.AuraSqlModule/zipball/814531f584bfaa759ee527192667b839228848a7", + "reference": "814531f584bfaa759ee527192667b839228848a7", "shasum": "" }, "require": { @@ -785,9 +785,9 @@ ], "support": { "issues": "https://github.com/ray-di/Ray.AuraSqlModule/issues", - "source": "https://github.com/ray-di/Ray.AuraSqlModule/tree/1.17.0" + "source": "https://github.com/ray-di/Ray.AuraSqlModule/tree/1.17.1" }, - "time": "2025-11-20T09:02:54+00:00" + "time": "2026-03-13T00:37:57+00:00" }, { "name": "ray/di", @@ -855,16 +855,16 @@ }, { "name": "ray/input-query", - "version": "v1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/ray-di/Ray.InputQuery.git", - "reference": "9d2954407a861a74e412eb12a5fc44e706ec36a9" + "reference": "457d355ba46b3983a54a0c9cb6a8b3cdac770b42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ray-di/Ray.InputQuery/zipball/9d2954407a861a74e412eb12a5fc44e706ec36a9", - "reference": "9d2954407a861a74e412eb12a5fc44e706ec36a9", + "url": "https://api.github.com/repos/ray-di/Ray.InputQuery/zipball/457d355ba46b3983a54a0c9cb6a8b3cdac770b42", + "reference": "457d355ba46b3983a54a0c9cb6a8b3cdac770b42", "shasum": "" }, "require": { @@ -905,22 +905,22 @@ "description": "Structured input objects from HTTP", "support": { "issues": "https://github.com/ray-di/Ray.InputQuery/issues", - "source": "https://github.com/ray-di/Ray.InputQuery/tree/v1.0.0" + "source": "https://github.com/ray-di/Ray.InputQuery/tree/1.1.0" }, - "time": "2025-07-16T15:19:01+00:00" + "time": "2026-05-02T05:39:06+00:00" }, { "name": "ray/media-query", - "version": "1.0.3", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/ray-di/Ray.MediaQuery.git", - "reference": "785065360127ab434d09b8b7243507703e6ad444" + "reference": "507d1efffd99f883b1c1afbc9d0cf1a35f927129" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ray-di/Ray.MediaQuery/zipball/785065360127ab434d09b8b7243507703e6ad444", - "reference": "785065360127ab434d09b8b7243507703e6ad444", + "url": "https://api.github.com/repos/ray-di/Ray.MediaQuery/zipball/507d1efffd99f883b1c1afbc9d0cf1a35f927129", + "reference": "507d1efffd99f883b1c1afbc9d0cf1a35f927129", "shasum": "" }, "require": { @@ -941,7 +941,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8", - "phpunit/phpunit": "^10.5.46" + "phpunit/phpunit": "^11.5" }, "suggest": { "koriym/csv-entities": "Provides one-to-many entity relation", @@ -973,9 +973,9 @@ "description": "PHP interface-based SQL framework", "support": { "issues": "https://github.com/ray-di/Ray.MediaQuery/issues", - "source": "https://github.com/ray-di/Ray.MediaQuery/tree/1.0.3" + "source": "https://github.com/ray-di/Ray.MediaQuery/tree/1.1.0" }, - "time": "2026-01-24T21:08:47+00:00" + "time": "2026-05-06T14:39:02+00:00" }, { "name": "rize/uri-template", @@ -1043,16 +1043,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -1065,7 +1065,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1090,7 +1090,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -1101,25 +1101,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", "shasum": "" }, "require": { @@ -1166,7 +1170,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" }, "funding": [ { @@ -1186,20 +1190,20 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "webmozart/assert", - "version": "2.1.6", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "shasum": "" }, "require": { @@ -1246,9 +1250,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.6" + "source": "https://github.com/webmozarts/assert/tree/2.3.0" }, - "time": "2026-02-27T10:28:38+00:00" + "time": "2026-04-11T10:33:05+00:00" } ], "packages-dev": [ @@ -2981,12 +2985,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.4" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.4.0" }, - "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.2.0" } diff --git a/src/Exception/InvalidFactoryException.php b/src/Exception/InvalidFactoryException.php new file mode 100644 index 0000000..fb1362e --- /dev/null +++ b/src/Exception/InvalidFactoryException.php @@ -0,0 +1,13 @@ + */ + public readonly array $fakeDirs; + public readonly string $fakeDir; + + /** @param string|list $fakeDir */ + public function __construct(string|array $fakeDir) + { + $fakeDirs = is_string($fakeDir) ? [$fakeDir] : $fakeDir; + $normalized = []; + foreach ($fakeDirs as $dir) { + $normalizedDir = rtrim($dir, '/\\'); + if (! is_dir($normalizedDir) || ! is_readable($normalizedDir)) { + throw new InvalidFakeDirException($dir); + } + + $normalized[] = $normalizedDir; } + + $this->fakeDirs = $normalized; + $this->fakeDir = implode(PATH_SEPARATOR, $normalized); } } diff --git a/src/FakeQueryInterceptor.php b/src/FakeQueryInterceptor.php index 32d43ee..4bf4d1a 100644 --- a/src/FakeQueryInterceptor.php +++ b/src/FakeQueryInterceptor.php @@ -5,34 +5,52 @@ namespace Ray\FakeQuery; use Override; +use Aura\Sql\ExtendedPdo; +use PDOStatement; use Ray\Aop\MethodInterceptor; use Ray\Aop\MethodInvocation; use Ray\FakeQuery\Exception\FakeJsonNotFoundException; use Ray\MediaQuery\Annotation\DbQuery; +use Ray\MediaQuery\ParamConverterInterface; +use Ray\MediaQuery\ParamInjectorInterface; +use Ray\MediaQuery\Result\PostQueryContext; +use Ray\MediaQuery\Result\PostQueryInterface; use Ray\MediaQuery\ReturnEntityInterface; use ReflectionNamedType; use ReflectionType; use ReflectionUnionType; use function array_filter; +use function array_key_exists; use function array_map; +use function array_slice; use function array_values; use function assert; +use function class_exists; use function explode; use function file_exists; use function file_get_contents; +use function is_array; +use function is_bool; +use function is_scalar; +use function is_subclass_of; use function json_decode; use function trim; use const JSON_THROW_ON_ERROR; -/** @psalm-import-type JsonRowList from Types */ +/** + * @psalm-import-type JsonRow from Types + * @psalm-import-type JsonRowList from Types + */ final class FakeQueryInterceptor implements MethodInterceptor { public function __construct( private readonly FakeQueryConfig $config, private readonly JsonHydrator $hydrator, private readonly ReturnEntityInterface $returnEntity, + private readonly ParamInjectorInterface $paramInjector, + private readonly ParamConverterInterface $paramConverter, ) { } @@ -49,18 +67,121 @@ public function invoke(MethodInvocation $invocation): mixed return null; } + $values = $this->values($invocation); + + /** @var class-string|null $entityClass */ + $entityClass = ($this->returnEntity)($method); + + if ($returnType instanceof ReflectionNamedType) { + $typeName = $returnType->getName(); + if (class_exists($typeName) && is_subclass_of($typeName, PostQueryInterface::class)) { + return $this->selectPostQuery($typeName, $dbQuery, $entityClass, $values); + } + } + $isRow = $dbQuery->type === 'row' || $returnType instanceof ReflectionUnionType || ($returnType instanceof ReflectionNamedType && $returnType->getName() !== 'array'); - $ext = $isRow ? '.json' : '.jsonl'; - $jsonFile = $this->config->fakeDir . '/' . $dbQuery->id . $ext; - - if (! file_exists($jsonFile)) { - if ($this->isNullable($returnType)) { + if ($isRow) { + $data = $this->readRow($dbQuery, $values); + if ($data === null && $this->isNullable($returnType)) { return null; } + if ($data === null) { + throw new FakeJsonNotFoundException($dbQuery->id . '.json', $this->config->fakeDir); + } + + return $this->hydrator->hydrate($data, $entityClass, true, $dbQuery); + } + + $data = $this->readRows($dbQuery, $values); + + return $this->hydrator->hydrate($data, $entityClass, false, $dbQuery); + } + + /** + * @param MethodInvocation $invocation + * + * @return array + */ + private function values(MethodInvocation $invocation): array + { + $values = $this->paramInjector->getArguments($invocation); + ($this->paramConverter)($values); + + /** @var array $values */ + return $values; + } + + /** + * @param class-string $postQueryClass + * @param class-string|null $entityClass + * @param array $values + */ + private function selectPostQuery(string $postQueryClass, DbQuery $dbQuery, string|null $entityClass, array $values): PostQueryInterface + { + $rows = $this->hydrator->hydrate( + $this->readRows($dbQuery, $values), + $dbQuery->factory === '' ? null : $entityClass, + false, + $dbQuery, + ); + assert(is_array($rows)); + + $pdo = new ExtendedPdo('sqlite::memory:'); + $statement = $pdo->query('SELECT 1'); + assert($statement instanceof PDOStatement); + + return $postQueryClass::fromContext(new PostQueryContext($statement, $pdo, $values, $rows)); + } + + /** + * @param array $values + * + * @return JsonRow|null + */ + private function readRow(DbQuery $dbQuery, array $values): array|null + { + if ($this->hasFile($dbQuery, '.json')) { + /** @var JsonRow $data */ + $data = json_decode($this->readContent($dbQuery, '.json'), true, 512, JSON_THROW_ON_ERROR); + + return $this->stripParams($data); + } + + if (! $this->hasFile($dbQuery, '.jsonl')) { + return null; + } + + $rows = $this->selectRows($this->parseJsonl($this->readContent($dbQuery, '.jsonl')), $values); + $row = $rows[0] ?? null; + + return $row === null ? null : $this->stripParams($row); + } + + /** + * @param array $values + * + * @return JsonRowList + */ + private function readRows(DbQuery $dbQuery, array $values): array + { + $rows = $this->parseJsonl($this->readContent($dbQuery, '.jsonl')); + + $selected = array_map( + fn (array $row): array => $this->stripParams($row), + $this->selectRows($rows, $values), + ); + + return $this->applyWindow($selected, $values); + } + + private function readContent(DbQuery $dbQuery, string $ext): string + { + $jsonFile = $this->findFile($dbQuery, $ext); + if ($jsonFile === null) { throw new FakeJsonNotFoundException($dbQuery->id . $ext, $this->config->fakeDir); } @@ -69,21 +190,155 @@ public function invoke(MethodInvocation $invocation): mixed throw new FakeJsonNotFoundException($dbQuery->id . $ext, $this->config->fakeDir); // @codeCoverageIgnore — file_exists passed } - /** @psalm-suppress MixedAssignment */ - $data = $isRow - ? json_decode($content, true, 512, JSON_THROW_ON_ERROR) - : $this->parseJsonl($content); + return $content; + } - $entityClass = ($this->returnEntity)($method); + private function findFile(DbQuery $dbQuery, string $ext): string|null + { + foreach ($this->config->fakeDirs as $fakeDir) { + $jsonFile = $fakeDir . '/' . $dbQuery->id . $ext; + if (file_exists($jsonFile)) { + return $jsonFile; + } + } - return $this->hydrator->hydrate($data, $entityClass, $isRow); + return null; + } + + private function hasFile(DbQuery $dbQuery, string $ext): bool + { + return $this->findFile($dbQuery, $ext) !== null; + } + + /** + * @param JsonRowList $rows + * @param array $values + * + * @return JsonRowList + */ + private function selectRows(array $rows, array $values): array + { + $hasParams = false; + foreach ($rows as $row) { + if (isset($row['_params']) && is_array($row['_params'])) { + $hasParams = true; + break; + } + } + + if (! $hasParams) { + return $rows; + } + + return array_values(array_filter( + $rows, + fn (array $row): bool => $this->rowMatchesParams($row, $values), + )); + } + + /** + * @param JsonRow $row + * @param array $values + */ + private function rowMatchesParams(array $row, array $values): bool + { + $params = $row['_params'] ?? null; + if (! is_array($params) || ! $this->hasOnlyScalarParams($params)) { + return false; + } + + /** @var array $params */ + return $this->matchesParams($params, $values); + } + + /** @param array $params */ + private function hasOnlyScalarParams(array $params): bool + { + return array_filter( + $params, + static fn (mixed $value): bool => ! is_scalar($value) && $value !== null, + ) === []; + } + + /** + * @param array $params + * @param array $values + */ + private function matchesParams(array $params, array $values): bool + { + foreach ($params as $key => $expected) { + if (! array_key_exists($key, $values)) { + return false; + } + + if ($this->normalize($values[$key]) !== $this->normalize($expected)) { + return false; + } + } + + return true; + } + + private function normalize(mixed $value): mixed + { + if (is_bool($value)) { + return $value ? 1 : 0; + } + + if (is_scalar($value) || $value === null) { + return $value; + } + + return $value; + } + + /** + * @param JsonRowList $rows + * @param array $values + * + * @return JsonRowList + */ + private function applyWindow(array $rows, array $values): array + { + $offset = isset($values['offset']) && is_scalar($values['offset']) ? (int) $values['offset'] : 0; + if ($offset < 0) { + $offset = 0; + } + + $limit = isset($values['limit']) && is_scalar($values['limit']) ? (int) $values['limit'] : null; + if ($limit === null) { + return $offset === 0 ? $rows : array_slice($rows, $offset); + } + + if ($limit < 0) { + $limit = 0; + } + + return array_slice($rows, $offset, $limit); + } + + /** + * @param JsonRow $row + * + * @return JsonRow + */ + private function stripParams(array $row): array + { + unset($row['_params']); + + return $row; } /** @return JsonRowList */ private function parseJsonl(string $content): array { + $trimmed = trim($content); + if ($trimmed === '') { + return []; + } + $lines = array_values(array_filter( - explode("\n", trim($content)), + explode("\n", $trimmed), static fn (string $line): bool => $line !== '', )); @@ -102,8 +357,6 @@ private function isNullable(ReflectionType|null $returnType): bool return $returnType->allowsNull(); } - // Ray.Di normalizes `Entity|null` to `?Entity` (ReflectionNamedType), - // so ReflectionUnionType is unreachable in practice. Verified via xtrace. if (! ($returnType instanceof ReflectionUnionType)) { // @codeCoverageIgnore return false; // @codeCoverageIgnore } diff --git a/src/FakeQueryModule.php b/src/FakeQueryModule.php index 4917f5b..8bcb4ff 100644 --- a/src/FakeQueryModule.php +++ b/src/FakeQueryModule.php @@ -4,31 +4,51 @@ namespace Ray\FakeQuery; +use FilesystemIterator; use Override; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; use Ray\Di\AbstractModule; +use Ray\Di\Scope; use Ray\FakeQuery\Exception\UnknownFakeJsonException; +use Ray\InputQuery\ToArray; +use Ray\InputQuery\ToArrayInterface; use Ray\MediaQuery\Annotation\DbQuery; +use Ray\MediaQuery\Annotation\Qualifier\FactoryMethod; +use Ray\MediaQuery\DbQueryInterceptor; +use Ray\MediaQuery\ParamConverter; +use Ray\MediaQuery\ParamConverterInterface; +use Ray\MediaQuery\ParamInjector; +use Ray\MediaQuery\ParamInjectorInterface; use Ray\MediaQuery\Queries; use Ray\MediaQuery\ReturnEntity; use Ray\MediaQuery\ReturnEntityInterface; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use ReflectionClass; +use SplFileInfo; +use function assert; use function basename; -use function glob; +use function is_array; use function pathinfo; +use function str_replace; +use function strlen; +use function substr; -use const GLOB_BRACE; -use const GLOB_NOSORT; +use const DIRECTORY_SEPARATOR; use const PATHINFO_EXTENSION; /** @psalm-import-type DbQueryIdSet from Types */ final class FakeQueryModule extends AbstractModule { + /** + * @param string|list $fakeDir + * @param string|list|Queries $interfaceDir + */ public function __construct( - private readonly string $fakeDir, - private readonly string $interfaceDir, + private readonly string|array $fakeDir, + private readonly string|array|Queries $interfaceDir, AbstractModule|null $module = null, ) { parent::__construct($module); @@ -37,42 +57,81 @@ public function __construct( #[Override] protected function configure(): void { - $this->bind(FakeQueryConfig::class)->toInstance(new FakeQueryConfig($this->fakeDir)); + $config = new FakeQueryConfig($this->fakeDir); + $this->bind(FakeQueryConfig::class)->toInstance($config); $this->bind(JsonHydrator::class); $this->bind(DocBlockFactoryInterface::class)->toInstance(DocBlockFactory::createInstance()); $this->bind(ReturnEntityInterface::class)->to(ReturnEntity::class); + $this->bind(ParamInjectorInterface::class)->to(ParamInjector::class); + $this->bind(ParamConverterInterface::class)->to(ParamConverter::class); + $this->bind(ToArrayInterface::class)->to(ToArray::class); + $this->bind()->annotatedWith(FactoryMethod::class)->toInstance('factory'); - $queries = Queries::fromDir($this->interfaceDir); + $queries = $this->queries(); foreach ($queries->classes as $class) { $this->bind($class)->toNull(); } - $this->validateFakeFiles($queries->classes); + $this->validateFakeFiles($queries->classes, $config); $this->bindInterceptor( $this->matcher->any(), $this->matcher->annotatedWith(DbQuery::class), - [FakeQueryInterceptor::class], + [DbQueryInterceptor::class], ); + $this->bind(DbQueryInterceptor::class)->to(FakeQueryInterceptor::class)->in(Scope::SINGLETON); + } + + private function queries(): Queries + { + if ($this->interfaceDir instanceof Queries) { + return $this->interfaceDir; + } + + if (is_array($this->interfaceDir)) { + return Queries::fromClasses($this->interfaceDir); + } + + return Queries::fromDir($this->interfaceDir); } /** @param list $classes */ - private function validateFakeFiles(array $classes): void + private function validateFakeFiles(array $classes, FakeQueryConfig $config): void { $knownIds = $this->collectDbQueryIds($classes); - $globResult = glob($this->fakeDir . '/*.{json,jsonl}', GLOB_NOSORT | GLOB_BRACE); - $files = $globResult === false ? [] : $globResult; - foreach ($files as $file) { - $basename = basename($file); - $ext = pathinfo($file, PATHINFO_EXTENSION); - $stem = $ext === 'jsonl' ? basename($file, '.jsonl') : basename($file, '.json'); - if (! isset($knownIds[$stem])) { - throw new UnknownFakeJsonException($basename, $this->fakeDir); + foreach ($config->fakeDirs as $fakeDir) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($fakeDir, FilesystemIterator::SKIP_DOTS), + ); + foreach ($files as $file) { + assert($file instanceof SplFileInfo); + if (! $file->isFile()) { + continue; + } + + $ext = pathinfo($file->getPathname(), PATHINFO_EXTENSION); + if ($ext !== 'json' && $ext !== 'jsonl') { + continue; + } + + $basename = basename($file->getPathname()); + $stem = $this->queryIdFromFile($file, $fakeDir, $ext); + if (! isset($knownIds[$stem])) { + throw new UnknownFakeJsonException($basename, $fakeDir); + } } } } + private function queryIdFromFile(SplFileInfo $file, string $fakeDir, string $ext): string + { + $relative = substr($file->getPathname(), strlen($fakeDir) + 1); + $queryId = substr($relative, 0, -strlen('.' . $ext)); + + return str_replace(DIRECTORY_SEPARATOR, '/', $queryId); + } + /** * @param list $classes * diff --git a/src/JsonHydrator.php b/src/JsonHydrator.php index f89cacd..d46c611 100644 --- a/src/JsonHydrator.php +++ b/src/JsonHydrator.php @@ -4,36 +4,58 @@ namespace Ray\FakeQuery; +use Ray\Di\InjectorInterface; +use Ray\FakeQuery\Exception\InvalidFactoryException; +use Ray\MediaQuery\Annotation\DbQuery; +use Ray\MediaQuery\Annotation\Qualifier\FactoryMethod; use Ray\MediaQuery\StringCase; use ReflectionClass; +use ReflectionMethod; use ReflectionParameter; use function array_key_exists; use function array_map; +use function array_values; use function assert; +use function class_exists; use function is_array; use function method_exists; /** @psalm-import-type JsonRow from Types */ final class JsonHydrator { + public function __construct( + #[FactoryMethod] + private readonly string $factoryMethod, + private readonly InjectorInterface $injector, + ) { + } + /** @param class-string|null $entityClass */ - public function hydrate(mixed $data, string|null $entityClass, bool $isRow): mixed + public function hydrate(mixed $data, string|null $entityClass, bool $isRow, DbQuery|null $dbQuery = null): mixed { if ($isRow) { - return $this->hydrateRow($data, $entityClass); + return $this->hydrateRow($data, $entityClass, $dbQuery); } - return $this->hydrateRowList($data, $entityClass); + return $this->hydrateRowList($data, $entityClass, $dbQuery); } /** @param class-string|null $entityClass */ - private function hydrateRow(mixed $data, string|null $entityClass): mixed + private function hydrateRow(mixed $data, string|null $entityClass, DbQuery|null $dbQuery): mixed { if ($data === null) { return null; // @codeCoverageIgnore json_decode returns null only for "null" content } + $factory = $this->factory($dbQuery); + if ($factory !== null) { + assert(is_array($data)); + /** @var JsonRow $data */ + + return $factory($data); + } + if ($entityClass === null) { return $data; } @@ -45,8 +67,23 @@ private function hydrateRow(mixed $data, string|null $entityClass): mixed } /** @param class-string|null $entityClass */ - private function hydrateRowList(mixed $data, string|null $entityClass): mixed + private function hydrateRowList(mixed $data, string|null $entityClass, DbQuery|null $dbQuery): mixed { + $factory = $this->factory($dbQuery); + if ($factory !== null) { + assert(is_array($data)); + + return array_map( + static function (mixed $row) use ($factory): mixed { + assert(is_array($row)); + /** @var JsonRow $row */ + + return $factory($row); + }, + $data, + ); + } + if ($entityClass === null) { return $data; // @codeCoverageIgnore ReturnEntity always resolves entity for @return array } @@ -64,6 +101,33 @@ function (mixed $row) use ($entityClass): object { ); } + /** @return (callable(JsonRow): mixed)|null */ + private function factory(DbQuery|null $dbQuery): callable|null + { + if ($dbQuery === null || $dbQuery->factory === '') { + return null; + } + + $factoryClass = $dbQuery->factory; + $factoryMethod = $this->factoryMethod; + if (! class_exists($factoryClass) || ! method_exists($factoryClass, $factoryMethod)) { + throw new InvalidFactoryException($factoryClass, $factoryMethod); + } + + $method = new ReflectionMethod($factoryClass, $factoryMethod); + if (! $method->isPublic()) { + throw new InvalidFactoryException($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)); + } + /** * @param JsonRow $row * @param class-string $entityClass diff --git a/tests/Fake/Entity/FactoryTodoEntity.php b/tests/Fake/Entity/FactoryTodoEntity.php new file mode 100644 index 0000000..137c16b --- /dev/null +++ b/tests/Fake/Entity/FactoryTodoEntity.php @@ -0,0 +1,17 @@ + */ + #[DbQuery('factory_static_list', factory: StaticTodoFactory::class)] + public function staticList(): array; + + #[DbQuery('nested/factory_static_item', factory: StaticTodoFactory::class)] + public function nestedStaticItem(): FactoryTodoEntity; + + #[DbQuery('factory_missing_class_item', factory: 'Ray\FakeQuery\Factory\MissingTodoFactory')] + public function missingFactoryClass(): FactoryTodoEntity; + + #[DbQuery('factory_missing_method_item', factory: MissingMethodTodoFactory::class)] + public function missingFactoryMethod(): FactoryTodoEntity; +} diff --git a/tests/Fake/Query/TodoQueryInterface.php b/tests/Fake/Query/TodoQueryInterface.php index 24db334..bb06763 100644 --- a/tests/Fake/Query/TodoQueryInterface.php +++ b/tests/Fake/Query/TodoQueryInterface.php @@ -16,6 +16,13 @@ public function item(string $todoId): ?TodoEntity; #[DbQuery('todo_list')] public function list(): array; + /** @return array */ + #[DbQuery('todo_by_status')] + public function listByStatus(bool $isCompleted, int $limit = 10, int $offset = 0): array; + + #[DbQuery('todo_by_status', type: 'row')] + public function firstByStatus(bool $isCompleted): ?TodoEntity; + /** @return array */ #[DbQuery('todo_item', type: 'row')] public function itemExplicitRow(string $todoId): array; diff --git a/tests/Fake/Query/TodoSelectionQueryInterface.php b/tests/Fake/Query/TodoSelectionQueryInterface.php new file mode 100644 index 0000000..ff6805a --- /dev/null +++ b/tests/Fake/Query/TodoSelectionQueryInterface.php @@ -0,0 +1,15 @@ + + */ +final readonly class TodoSelection implements PostQueryInterface, Countable, IteratorAggregate +{ + /** + * @param list $rows + * @param array $values + */ + private function __construct( + public array $rows, + public array $values, + ) { + } + + public static function fromContext(PostQueryContext $context): static + { + /** @var list $rows */ + $rows = $context->rows; + + return new self($rows, $context->values); + } + + /** @return list */ + public function titles(): array + { + return array_map(static fn (FactoryTodoEntity $todo): string => $todo->todoTitle, $this->rows); + } + + public function count(): int + { + return count($this->rows); + } + + /** @return ArrayIterator */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->rows); + } +} diff --git a/tests/Fake/ThrowingSqlQuery.php b/tests/Fake/ThrowingSqlQuery.php new file mode 100644 index 0000000..e7541eb --- /dev/null +++ b/tests/Fake/ThrowingSqlQuery.php @@ -0,0 +1,59 @@ + $values */ + public function getRow(string $sqlId, array $values = [], FetchInterface|null $fetch = null): array|object|null + { + throw new RuntimeException('Real SQL getRow should not be called.'); + } + + /** @param array $values */ + public function getRowList(string $sqlId, array $values = [], FetchInterface|null $fetch = null): array + { + throw new RuntimeException('Real SQL getRowList should not be called.'); + } + + /** @param array $values */ + public function exec(string $sqlId, array $values = [], FetchInterface|null $fetch = null): void + { + throw new RuntimeException('Real SQL exec should not be called.'); + } + + /** @param array $values */ + public function execPostQuery( + string $sqlId, + array $values, + string $postQueryClass, + FetchInterface|null $fetch = null, + ): PostQueryInterface { + throw new RuntimeException('Real SQL execPostQuery should not be called.'); + } + + /** @param array $values */ + public function getCount(string $sqlId, array $values): int + { + throw new RuntimeException('Real SQL getCount should not be called.'); + } + + /** @param array $values */ + public function getPages( + string $sqlId, + array $values, + int $perPage, + string $queryTemplate = '/{?page}', + string|null $entity = null, + ): PagesInterface { + throw new RuntimeException('Real SQL getPages should not be called.'); + } +} diff --git a/tests/Fake/factory_injected_item.json b/tests/Fake/factory_injected_item.json new file mode 100644 index 0000000..8f4fd3e --- /dev/null +++ b/tests/Fake/factory_injected_item.json @@ -0,0 +1,5 @@ +{ + "todo_id": "01HVFACTORY2", + "todo_title": "Injected factory item", + "created_at": "2026-01-02 00:00:00" +} diff --git a/tests/Fake/factory_missing_class_item.json b/tests/Fake/factory_missing_class_item.json new file mode 100644 index 0000000..0d50414 --- /dev/null +++ b/tests/Fake/factory_missing_class_item.json @@ -0,0 +1,5 @@ +{ + "todo_id": "01HVMISSING1", + "todo_title": "Missing factory class", + "created_at": "2026-01-05 00:00:00" +} diff --git a/tests/Fake/factory_missing_method_item.json b/tests/Fake/factory_missing_method_item.json new file mode 100644 index 0000000..0e943a2 --- /dev/null +++ b/tests/Fake/factory_missing_method_item.json @@ -0,0 +1,5 @@ +{ + "todo_id": "01HVMISSING2", + "todo_title": "Missing factory method", + "created_at": "2026-01-06 00:00:00" +} diff --git a/tests/Fake/factory_static_item.json b/tests/Fake/factory_static_item.json new file mode 100644 index 0000000..ee52eb5 --- /dev/null +++ b/tests/Fake/factory_static_item.json @@ -0,0 +1,5 @@ +{ + "todo_id": "01HVFACTORY1", + "todo_title": "Static factory item", + "created_at": "2026-01-01 00:00:00" +} diff --git a/tests/Fake/factory_static_list.jsonl b/tests/Fake/factory_static_list.jsonl new file mode 100644 index 0000000..e9ab8dd --- /dev/null +++ b/tests/Fake/factory_static_list.jsonl @@ -0,0 +1,2 @@ +{"todo_id":"01HVFACTORY3","todo_title":"Static factory list 1","created_at":"2026-01-03 00:00:00"} +{"todo_id":"01HVFACTORY4","todo_title":"Static factory list 2","created_at":"2026-01-04 00:00:00"} diff --git a/tests/Fake/nested/factory_static_item.json b/tests/Fake/nested/factory_static_item.json new file mode 100644 index 0000000..d15c02f --- /dev/null +++ b/tests/Fake/nested/factory_static_item.json @@ -0,0 +1,5 @@ +{ + "todo_id": "01HVNESTED1", + "todo_title": "Nested static factory item", + "created_at": "2026-01-05 00:00:00" +} diff --git a/tests/FakeQueryModuleTest.php b/tests/FakeQueryModuleTest.php index 5526113..d48f73a 100644 --- a/tests/FakeQueryModuleTest.php +++ b/tests/FakeQueryModuleTest.php @@ -7,20 +7,33 @@ use PHPUnit\Framework\TestCase; use Ray\Di\AbstractModule; use Ray\Di\Injector; +use Ray\FakeQuery\Entity\FactoryTodoEntity; use Ray\FakeQuery\Entity\TodoEntity; use Ray\FakeQuery\Entity\UserEntity; use Ray\FakeQuery\Exception\FakeJsonNotFoundException; +use Ray\FakeQuery\Exception\InvalidFactoryException; use Ray\FakeQuery\Exception\InvalidFakeDirException; use Ray\FakeQuery\Exception\UnknownFakeJsonException; +use Ray\FakeQuery\Query\FactoryTodoQueryInterface; use Ray\FakeQuery\Query\TodoCommandInterface; use Ray\FakeQuery\Query\TodoQueryInterface; +use Ray\FakeQuery\Query\TodoSelectionQueryInterface; use Ray\FakeQuery\Query\UserQueryInterface; +use Ray\FakeQuery\Result\TodoSelection; +use Ray\MediaQuery\DbQueryConfig; +use Ray\MediaQuery\MediaQueryModule; +use Ray\MediaQuery\Queries; +use Ray\MediaQuery\SqlQueryInterface; + +use const PATH_SEPARATOR; final class FakeQueryModuleTest extends TestCase { private TodoQueryInterface $query; private TodoCommandInterface $command; private UserQueryInterface $userQuery; + private FactoryTodoQueryInterface $factoryQuery; + private TodoSelectionQueryInterface $selectionQuery; protected function setUp(): void { @@ -52,6 +65,14 @@ protected function configure(): void /** @var UserQueryInterface $userQuery */ $userQuery = $injector->getInstance(UserQueryInterface::class); $this->userQuery = $userQuery; + + /** @var FactoryTodoQueryInterface $factoryQuery */ + $factoryQuery = $injector->getInstance(FactoryTodoQueryInterface::class); + $this->factoryQuery = $factoryQuery; + + /** @var TodoSelectionQueryInterface $selectionQuery */ + $selectionQuery = $injector->getInstance(TodoSelectionQueryInterface::class); + $this->selectionQuery = $selectionQuery; } public function testItemReturnsEntity(): void @@ -107,6 +128,99 @@ public function testConstructorHydrationList(): void $this->assertFalse($list[1]->isActive); } + public function testStaticFactoryHydration(): void + { + $todo = $this->factoryQuery->staticItem(); + + $this->assertInstanceOf(FactoryTodoEntity::class, $todo); + $this->assertSame('01HVFACTORY1', $todo->todoId); + $this->assertSame('Static factory item', $todo->todoTitle); + $this->assertSame('2026-01-01 00:00:00', $todo->createdAt->format('Y-m-d H:i:s')); + } + + public function testInjectedFactoryHydration(): void + { + $todo = $this->factoryQuery->injectedItem(); + + $this->assertInstanceOf(FactoryTodoEntity::class, $todo); + $this->assertSame('01HVFACTORY2', $todo->todoId); + $this->assertSame('Injected factory item', $todo->todoTitle); + $this->assertSame('2026-01-02 00:00:00', $todo->createdAt->format('Y-m-d H:i:s')); + } + + public function testStaticFactoryListHydration(): void + { + $list = $this->factoryQuery->staticList(); + + $this->assertContainsOnlyInstancesOf(FactoryTodoEntity::class, $list); + $this->assertSame('01HVFACTORY3', $list[0]->todoId); + $this->assertSame('2026-01-04 00:00:00', $list[1]->createdAt->format('Y-m-d H:i:s')); + } + + public function testNestedQueryIdFixture(): void + { + $todo = $this->factoryQuery->nestedStaticItem(); + + $this->assertSame('01HVNESTED1', $todo->todoId); + $this->assertSame('Nested static factory item', $todo->todoTitle); + } + + public function testMissingFactoryClassThrows(): void + { + $this->expectException(InvalidFactoryException::class); + $this->expectExceptionMessage('Ray\FakeQuery\Factory\MissingTodoFactory::factory()'); + + $this->factoryQuery->missingFactoryClass(); + } + + public function testMissingFactoryMethodThrows(): void + { + $this->expectException(InvalidFactoryException::class); + $this->expectExceptionMessage('Ray\FakeQuery\Factory\MissingMethodTodoFactory::factory()'); + + $this->factoryQuery->missingFactoryMethod(); + } + + public function testPostQuerySelectionHydratesFactoryRows(): void + { + $selection = $this->selectionQuery->list(); + + $this->assertInstanceOf(TodoSelection::class, $selection); + $this->assertCount(2, $selection); + $this->assertSame(['Static factory list 1', 'Static factory list 2'], $selection->titles()); + $this->assertSame([], $selection->values); + } + + public function testFakeQueryOverridesExistingMediaQueryInterceptors(): void + { + $fakeDir = __DIR__ . '/Fake'; + $interfaceDir = __DIR__ . '/Fake/Query'; + $module = new class ($interfaceDir) extends AbstractModule { + public function __construct(private readonly string $interfaceDir) + { + parent::__construct(); + } + + protected function configure(): void + { + $this->install(new MediaQueryModule( + Queries::fromDir($this->interfaceDir), + [new DbQueryConfig(__DIR__ . '/FakeSql')], + )); + $this->bind(SqlQueryInterface::class)->to(ThrowingSqlQuery::class); + } + }; + $module->override(new FakeQueryModule($fakeDir, $interfaceDir)); + $injector = new Injector($module, __DIR__ . '/tmp'); + + /** @var TodoQueryInterface $query */ + $query = $injector->getInstance(TodoQueryInterface::class); + $todo = $query->item('01HVXXXXXX0008'); + + $this->assertInstanceOf(TodoEntity::class, $todo); + $this->assertSame('Write Be Framework tutorial', $todo->todoTitle); + } + public function testUnionNullableWithMissingFileReturnsNull(): void { $injector = new Injector(new class extends AbstractModule { @@ -175,6 +289,139 @@ public function testInvalidFakeDirThrows(): void new FakeQueryConfig('/nonexistent/dir'); } + public function testFakeDirTrailingSeparatorsAreNormalized(): void + { + $slashConfig = new FakeQueryConfig(__DIR__ . '/Fake/'); + $backslashConfig = new FakeQueryConfig(__DIR__ . '/Fake\\'); + + $this->assertSame(__DIR__ . '/Fake', $slashConfig->fakeDir); + $this->assertSame(__DIR__ . '/Fake', $backslashConfig->fakeDir); + } + + public function testFakeQueryModuleAcceptsTrailingFakeDirSeparator(): void + { + $injector = new Injector(new class extends AbstractModule { + protected function configure(): void + { + $this->install(new FakeQueryModule( + __DIR__ . '/Fake/', + __DIR__ . '/Fake/Query', + )); + } + }, __DIR__ . '/tmp'); + + /** @var TodoQueryInterface $query */ + $query = $injector->getInstance(TodoQueryInterface::class); + $todo = $query->item('01HVXXXXXX0008'); + + $this->assertInstanceOf(TodoEntity::class, $todo); + $this->assertSame('Write Be Framework tutorial', $todo->todoTitle); + } + + public function testFakeQueryConfigAcceptsMultipleDirectories(): void + { + $config = new FakeQueryConfig([__DIR__ . '/Fake/', __DIR__ . '/FakeSecondary/']); + + $this->assertSame([__DIR__ . '/Fake', __DIR__ . '/FakeSecondary'], $config->fakeDirs); + $this->assertSame(__DIR__ . '/Fake' . PATH_SEPARATOR . __DIR__ . '/FakeSecondary', $config->fakeDir); + } + + public function testFakeQueryModuleAcceptsQueryClassList(): void + { + $injector = new Injector(new class extends AbstractModule { + protected function configure(): void + { + $this->install(new FakeQueryModule( + __DIR__ . '/Fake', + [ + TodoQueryInterface::class, + TodoCommandInterface::class, + UserQueryInterface::class, + FactoryTodoQueryInterface::class, + TodoSelectionQueryInterface::class, + ], + )); + } + }, __DIR__ . '/tmp'); + + /** @var TodoQueryInterface $query */ + $query = $injector->getInstance(TodoQueryInterface::class); + + $todo = $query->item('01HVXXXXXX0008'); + + $this->assertInstanceOf(TodoEntity::class, $todo); + $this->assertSame('Write Be Framework tutorial', $todo->todoTitle); + } + + public function testFakeQueryModuleAcceptsQueriesObject(): void + { + $injector = new Injector(new class extends AbstractModule { + protected function configure(): void + { + $this->install(new FakeQueryModule( + __DIR__ . '/Fake', + Queries::fromClasses([ + TodoQueryInterface::class, + TodoCommandInterface::class, + UserQueryInterface::class, + FactoryTodoQueryInterface::class, + TodoSelectionQueryInterface::class, + ]), + )); + } + }, __DIR__ . '/tmp'); + + /** @var TodoQueryInterface $query */ + $query = $injector->getInstance(TodoQueryInterface::class); + + $todo = $query->item('01HVXXXXXX0008'); + + $this->assertInstanceOf(TodoEntity::class, $todo); + $this->assertSame('Write Be Framework tutorial', $todo->todoTitle); + } + + public function testFakeQueryModuleReadsMultipleFakeDirsAndFiltersParams(): void + { + $injector = new Injector(new class extends AbstractModule { + protected function configure(): void + { + $this->install(new FakeQueryModule( + [__DIR__ . '/FakeEmpty', __DIR__ . '/FakeSecondary'], + [TodoQueryInterface::class], + )); + } + }, __DIR__ . '/tmp'); + + /** @var TodoQueryInterface $query */ + $query = $injector->getInstance(TodoQueryInterface::class); + $list = $query->listByStatus(true, limit: 1, offset: 1); + + $this->assertCount(1, $list); + $this->assertSame('01HVSTATUS3', $list[0]->todoId); + $this->assertTrue($list[0]->isCompleted); + } + + public function testFakeQueryModuleSelectsRowFromJsonlWhenJsonIsMissing(): void + { + $injector = new Injector(new class extends AbstractModule { + protected function configure(): void + { + $this->install(new FakeQueryModule( + __DIR__ . '/FakeSecondary', + [TodoQueryInterface::class], + )); + } + }, __DIR__ . '/tmp'); + + /** @var TodoQueryInterface $query */ + $query = $injector->getInstance(TodoQueryInterface::class); + $todo = $query->firstByStatus(false); + + $this->assertInstanceOf(TodoEntity::class, $todo); + $this->assertSame('01HVSTATUS1', $todo->todoId); + $this->assertFalse($todo->isCompleted); + } + public function testUnknownFakeJsonFileThrows(): void { $this->expectException(UnknownFakeJsonException::class); @@ -190,4 +437,20 @@ protected function configure(): void } }, __DIR__ . '/tmp'); } + + public function testUnknownNestedFakeJsonFileThrows(): void + { + $this->expectException(UnknownFakeJsonException::class); + $this->expectExceptionMessage('stray_query.json'); + + new Injector(new class extends AbstractModule { + protected function configure(): void + { + $this->install(new FakeQueryModule( + __DIR__ . '/FakeUnknownNested', + __DIR__ . '/Fake/Query', + )); + } + }, __DIR__ . '/tmp'); + } } diff --git a/tests/FakeSecondary/todo_by_status.jsonl b/tests/FakeSecondary/todo_by_status.jsonl new file mode 100644 index 0000000..f4a511f --- /dev/null +++ b/tests/FakeSecondary/todo_by_status.jsonl @@ -0,0 +1,3 @@ +{"_params":{"isCompleted":false},"todoId":"01HVSTATUS1","todoTitle":"Open item","isCompleted":false} +{"_params":{"isCompleted":true},"todoId":"01HVSTATUS2","todoTitle":"Done item 1","isCompleted":true} +{"_params":{"isCompleted":true},"todoId":"01HVSTATUS3","todoTitle":"Done item 2","isCompleted":true} diff --git a/tests/FakeUnknownNested/nested/stray_query.json b/tests/FakeUnknownNested/nested/stray_query.json new file mode 100644 index 0000000..a5ce8cb --- /dev/null +++ b/tests/FakeUnknownNested/nested/stray_query.json @@ -0,0 +1,3 @@ +{ + "todo_id": "01HVUNKNOWN" +} diff --git a/vendor-bin/require-checker/composer.lock b/vendor-bin/require-checker/composer.lock index 1315571..7c9e8db 100644 --- a/vendor-bin/require-checker/composer.lock +++ b/vendor-bin/require-checker/composer.lock @@ -7,103 +7,27 @@ "content-hash": "521e65753d7d272f2212013dd15ca284", "packages": [], "packages-dev": [ - { - "name": "azjezz/psl", - "version": "4.3.0", - "source": { - "type": "git", - "url": "https://github.com/azjezz/psl.git", - "reference": "74c95be0214eb7ea39146ed00ac4eb71b45d787b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/azjezz/psl/zipball/74c95be0214eb7ea39146ed00ac4eb71b45d787b", - "reference": "74c95be0214eb7ea39146ed00ac4eb71b45d787b", - "shasum": "" - }, - "require": { - "ext-bcmath": "*", - "ext-intl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-sodium": "*", - "php": "~8.3.0 || ~8.4.0 || ~8.5.0", - "revolt/event-loop": "^1.0.7" - }, - "require-dev": { - "carthage-software/mago": "^1.6.0", - "infection/infection": "^0.31.2", - "php-coveralls/php-coveralls": "^2.7.0", - "phpbench/phpbench": "^1.4.0", - "phpunit/phpunit": "^9.6.22" - }, - "suggest": { - "php-standard-library/phpstan-extension": "PHPStan integration", - "php-standard-library/psalm-plugin": "Psalm integration" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/hhvm/hsl", - "name": "hhvm/hsl" - } - }, - "autoload": { - "files": [ - "src/bootstrap.php" - ], - "psr-4": { - "Psl\\": "src/Psl" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "azjezz", - "email": "azjezz@protonmail.com" - } - ], - "description": "PHP Standard Library", - "support": { - "issues": "https://github.com/azjezz/psl/issues", - "source": "https://github.com/azjezz/psl/tree/4.3.0" - }, - "funding": [ - { - "url": "https://github.com/azjezz", - "type": "github" - }, - { - "url": "https://github.com/veewee", - "type": "github" - } - ], - "time": "2026-02-24T01:58:53+00:00" - }, { "name": "maglnet/composer-require-checker", - "version": "4.21.0", + "version": "4.24.0", "source": { "type": "git", "url": "https://github.com/maglnet/ComposerRequireChecker.git", - "reference": "3f9bb5cb553412d72f02e1c38a3748fcc2b90f83" + "reference": "a15ec28d7a747109682c7774af665d2540516750" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maglnet/ComposerRequireChecker/zipball/3f9bb5cb553412d72f02e1c38a3748fcc2b90f83", - "reference": "3f9bb5cb553412d72f02e1c38a3748fcc2b90f83", + "url": "https://api.github.com/repos/maglnet/ComposerRequireChecker/zipball/a15ec28d7a747109682c7774af665d2540516750", + "reference": "a15ec28d7a747109682c7774af665d2540516750", "shasum": "" }, "require": { - "azjezz/psl": "^4.2.1", "composer-runtime-api": "^2.0.0", "ext-phar": "*", "nikic/php-parser": "^5.7.0", "php": "~8.4.0 || ~8.5.0", - "symfony/console": "^8.0.4", + "php-standard-library/php-standard-library": "^6.1.1", + "symfony/console": "^8.0.7", "webmozart/glob": "^4.7.0" }, "conflict": { @@ -113,14 +37,14 @@ "doctrine/coding-standard": "^14.0.0", "ext-zend-opcache": "*", "phing/phing": "^3.1.2", - "php-standard-library/phpstan-extension": "^2.0.2", + "php-standard-library/phpstan-extension": "^2.0.3", "php-standard-library/psalm-plugin": "^2.3", - "phpstan/phpstan": "^2.1.39", - "phpunit/phpunit": "^12.5.11", + "phpstan/phpstan": "^2.1.40", + "phpunit/phpunit": "^12.5.14", "psalm/plugin-phpunit": "^0.19.5", "roave/infection-static-analysis-plugin": "^1.43.0", "spatie/temporary-directory": "^2.3.1", - "vimeo/psalm": "^6.15.1" + "vimeo/psalm": "^6.16.1" }, "bin": [ "bin/composer-require-checker" @@ -165,9 +89,9 @@ ], "support": { "issues": "https://github.com/maglnet/ComposerRequireChecker/issues", - "source": "https://github.com/maglnet/ComposerRequireChecker/tree/4.21.0" + "source": "https://github.com/maglnet/ComposerRequireChecker/tree/4.24.0" }, - "time": "2026-02-16T09:56:29+00:00" + "time": "2026-03-20T12:51:42+00:00" }, { "name": "nikic/php-parser", @@ -227,6 +151,269 @@ }, "time": "2025-12-06T11:56:16+00:00" }, + { + "name": "php-standard-library/php-standard-library", + "version": "6.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/php-standard-library.git", + "reference": "b7d151cb1c21589cdde17a6adff50d3375de0919" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/php-standard-library/zipball/b7d151cb1c21589cdde17a6adff50d3375de0919", + "reference": "b7d151cb1c21589cdde17a6adff50d3375de0919", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.4.0 || ~8.5.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "replace": { + "php-standard-library/ansi": "self.version", + "php-standard-library/async": "self.version", + "php-standard-library/binary": "self.version", + "php-standard-library/cache": "self.version", + "php-standard-library/channel": "self.version", + "php-standard-library/cidr": "self.version", + "php-standard-library/class": "self.version", + "php-standard-library/collection": "self.version", + "php-standard-library/comparison": "self.version", + "php-standard-library/compression": "self.version", + "php-standard-library/crypto": "self.version", + "php-standard-library/data-structure": "self.version", + "php-standard-library/date-time": "self.version", + "php-standard-library/default": "self.version", + "php-standard-library/dict": "self.version", + "php-standard-library/either": "self.version", + "php-standard-library/encoding": "self.version", + "php-standard-library/env": "self.version", + "php-standard-library/file": "self.version", + "php-standard-library/filesystem": "self.version", + "php-standard-library/foundation": "self.version", + "php-standard-library/fun": "self.version", + "php-standard-library/graph": "self.version", + "php-standard-library/h2": "self.version", + "php-standard-library/hash": "self.version", + "php-standard-library/hpack": "self.version", + "php-standard-library/html": "self.version", + "php-standard-library/interface": "self.version", + "php-standard-library/interoperability": "self.version", + "php-standard-library/io": "self.version", + "php-standard-library/ip": "self.version", + "php-standard-library/iri": "self.version", + "php-standard-library/iter": "self.version", + "php-standard-library/json": "self.version", + "php-standard-library/locale": "self.version", + "php-standard-library/math": "self.version", + "php-standard-library/network": "self.version", + "php-standard-library/observer": "self.version", + "php-standard-library/option": "self.version", + "php-standard-library/os": "self.version", + "php-standard-library/password": "self.version", + "php-standard-library/process": "self.version", + "php-standard-library/promise": "self.version", + "php-standard-library/pseudo-random": "self.version", + "php-standard-library/punycode": "self.version", + "php-standard-library/random-sequence": "self.version", + "php-standard-library/range": "self.version", + "php-standard-library/regex": "self.version", + "php-standard-library/result": "self.version", + "php-standard-library/runtime": "self.version", + "php-standard-library/secure-random": "self.version", + "php-standard-library/shell": "self.version", + "php-standard-library/socks": "self.version", + "php-standard-library/str": "self.version", + "php-standard-library/tcp": "self.version", + "php-standard-library/terminal": "self.version", + "php-standard-library/tls": "self.version", + "php-standard-library/trait": "self.version", + "php-standard-library/tree": "self.version", + "php-standard-library/type": "self.version", + "php-standard-library/udp": "self.version", + "php-standard-library/unix": "self.version", + "php-standard-library/uri": "self.version", + "php-standard-library/url": "self.version", + "php-standard-library/vec": "self.version" + }, + "require-dev": { + "carthage-software/mago": "^1.15.2", + "ext-brotli": "*", + "infection/infection": "^0.32.6", + "php-coveralls/php-coveralls": "^2.9.1", + "phpbench/phpbench": "^1.5.1", + "phpunit/phpunit": "^13.0.5" + }, + "suggest": { + "php-standard-library/phpstan-extension": "PHPStan integration", + "php-standard-library/psalm-plugin": "Psalm integration" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.1.x-dev" + } + }, + "autoload": { + "files": [ + "packages/foundation/src/Psl/bootstrap.php", + "packages/ansi/src/Psl/bootstrap.php", + "packages/async/src/Psl/bootstrap.php", + "packages/binary/src/Psl/bootstrap.php", + "packages/cache/src/Psl/bootstrap.php", + "packages/channel/src/Psl/bootstrap.php", + "packages/class/src/Psl/bootstrap.php", + "packages/comparison/src/Psl/bootstrap.php", + "packages/compression/src/Psl/bootstrap.php", + "packages/crypto/src/Psl/bootstrap.php", + "packages/date-time/src/Psl/bootstrap.php", + "packages/dict/src/Psl/bootstrap.php", + "packages/encoding/src/Psl/bootstrap.php", + "packages/env/src/Psl/bootstrap.php", + "packages/file/src/Psl/bootstrap.php", + "packages/filesystem/src/Psl/bootstrap.php", + "packages/fun/src/Psl/bootstrap.php", + "packages/graph/src/Psl/bootstrap.php", + "packages/h2/src/Psl/bootstrap.php", + "packages/hash/src/Psl/bootstrap.php", + "packages/hpack/src/Psl/bootstrap.php", + "packages/html/src/Psl/bootstrap.php", + "packages/interface/src/Psl/bootstrap.php", + "packages/io/src/Psl/bootstrap.php", + "packages/iter/src/Psl/bootstrap.php", + "packages/json/src/Psl/bootstrap.php", + "packages/math/src/Psl/bootstrap.php", + "packages/network/src/Psl/bootstrap.php", + "packages/option/src/Psl/bootstrap.php", + "packages/os/src/Psl/bootstrap.php", + "packages/password/src/Psl/bootstrap.php", + "packages/pseudo-random/src/Psl/bootstrap.php", + "packages/punycode/src/Psl/bootstrap.php", + "packages/range/src/Psl/bootstrap.php", + "packages/regex/src/Psl/bootstrap.php", + "packages/result/src/Psl/bootstrap.php", + "packages/runtime/src/Psl/bootstrap.php", + "packages/secure-random/src/Psl/bootstrap.php", + "packages/shell/src/Psl/bootstrap.php", + "packages/socks/src/Psl/bootstrap.php", + "packages/str/src/Psl/bootstrap.php", + "packages/tcp/src/Psl/bootstrap.php", + "packages/terminal/src/Psl/bootstrap.php", + "packages/tls/src/Psl/bootstrap.php", + "packages/trait/src/Psl/bootstrap.php", + "packages/tree/src/Psl/bootstrap.php", + "packages/type/src/Psl/bootstrap.php", + "packages/udp/src/Psl/bootstrap.php", + "packages/unix/src/Psl/bootstrap.php", + "packages/uri/src/Psl/bootstrap.php", + "packages/url/src/Psl/bootstrap.php", + "packages/iri/src/Psl/bootstrap.php", + "packages/vec/src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\": "packages/foundation/src/Psl/", + "Psl\\H2\\": "packages/h2/src/Psl/H2/", + "Psl\\IO\\": "packages/io/src/Psl/IO/", + "Psl\\IP\\": "packages/ip/src/Psl/IP/", + "Psl\\OS\\": "packages/os/src/Psl/OS/", + "Psl\\Env\\": "packages/env/src/Psl/Env/", + "Psl\\Fun\\": "packages/fun/src/Psl/Fun/", + "Psl\\IRI\\": "packages/iri/src/Psl/IRI/", + "Psl\\Str\\": "packages/str/src/Psl/Str/", + "Psl\\TCP\\": "packages/tcp/src/Psl/TCP/", + "Psl\\TLS\\": "packages/tls/src/Psl/TLS/", + "Psl\\UDP\\": "packages/udp/src/Psl/UDP/", + "Psl\\URI\\": "packages/uri/src/Psl/URI/", + "Psl\\URL\\": "packages/url/src/Psl/URL/", + "Psl\\Vec\\": "packages/vec/src/Psl/Vec/", + "Psl\\Ansi\\": "packages/ansi/src/Psl/Ansi/", + "Psl\\CIDR\\": "packages/cidr/src/Psl/CIDR/", + "Psl\\Dict\\": "packages/dict/src/Psl/Dict/", + "Psl\\File\\": "packages/file/src/Psl/File/", + "Psl\\Hash\\": "packages/hash/src/Psl/Hash/", + "Psl\\Html\\": "packages/html/src/Psl/Html/", + "Psl\\Iter\\": "packages/iter/src/Psl/Iter/", + "Psl\\Json\\": "packages/json/src/Psl/Json/", + "Psl\\Math\\": "packages/math/src/Psl/Math/", + "Psl\\Tree\\": "packages/tree/src/Psl/Tree/", + "Psl\\Type\\": "packages/type/src/Psl/Type/", + "Psl\\Unix\\": "packages/unix/src/Psl/Unix/", + "Psl\\Async\\": "packages/async/src/Psl/Async/", + "Psl\\Cache\\": "packages/cache/src/Psl/Cache/", + "Psl\\Class\\": "packages/class/src/Psl/Class/", + "Psl\\Graph\\": "packages/graph/src/Psl/Graph/", + "Psl\\HPACK\\": "packages/hpack/src/Psl/HPACK/", + "Psl\\Range\\": "packages/range/src/Psl/Range/", + "Psl\\Regex\\": "packages/regex/src/Psl/Regex/", + "Psl\\Shell\\": "packages/shell/src/Psl/Shell/", + "Psl\\Socks\\": "packages/socks/src/Psl/Socks/", + "Psl\\Trait\\": "packages/trait/src/Psl/Trait/", + "Psl\\Binary\\": "packages/binary/src/Psl/Binary/", + "Psl\\Crypto\\": "packages/crypto/src/Psl/Crypto/", + "Psl\\Either\\": "packages/either/src/Psl/Either/", + "Psl\\Locale\\": "packages/locale/src/Psl/Locale/", + "Psl\\Option\\": "packages/option/src/Psl/Option/", + "Psl\\Result\\": "packages/result/src/Psl/Result/", + "Psl\\Channel\\": "packages/channel/src/Psl/Channel/", + "Psl\\Default\\": "packages/default/src/Psl/Default/", + "Psl\\Network\\": "packages/network/src/Psl/Network/", + "Psl\\Process\\": "packages/process/src/Psl/Process/", + "Psl\\Promise\\": "packages/promise/src/Psl/Promise/", + "Psl\\Runtime\\": "packages/runtime/src/Psl/Runtime/", + "Psl\\DateTime\\": "packages/date-time/src/Psl/DateTime/", + "Psl\\Encoding\\": "packages/encoding/src/Psl/Encoding/", + "Psl\\Observer\\": "packages/observer/src/Psl/Observer/", + "Psl\\Password\\": "packages/password/src/Psl/Password/", + "Psl\\Punycode\\": "packages/punycode/src/Psl/Punycode/", + "Psl\\Terminal\\": "packages/terminal/src/Psl/Terminal/", + "Psl\\Interface\\": "packages/interface/src/Psl/Interface/", + "Psl\\Collection\\": "packages/collection/src/Psl/Collection/", + "Psl\\Comparison\\": "packages/comparison/src/Psl/Comparison/", + "Psl\\Filesystem\\": "packages/filesystem/src/Psl/Filesystem/", + "Psl\\Compression\\": "packages/compression/src/Psl/Compression/", + "Psl\\PseudoRandom\\": "packages/pseudo-random/src/Psl/PseudoRandom/", + "Psl\\SecureRandom\\": "packages/secure-random/src/Psl/SecureRandom/", + "Psl\\DataStructure\\": "packages/data-structure/src/Psl/DataStructure/", + "Psl\\RandomSequence\\": "packages/random-sequence/src/Psl/RandomSequence/", + "Psl\\Interoperability\\": "packages/interoperability/src/Psl/Interoperability/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "azjezz", + "email": "azjezz@protonmail.com" + } + ], + "description": "PHP Standard Library", + "support": { + "issues": "https://github.com/php-standard-library/php-standard-library/issues", + "source": "https://github.com/php-standard-library/php-standard-library/tree/6.1.1" + }, + "funding": [ + { + "url": "https://github.com/azjezz", + "type": "github" + }, + { + "url": "https://github.com/veewee", + "type": "github" + } + ], + "time": "2026-03-20T08:09:20+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -354,16 +541,16 @@ }, { "name": "symfony/console", - "version": "v8.0.6", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "488285876e807a4777f074041d8bb508623419fa" + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/488285876e807a4777f074041d8bb508623419fa", - "reference": "488285876e807a4777f074041d8bb508623419fa", + "url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782", + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782", "shasum": "" }, "require": { @@ -420,7 +607,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.6" + "source": "https://github.com/symfony/console/tree/v8.0.11" }, "funding": [ { @@ -440,20 +627,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -466,7 +653,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -491,7 +678,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -502,25 +689,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -570,7 +761,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -590,20 +781,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -652,7 +843,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -672,11 +863,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -737,7 +928,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -761,16 +952,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -822,7 +1013,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -842,20 +1033,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -873,7 +1064,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -909,7 +1100,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -929,20 +1120,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v8.0.6", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff", + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff", "shasum": "" }, "require": { @@ -999,7 +1190,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/string/tree/v8.0.11" }, "funding": [ { @@ -1019,7 +1210,7 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "webmozart/glob", diff --git a/vendor-bin/tools/composer.lock b/vendor-bin/tools/composer.lock index d2ee0c2..4249662 100644 --- a/vendor-bin/tools/composer.lock +++ b/vendor-bin/tools/composer.lock @@ -319,16 +319,16 @@ }, { "name": "amphp/parallel", - "version": "v2.3.3", + "version": "v2.3.4", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" + "reference": "3ad45d1cff1bfbfe832c79671e6a4a1017dd9921" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", + "url": "https://api.github.com/repos/amphp/parallel/zipball/3ad45d1cff1bfbfe832c79671e6a4a1017dd9921", + "reference": "3ad45d1cff1bfbfe832c79671e6a4a1017dd9921", "shasum": "" }, "require": { @@ -348,7 +348,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -391,7 +391,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.3" + "source": "https://github.com/amphp/parallel/tree/v2.3.4" }, "funding": [ { @@ -399,7 +399,7 @@ "type": "github" } ], - "time": "2025-11-15T06:23:42+00:00" + "time": "2026-05-06T19:26:51+00:00" }, { "name": "amphp/parser", @@ -465,16 +465,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" + "reference": "a044733e080940d1483f56caff0c412ad6982776" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", + "reference": "a044733e080940d1483f56caff0c412ad6982776", "shasum": "" }, "require": { @@ -486,7 +486,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -520,7 +520,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + "source": "https://github.com/amphp/pipeline/tree/v1.2.4" }, "funding": [ { @@ -528,7 +528,7 @@ "type": "github" } ], - "time": "2025-03-16T16:33:53+00:00" + "time": "2026-05-06T05:37:57+00:00" }, { "name": "amphp/process", @@ -600,24 +600,27 @@ }, { "name": "amphp/serialization", - "version": "v1.0.0", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -652,22 +655,28 @@ ], "support": { "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" + "source": "https://github.com/amphp/serialization/tree/v1.1.0" }, - "time": "2020-03-25T21:39:07+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" }, { "name": "amphp/socket", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", "shasum": "" }, "require": { @@ -676,17 +685,17 @@ "amphp/dns": "^2", "ext-openssl": "*", "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", + "league/uri": "^7", + "league/uri-interfaces": "^7", "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" + "revolt/event-loop": "^1" }, "require-dev": { "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "amphp/process": "^2", "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -730,7 +739,7 @@ ], "support": { "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" + "source": "https://github.com/amphp/socket/tree/v2.4.0" }, "funding": [ { @@ -738,7 +747,7 @@ "type": "github" } ], - "time": "2024-04-21T14:33:03+00:00" + "time": "2026-04-19T15:09:56+00:00" }, { "name": "amphp/sync", @@ -1135,16 +1144,16 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", "shasum": "" }, "require": { @@ -1227,7 +1236,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-11T04:32:07+00:00" + "time": "2026-05-06T08:26:05+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -1594,20 +1603,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -1680,7 +1689,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -1688,20 +1697,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -1764,7 +1773,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -1772,7 +1781,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "netresearch/jsonmapper", @@ -2056,16 +2065,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -2115,9 +2124,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-01T18:43:49+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2183,12 +2192,12 @@ "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "40b34162767e86b78f894124bd7c4cb03ff27ad4" + "reference": "3cf305bd4165087c2950f063ef216704a3fe8828" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/40b34162767e86b78f894124bd7c4cb03ff27ad4", - "reference": "40b34162767e86b78f894124bd7c4cb03ff27ad4", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/3cf305bd4165087c2950f063ef216704a3fe8828", + "reference": "3cf305bd4165087c2950f063ef216704a3fe8828", "shasum": "" }, "require": { @@ -2200,15 +2209,15 @@ "symfony/yaml": "^5.4.31 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "brianium/paratest": "^7.3", + "brianium/paratest": "~7.3.2|^7.8.5", "easy-doc/easy-doc": "^1.3.2", "ext-json": "*", "ext-simplexml": "*", "friendsofphp/php-cs-fixer": "^3.57", "gregwar/rst": "^1.0", "mikey179/vfsstream": "^1.6.8", - "phpstan/phpstan": "~2.1.38", - "phpunit/phpunit": "^10.5.20,<10.5.32|^11.0.0", + "phpstan/phpstan": "~2.1.44", + "phpunit/phpunit": "^10.5.62|^11.5.50", "squizlabs/php_codesniffer": "^3.8.0" }, "bin": [ @@ -2264,7 +2273,7 @@ "type": "tidelift" } ], - "time": "2026-02-15T12:53:29+00:00" + "time": "2026-05-14T05:08:51+00:00" }, { "name": "phpmetrics/phpmetrics", @@ -2392,11 +2401,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.54", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", "shasum": "" }, "require": { @@ -2441,11 +2450,11 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-04-29T13:31:09+00:00" }, { "name": "psalm/plugin-phpunit", - "version": "0.19.5", + "version": "0.19.7", "source": { "type": "git", "url": "https://github.com/psalm/psalm-plugin-phpunit.git", @@ -2497,7 +2506,7 @@ "description": "Psalm plugin for PHPUnit", "support": { "issues": "https://github.com/psalm/psalm-plugin-phpunit/issues", - "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.19.5" + "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.19.7" }, "time": "2025-03-31T18:49:55+00:00" }, @@ -3011,16 +3020,16 @@ }, { "name": "sebastian/diff", - "version": "8.0.0", + "version": "8.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3" + "reference": "cce1bb200e0062e72f9b85ccfe54d3fd38bbd044" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2b6d09d7729ee87d605a439469f9dcc39be5ea3", - "reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cce1bb200e0062e72f9b85ccfe54d3fd38bbd044", + "reference": "cce1bb200e0062e72f9b85ccfe54d3fd38bbd044", "shasum": "" }, "require": { @@ -3033,7 +3042,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "8.2-dev" } }, "autoload": { @@ -3066,7 +3075,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/8.2.1" }, "funding": [ { @@ -3086,7 +3095,7 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:42:27+00:00" + "time": "2026-05-14T05:24:37+00:00" }, { "name": "slevomat/coding-standard", @@ -3302,16 +3311,16 @@ }, { "name": "symfony/config", - "version": "v8.0.6", + "version": "v8.0.10", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "94ea198de42f93dffa920a098cac3961a82e63b7" + "reference": "de665e669412ec2effe004d90298dbbdaf6e7e8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/94ea198de42f93dffa920a098cac3961a82e63b7", - "reference": "94ea198de42f93dffa920a098cac3961a82e63b7", + "url": "https://api.github.com/repos/symfony/config/zipball/de665e669412ec2effe004d90298dbbdaf6e7e8b", + "reference": "de665e669412ec2effe004d90298dbbdaf6e7e8b", "shasum": "" }, "require": { @@ -3356,7 +3365,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v8.0.6" + "source": "https://github.com/symfony/config/tree/v8.0.10" }, "funding": [ { @@ -3376,20 +3385,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-05-04T13:41:39+00:00" }, { "name": "symfony/console", - "version": "v8.0.6", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "488285876e807a4777f074041d8bb508623419fa" + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/488285876e807a4777f074041d8bb508623419fa", - "reference": "488285876e807a4777f074041d8bb508623419fa", + "url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782", + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782", "shasum": "" }, "require": { @@ -3446,7 +3455,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.6" + "source": "https://github.com/symfony/console/tree/v8.0.11" }, "funding": [ { @@ -3466,20 +3475,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "symfony/dependency-injection", - "version": "v8.0.6", + "version": "v8.0.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "edd98864a7b9eaaa10f389bd414e7d9e816bb59d" + "reference": "6fc374dae45a7633a5865da7fc2908baf29d4900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/edd98864a7b9eaaa10f389bd414e7d9e816bb59d", - "reference": "edd98864a7b9eaaa10f389bd414e7d9e816bb59d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6fc374dae45a7633a5865da7fc2908baf29d4900", + "reference": "6fc374dae45a7633a5865da7fc2908baf29d4900", "shasum": "" }, "require": { @@ -3527,7 +3536,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v8.0.6" + "source": "https://github.com/symfony/dependency-injection/tree/v8.0.10" }, "funding": [ { @@ -3547,20 +3556,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-05-06T11:55:35+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -3573,7 +3582,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3598,7 +3607,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -3609,25 +3618,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/filesystem", - "version": "v8.0.6", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + "reference": "224db910898ce1317b892a9a1338f1f8f17eb7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/224db910898ce1317b892a9a1338f1f8f17eb7c7", + "reference": "224db910898ce1317b892a9a1338f1f8f17eb7c7", "shasum": "" }, "require": { @@ -3664,7 +3677,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.11" }, "funding": [ { @@ -3684,20 +3697,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-05-11T16:39:47+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -3747,7 +3760,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -3767,20 +3780,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -3829,7 +3842,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -3849,11 +3862,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3914,7 +3927,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -3938,16 +3951,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -3999,7 +4012,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -4019,20 +4032,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { @@ -4079,7 +4092,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" }, "funding": [ { @@ -4099,20 +4112,20 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -4130,7 +4143,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4166,7 +4179,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -4186,20 +4199,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v8.0.6", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff", + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff", "shasum": "" }, "require": { @@ -4256,7 +4269,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/string/tree/v8.0.11" }, "funding": [ { @@ -4276,20 +4289,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "symfony/var-exporter", - "version": "v8.0.0", + "version": "v8.0.9", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" + "reference": "24cf67be4dd0926e4413635418682f4fff831412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", - "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/24cf67be4dd0926e4413635418682f4fff831412", + "reference": "24cf67be4dd0926e4413635418682f4fff831412", "shasum": "" }, "require": { @@ -4336,7 +4349,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.9" }, "funding": [ { @@ -4356,20 +4369,20 @@ "type": "tidelift" } ], - "time": "2025-11-05T18:53:00+00:00" + "time": "2026-04-18T13:51:42+00:00" }, { "name": "symfony/yaml", - "version": "v8.0.6", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0" + "reference": "48046fbd5567bd1717f278eaa2cfc3131f489984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0", - "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/48046fbd5567bd1717f278eaa2cfc3131f489984", + "reference": "48046fbd5567bd1717f278eaa2cfc3131f489984", "shasum": "" }, "require": { @@ -4411,7 +4424,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.6" + "source": "https://github.com/symfony/yaml/tree/v8.0.11" }, "funding": [ { @@ -4431,20 +4444,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "vimeo/psalm", - "version": "6.15.1", + "version": "6.16.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9" + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/28dc127af1b5aecd52314f6f645bafc10d0e11f9", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", "shasum": "" }, "require": { @@ -4549,20 +4562,20 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2026-02-07T19:27:16+00:00" + "time": "2026-03-19T10:56:09+00:00" }, { "name": "webmozart/assert", - "version": "2.1.6", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "shasum": "" }, "require": { @@ -4609,9 +4622,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.6" + "source": "https://github.com/webmozarts/assert/tree/2.3.0" }, - "time": "2026-02-27T10:28:38+00:00" + "time": "2026-04-11T10:33:05+00:00" } ], "aliases": [],