ブログ記事とコメントシステムを例に、フォームデータがどのようにRay.InputQuery、Ray.MediaQuery、Koriym.CsvEntitiesを通じて処理され、最終的にテンプレートで表示されるまでの流れを示します。
[Webフォーム]
↓ POST /articles/123/comments
[Ray.InputQuery]
↓ CommentInputオブジェクト生成
[BEAR.Resource / Controller]
↓ ビジネスロジック実行
[Ray.MediaQuery]
↓ データベース操作
[Koriym.CsvEntities]
↓ 1対多の関係を効率的に取得
[Twigテンプレート]
↓ 表示
[ユーザー]
<!-- article.html.twig -->
<article>
<h1>{{ article.title }}</h1>
<div>{{ article.content }}</div>
<!-- コメント投稿フォーム -->
<form action="/articles/{{ article.id }}/comments" method="POST">
<h3>Leave a Comment</h3>
<!-- フラットな名前付け -->
<input type="text" name="author_name" placeholder="Your name" required>
<input type="email" name="author_email" placeholder="Your email" required>
<input type="url" name="author_website" placeholder="Website (optional)">
<textarea name="content" placeholder="Your comment" required></textarea>
<!-- 返信の場合 -->
<input type="hidden" name="parent_comment_id" value="">
<!-- 評価 -->
<select name="rating">
<option value="">No rating</option>
<option value="1">1 star</option>
<option value="2">2 stars</option>
<option value="3">3 stars</option>
<option value="4">4 stars</option>
<option value="5">5 stars</option>
</select>
<button type="submit">Post Comment</button>
</form>
</article>use Ray\InputQuery\Attribute\Input;
use Ray\Di\Di\Named;
final class CommentInput
{
public function __construct(
#[Input] public readonly string $content,
#[Input] public readonly AuthorInput $author,
#[Input] public readonly ?string $parentCommentId = null,
#[Input] public readonly ?int $rating = null
) {}
}
final class AuthorInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email,
#[Input] public readonly ?string $website = null
) {}
}use BEAR\Resource\ResourceObject;
use Ray\MediaQuery\Annotation\DbQuery;
class Comment extends ResourceObject
{
public function __construct(
private CommentAddInterface $commentAdd,
private NotificationService $notifier // DIから注入
) {}
public function onPost(string $articleId, CommentInput $comment): static
{
// Ray.InputQueryが内部で自動的に以下を実行:
// 1. メソッドのパラメータを解析
// 2. CommentInputパラメータに#[Input]があることを検出
// 3. クエリーからCommentInputオブジェクトを生成
// - author_name, author_email → AuthorInputオブジェクト
// - content, rating, parent_comment_id → CommentInputのプロパティ
$commentId = $this->commentAdd->add($articleId, $comment);
// 通知サービス(DIから注入されたもの)を使用
$this->notifier->notifyNewComment($articleId, $commentId);
$this->code = 201;
$this->headers['Location'] = "/articles/{$articleId}#comment-{$commentId}";
return $this;
}
}interface CommentAddInterface
{
#[DbQuery('comment_add')]
public function add(string $articleId, CommentInput $comment): string;
}-- comment_add.sql
INSERT INTO comments (
article_id,
content,
author_name,
author_email,
author_website,
parent_comment_id,
rating,
created_at
) VALUES (
:articleId,
:content,
:authorName, -- CommentInput->author->name が自動展開
:authorEmail, -- CommentInput->author->email が自動展開
:authorWebsite, -- CommentInput->author->website が自動展開
:parentCommentId,
:rating,
NOW()
);
SELECT LAST_INSERT_ID() as id;interface ArticleDetailInterface
{
#[DbQuery('article_with_comments')]
public function get(string $articleId): ArticleDetail;
}
final class ArticleDetail
{
/** @var Comment[] */
public array $comments;
public function __construct(
public readonly string $id,
public readonly string $title,
public readonly string $content,
public readonly string $authorName,
public readonly DateTime $publishedAt,
?string $commentIds,
?string $commentContents,
?string $commentAuthorNames,
?string $commentAuthorEmails,
?string $commentRatings,
?string $commentCreatedAts
) {
// Koriym.CsvEntitiesで1対多の関係を構築
$this->comments = (new CsvEntities())(
Comment::class,
$commentIds,
$commentContents,
$commentAuthorNames,
$commentAuthorEmails,
$commentRatings,
$commentCreatedAts
);
}
}
final class Comment
{
public function __construct(
public readonly string $id,
public readonly string $content,
public readonly string $authorName,
public readonly string $authorEmail,
public readonly ?int $rating,
public readonly DateTime $createdAt
) {}
}-- article_with_comments.sql
SELECT
a.id,
a.title,
a.content,
a.author_name,
a.published_at,
GROUP_CONCAT(c.id ORDER BY c.created_at) as comment_ids,
GROUP_CONCAT(c.content ORDER BY c.created_at) as comment_contents,
GROUP_CONCAT(c.author_name ORDER BY c.created_at) as comment_author_names,
GROUP_CONCAT(c.author_email ORDER BY c.created_at) as comment_author_emails,
GROUP_CONCAT(c.rating ORDER BY c.created_at) as comment_ratings,
GROUP_CONCAT(c.created_at ORDER BY c.created_at) as comment_created_ats
FROM articles a
LEFT JOIN comments c ON c.article_id = a.id
WHERE a.id = :articleId
GROUP BY a.id;class Article extends ResourceObject
{
public function __construct(
private ArticleDetailInterface $articleDetail
) {}
public function onGet(string $id): static
{
$article = $this->articleDetail->get($id);
$this->body = [
'article' => $article,
'commentCount' => count($article->comments),
'averageRating' => $this->calculateAverageRating($article->comments)
];
return $this;
}
private function calculateAverageRating(array $comments): ?float
{
$ratings = array_filter(
array_column($comments, 'rating'),
fn($rating) => $rating !== null
);
return $ratings ? array_sum($ratings) / count($ratings) : null;
}
}{# article_detail.html.twig #}
<!DOCTYPE html>
<html>
<head>
<title>{{ article.title }}</title>
</head>
<body>
<article>
<header>
<h1>{{ article.title }}</h1>
<p>By {{ article.authorName }} on {{ article.publishedAt|date('F j, Y') }}</p>
</header>
<div class="content">
{{ article.content|markdown }}
</div>
<section class="comments">
<h2>Comments ({{ commentCount }})</h2>
{% if averageRating %}
<p>Average Rating: {{ averageRating|round(1) }} / 5</p>
{% endif %}
{% for comment in article.comments %}
<article class="comment" id="comment-{{ comment.id }}">
<header>
<strong>{{ comment.authorName }}</strong>
<time>{{ comment.createdAt|date('F j, Y g:i A') }}</time>
{% if comment.rating %}
<span class="rating">{{ '⭐'|repeat(comment.rating) }}</span>
{% endif %}
</header>
<div>{{ comment.content|nl2br }}</div>
</article>
{% endfor %}
{# コメントフォームをインクルード #}
{% include 'comment_form.html.twig' with {'articleId': article.id} %}
</section>
</article>
</body>
</html>- フォーム送信:
author_name=John&author_email=john@example.com&content=Great! - Ray.InputQuery:
getArguments()でメソッドの引数リストを生成- フラットなデータ →
CommentInput { author: AuthorInput { ... } }
- Ray.MediaQuery: オブジェクト → 自動展開してSQL実行
- レスポンス: 201 Created
- リクエスト: GET /articles/123
- Ray.MediaQuery: SQL実行(GROUP_CONCAT使用)
- Koriym.CsvEntities: CSV形式のデータ →
Comment[]配列 - Twig: オブジェクトのプロパティに自然にアクセス
- 型安全性: フォームからDBまで一貫した型チェック
- N+1問題の回避: GROUP_CONCATで1クエリで取得
- シンプルなテンプレート: オブジェクトとして自然にアクセス
- 保守性: SQLが明確で、データフローが追跡しやすい
- パフォーマンス: 最小限のクエリで必要なデータを取得