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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 119 additions & 1 deletion docs/tutorial/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ docs/tutorial/src/
│ ├── ArticleStats.php
│ ├── ArticleStatsFactory.php # DI ファクトリ
│ ├── MarkdownExcerpter.php # ファクトリへの注入対象
│ ├── AuthorProfile.php # `age` はカラムではない
│ ├── AuthorProfileFactory.php # クロックを注入する DI ファクトリ
│ ├── AuthorQueryInterface.php
│ ├── ArticleSearchResult.php # SELECT 用 PostQueryInterface
│ ├── CreatedArticle.php # DML + SELECT 用 PostQueryInterface
│ └── Exception/
Expand All @@ -104,6 +107,7 @@ docs/tutorial/src/
├── article_search.sql
├── article_stats.sql
├── article_stats_paginated.sql
├── author_profile.sql
├── comment_add.sql
└── comment_list.sql
```
Expand Down Expand Up @@ -1066,6 +1070,119 @@ comments=2, first body='Great post!' (id=1)
- これが Ray.MediaQuery を「単なるクエリマッパー」と区別する点 — **SQL の結果に対してドメイン処理を効率良く適用できる**。
- Business Domain Repository (BDR) パターンは [BDR_PATTERN-ja.md](https://github.com/ray-di/Ray.MediaQuery/blob/1.x/BDR_PATTERN-ja.md) で詳述。

### BDR の核心: DI が不可欠な例

`age`(年齢)はデータベースのカラムではない。`birth_date` と「現在時刻」の2つから計算する。現在時刻はファクトリの外から注入するしかない — `DateTimeInterface` として DI で受け取る。

`mywork/schema.sql` に `author` テーブルを追加:

```sql
CREATE TABLE IF NOT EXISTS author (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
birth_date TEXT NOT NULL
);
```

`sql/author_profile.sql`:

```sql
SELECT
id,
name,
birth_date
FROM author
WHERE id = :id;
```

`Blog/AuthorProfile.php`:

```php
final class AuthorProfile
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $birthDate,
public readonly int $age, // カラムではない — ファクトリが計算する
) {}
}
```

`Blog/AuthorProfileFactory.php`:

```php
use DateTimeImmutable;
use DateTimeInterface;

final class AuthorProfileFactory
{
public function __construct(
private readonly DateTimeInterface $now,
) {}

public function factory(int $id, string $name, string $birthDate): AuthorProfile
{
$age = (new DateTimeImmutable($birthDate))->diff($this->now)->y;

return new AuthorProfile(
id: $id,
name: $name,
birthDate: $birthDate,
age: $age,
);
}
}
```

`Blog/AuthorQueryInterface.php`:

```php
<?php

declare(strict_types=1);

namespace MyBlog;

use Ray\MediaQuery\Annotation\DbQuery;

interface AuthorQueryInterface
{
#[DbQuery('author_profile', type: 'row', factory: AuthorProfileFactory::class)]
public function profile(int $id): AuthorProfile|null;
}
```

`Queries::fromClasses()` に `AuthorQueryInterface::class` を追加する。`AuthorProfileFactory` 自体の bind は不要 — Ray.Di がインスタンス化し、コンストラクタ引数も解決する。

`DateTimeInterface` は `MediaQueryModule` 内部で既に `DateTimeImmutable` に bind されており、現在時刻に解決される。このサンプルの `age` を再現可能にするため、チュートリアルでは Module でクロックを固定する:

```php
$this->bind(DateTimeInterface::class)->toInstance(new DateTimeImmutable('2026-06-06'));
```

`run.php` でデータを挿入して呼び出す:

```php
$pdo->perform('INSERT INTO author (name, birth_date) VALUES (?, ?)', ['Alice', '1990-06-15']);

/** @var AuthorQueryInterface $authorQuery */
$authorQuery = $injector->getInstance(AuthorQueryInterface::class);
$profile = $authorQuery->profile(1);
assert($profile !== null);
printf("name=%s birth_date=%s age=%d\n", $profile->name, $profile->birthDate, $profile->age);
```

### 期待出力 (第8章 / BDR フォーカス / 統合 run.php)

```text
name=Alice birth_date=1990-06-15 age=35
```

> `age` はクエリ境界でファクトリが `birth_date` と注入された `DateTimeInterface $now` から計算する。Module でクロックを `2026-06-06` に固定しているため `age` は再現可能な `35`(その年の6月15日の誕生日をまだ迎えていない)。固定を外せば `MediaQueryModule` の既定の `DateTimeImmutable` bind が実際の現在時刻に解決され、`age` は今日の日付を反映する。

コントローラーとテンプレートは `$profile->age` と書くだけで値が手に入る — クエリ境界の外での計算はゼロ。これが BDR の本質: **エンティティは受け取った時点で完成している**。

---

## 第9章: UPDATE / DELETE と影響行数
Expand Down Expand Up @@ -1277,7 +1394,7 @@ echo $page1->data[0]->title, "\n";
```text
total items=31
page 1 has 10 items, hasNext=yes
Hello (edited)
Hello, Ray.MediaQuery (edited)
```

### Step 4. Ray.MediaQuery 1.1: Pager と factory を組み合わせる
Expand Down Expand Up @@ -1749,6 +1866,7 @@ Ray.MediaQuery は、その Read 側を Query-first に分割する。`UserRepos

### 次に読むもの

- [BDR パターン集](https://ray-di.github.io/Ray.MediaQuery/tutorial/bdr-patterns/ja/) — 行ごとの加工(`factory:`)と結果セット全体の成形(`PostQueryInterface`): バッジ・enum・JOINグルーピング・ソート・SPLイテレータ・Null Object
- [BDR Pattern Guide 日本語版](https://github.com/ray-di/Ray.MediaQuery/blob/1.x/BDR_PATTERN-ja.md) — ファクトリパターンとドメインオブジェクトの設計
- [Manual](https://ray-di.github.io/Ray.MediaQuery/reference/) — マニュアル (`#[Input]` Object Flattening, `SqlQueryInterface` 直接実行などの応用)
- [llms-full.txt](../llms-full.txt) — AI エージェント向けの圧縮リファレンス
Expand Down
124 changes: 122 additions & 2 deletions docs/tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,13 @@ docs/tutorial/src/
| |-- ArticleStats.php
| |-- ArticleStatsFactory.php # DI factory
| |-- MarkdownExcerpter.php # Injected into the factory
| |-- AuthorProfile.php # `age` is not a column
| |-- AuthorProfileFactory.php # DI factory with an injected clock
| |-- AuthorQueryInterface.php
| |-- ArticleSearchResult.php # SELECT PostQueryInterface
| `-- CreatedArticle.php # DML + SELECT PostQueryInterface
| |-- CreatedArticle.php # DML + SELECT PostQueryInterface
| `-- Exception/
| `-- UnexpectedRowException.php # Domain exception thrown by result classes
`-- sql/
|-- article_add.sql
|-- article_create_and_get.sql
Expand All @@ -102,6 +107,7 @@ docs/tutorial/src/
|-- article_search.sql
|-- article_stats.sql
|-- article_stats_paginated.sql
|-- author_profile.sql
|-- comment_add.sql
`-- comment_list.sql
```
Expand Down Expand Up @@ -1064,6 +1070,119 @@ comments=2, first body='Great post!' (id=1)
- This is what distinguishes Ray.MediaQuery from a simple query mapper: **domain processing can be applied efficiently at the SQL result boundary**.
- The Business Domain Repository pattern is described in [BDR_PATTERN.md](https://github.com/ray-di/Ray.MediaQuery/blob/1.x/BDR_PATTERN.md).

### BDR focus: why DI is necessary

`age` is not a database column. It requires two inputs: the stored `birth_date` and the current time. The current time must come from outside the factory — injected as `DateTimeInterface`.

Add an `author` table to `mywork/schema.sql`:

```sql
CREATE TABLE IF NOT EXISTS author (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
birth_date TEXT NOT NULL
);
```

`sql/author_profile.sql`:

```sql
SELECT
id,
name,
birth_date
FROM author
WHERE id = :id;
```

`Blog/AuthorProfile.php`:

```php
final class AuthorProfile
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $birthDate,
public readonly int $age, // not a column — computed by the factory
) {}
}
```

`Blog/AuthorProfileFactory.php`:

```php
use DateTimeImmutable;
use DateTimeInterface;

final class AuthorProfileFactory
{
public function __construct(
private readonly DateTimeInterface $now,
) {}

public function factory(int $id, string $name, string $birthDate): AuthorProfile
{
$age = (new DateTimeImmutable($birthDate))->diff($this->now)->y;

return new AuthorProfile(
id: $id,
name: $name,
birthDate: $birthDate,
age: $age,
);
}
}
```

`Blog/AuthorQueryInterface.php`:

```php
<?php

declare(strict_types=1);

namespace MyBlog;

use Ray\MediaQuery\Annotation\DbQuery;

interface AuthorQueryInterface
{
#[DbQuery('author_profile', type: 'row', factory: AuthorProfileFactory::class)]
public function profile(int $id): AuthorProfile|null;
}
```

Add `AuthorQueryInterface::class` to `Queries::fromClasses()`. `AuthorProfileFactory` itself needs no binding — Ray.Di instantiates it and resolves its constructor arguments.

`DateTimeInterface` is already bound to `DateTimeImmutable` inside `MediaQueryModule`, so it resolves to the current time. To keep this sample's `age` reproducible, the tutorial pins the clock in the Module:

```php
$this->bind(DateTimeInterface::class)->toInstance(new DateTimeImmutable('2026-06-06'));
```

Seed an author and call `profile()` in `run.php`:

```php
$pdo->perform('INSERT INTO author (name, birth_date) VALUES (?, ?)', ['Alice', '1990-06-15']);

/** @var AuthorQueryInterface $authorQuery */
$authorQuery = $injector->getInstance(AuthorQueryInterface::class);
$profile = $authorQuery->profile(1);
assert($profile !== null);
printf("name=%s birth_date=%s age=%d\n", $profile->name, $profile->birthDate, $profile->age);
```

### Expected Output (chapter 8 / BDR focus / integrated run.php)

```text
name=Alice birth_date=1990-06-15 age=35
```

> `age` is computed at the query boundary from `birth_date` and the injected `DateTimeInterface $now`. Because the Module pins the clock to `2026-06-06`, `age` is a reproducible `35` (the June 15 birthday has not yet passed that year). Remove the pin and `MediaQueryModule`'s default `DateTimeImmutable` binding resolves to the real current time, so `age` tracks today's date.

The controller and template write `$profile->age` and receive a ready value — no calculation outside the query boundary. This is BDR: the entity arrives complete.

---

## Chapter 9: UPDATE / DELETE and Affected Row Counts
Expand Down Expand Up @@ -1275,7 +1394,7 @@ echo $page1->data[0]->title, "\n";
```text
total items=31
page 1 has 10 items, hasNext=yes
Hello (edited)
Hello, Ray.MediaQuery (edited)
```

### Step 4. Ray.MediaQuery 1.1: Combine Pager and Factory
Expand Down Expand Up @@ -1745,6 +1864,7 @@ This hands-on tutorial focuses on understanding application Query contracts buil

### Next Reading

- [BDR Pattern Cookbook](https://ray-di.github.io/Ray.MediaQuery/tutorial/bdr-patterns/) - per-row enrichment (`factory:`) vs. whole-result-set shaping (`PostQueryInterface`): badges, enums, JOIN grouping, sorting, SPL iterators, Null Object
- [BDR Pattern Guide](https://github.com/ray-di/Ray.MediaQuery/blob/1.x/BDR_PATTERN.md) - factory pattern and domain object design
- [Manual 日本語版](https://ray-di.github.io/Ray.MediaQuery/reference/) - advanced feature reference, including `#[Input]` Object Flattening and direct `SqlQueryInterface` execution
- [llms-full.txt](../llms-full.txt) - compact reference for AI agents
Expand Down
Loading
Loading