Ray.InputQueryは、フラットなクエリーデータから型安全なPHPオブジェクトを生成する基盤ライブラリです。単体で動作し、Ray.MediaQueryやBEAR.Resourceなど他のライブラリから利用されることを前提に設計されています。
HTTPリクエストやフォームデータなどの外部入力を、アプリケーションが扱いやすい型付きオブジェクトに変換することで、型安全性を外部境界まで拡張します。
[HTTPリクエスト/クエリー]
↓ (フラットな構造)
[Ray.InputQuery]
↓ (Inputオブジェクト生成)
[アプリケーション層]
├→ [BEAR.Resource] → ResourceObject
└→ [Ray.MediaQuery] → Database
POST /articles/123/comments
author_name=John&author_email=john@example.com&content=Great!
↓
Ray.InputQuery
↓
CommentInput {
content: "Great!",
author: AuthorInput {
name: "John",
email: "john@example.com"
}
}
↓
アプリケーションロジック
// 型安全性の欠如
public function createArticle(Request $request)
{
$title = $request->input('title'); // string? null? array?
$authorName = $request->input('author_name');
$authorEmail = $request->input('author_email');
// 手動でのバリデーションと変換
if (empty($title)) {
throw new ValidationException('Title is required');
}
// 構造が不明確
$author = new Author($authorName, $authorEmail);
$article = new Article($title, $content, $author);
}// 型安全で構造が明確
public function createArticle(ArticleInput $input)
{
// すでに型チェック済み、構造化済み
return $this->repository->save(
Article::fromInput($input)
);
}Ray.InputQueryは「入力を第一級市民として扱う」という設計思想に基づいています:
- 一方向性: 外部から内部への入力専用
- 構造の表現: フォームやAPIの構造をそのまま反映
- 一時性: 処理の入口でのみ存在
- 組み合わせ可能: 小さなInputを組み合わせて複雑な入力を表現
namespace Ray\InputQuery;
interface InputQueryInterface
{
/**
* メソッドの引数リストを取得
*
* @param \ReflectionMethod $method
* @param array<string, mixed> $query
* @return array<int, mixed> Position-based arguments
*/
public function getArguments(\ReflectionMethod $method, array $query): array;
/**
* Inputオブジェクトを生成
*
* @param class-string $class
* @param array<string, mixed> $query
* @return object
*/
public function create(string $class, array $query): object;
}namespace Ray\InputQuery;
use Ray\Di\InjectorInterface;
final class InputQuery implements InputQueryInterface
{
public function __construct(
private InjectorInterface $injector
) {}
public function getArguments(\ReflectionMethod $method, array $query): array
{
// メソッドの引数を解析し、引数リストを生成
$args = [];
foreach ($method->getParameters() as $param) {
$args[] = $this->resolveParameter($param, $query);
}
return $args;
}
public function create(string $class, array $query): object
{
// クラスのコンストラクタを解析し、オブジェクトを生成
$reflection = new \ReflectionClass($class);
$constructor = $reflection->getConstructor();
if (!$constructor) {
return new $class();
}
$args = $this->getArguments($constructor, $query);
return $reflection->newInstanceArgs($args);
}
}namespace Ray\InputQuery\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_PARAMETER)]
final class Input
{
}Input属性はパラメータレベルで使用し、そのパラメータがクエリーデータから取得されることを示します:
final class ExampleInput
{
public function __construct(
#[Input] public readonly string $name, // クエリーから
#[Input] public readonly ?int $age, // クエリーから(nullable)
#[Input] public readonly UserInput $user, // クエリーから(ネスト)
private LoggerInterface $logger // DIから(#[Input]なし)
) {}
}- クラスレベルではなくパラメータレベルで属性を使用
- これにより、同一クラス内でクエリーデータとDIの両方を受け取ることが可能
- データの出所が明確になり、保守性が向上
すべてのキーをキャメルケースに統一:
user_name→userNameuser-name→userNameUserName→userName
final class ExampleInput
{
public function __construct(
#[Input] public readonly string $title, // クエリーから
#[Input] public readonly ?string $description, // クエリーから
#[Input] public readonly UserInput $user, // クエリーから(ネスト)
#[Named('app.timezone')] private string $zone, // DIから
private LoggerInterface $logger // DIから
) {}
}シンプルなルール:
#[Input]→ クエリーから(スカラー・オブジェクト問わず)- それ以外 → DIから
// フラットなクエリー
[
'title' => 'Buy milk',
'assigneeId' => '123',
'assigneeName' => 'John',
'assigneeEmail' => 'john@example.com'
]
// assigneeプレフィックスを持つキーを検出し、UserInputを構築
UserInput(
id: '123',
name: 'John',
email: 'john@example.com'
)-
パラメータの#[Input]属性を確認
- ある場合 → クエリーから取得
- ない場合 → DIから取得
-
クエリーからの取得(#[Input]がある場合)
- スカラー型 → 直接取得・型変換
- オブジェクト型 → ネスト解決
-
DIからの取得(#[Input]がない場合)
- オブジェクト → インジェクターから取得
- スカラー → #[Named]があれば名前付きバインディング
- 必須パラメータが見つからない場合は、型のデフォルト値やnullを使用
- 型変換エラーは適切に処理
- リフレクションの結果はキャッシュ可能な設計に
- 深い再帰を避ける(実用上3階層程度まで)
- 将来的なJSON Schema統合を考慮した設計
- カスタムコンバーターを追加可能な構造
public function testCreateObject(): void
{
$injector = new Injector();
$inputQuery = new InputQuery($injector);
$query = [
'name' => 'John',
'email' => 'john@example.com'
];
$user = $inputQuery->create(UserInput::class, $query);
assert($user->name === 'John');
assert($user->email === 'john@example.com');
}
public function testGetArguments(): void
{
$inputQuery = new InputQuery(new Injector());
$method = new \ReflectionMethod(TodoController::class, 'create');
$query = [
'title' => 'Buy milk',
'assignee_id' => '123',
'assignee_name' => 'John'
];
$args = $inputQuery->getArguments($method, $query);
assert($args[0] instanceof TodoInput);
assert($args[0]->title === 'Buy milk');
}public function testNestedInput(): void
{
$inputQuery = new InputQuery(new Injector());
$query = [
'title' => 'Buy milk',
'assignee_id' => '123',
'assignee_name' => 'John',
'assignee_email' => 'john@example.com'
];
$todo = $inputQuery->create(TodoCreateInput::class, $query);
assert($todo->title === 'Buy milk');
assert($todo->assignee instanceof UserInput);
assert($todo->assignee->id === '123');
}// HTTPリクエスト
POST /users
name=John+Doe&email=john@example.com
// Inputクラス
final class UserInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email
) {}
}
// 使用
$user = $inputQuery->create(UserInput::class, $_POST);
echo $user->name; // "John Doe"
echo $user->email; // "john@example.com"// HTTPリクエスト
POST /articles
title=Hello+World&content=Lorem+ipsum&author_name=John&author_email=john@example.com
// Inputクラス
final class ArticleInput
{
public function __construct(
#[Input] public readonly string $title,
#[Input] public readonly string $content,
#[Input] public readonly AuthorInput $author
) {}
}
final class AuthorInput
{
public function __construct(
#[Input] public readonly string $name,
#[Input] public readonly string $email
) {}
}
// 自動的にネスト構造を構築
$article = $inputQuery->create(ArticleInput::class, $_POST);
echo $article->author->name; // "John"final class ProcessOrderInput
{
public function __construct(
#[Input] public readonly string $orderId,
#[Input] public readonly CustomerInput $customer,
private OrderService $orderService, // DIから注入
private MailerInterface $mailer // DIから注入
) {}
public function process(): Order
{
$order = $this->orderService->create($this->orderId, $this->customer);
$this->mailer->sendConfirmation($order);
return $order;
}
}Ray.InputQueryは、Ray.MediaQueryと組み合わせることで、完全な型安全データフローを実現します:
[Webフォーム/API]
↓ Ray.InputQuery(入力の構造化)
[型付きInputオブジェクト]
↓ ビジネスロジック
[Ray.MediaQuery]
↓ 自動展開
[SQL/WebAPI]
class Article extends ResourceObject
{
public function __construct(
private ArticleRepositoryInterface $repository
) {}
public function onPost(ArticleInput $article): static
{
// Ray.InputQueryが自動的に:
// 1. POSTデータを解析
// 2. ArticleInputオブジェクトを生成
// 3. メソッドに注入
$id = $this->repository->save($article);
$this->code = 201;
$this->headers['Location'] = "/articles/{$id}";
return $this;
}
}interface ArticleRepositoryInterface
{
#[DbQuery('article_add')]
public function save(ArticleInput $article): string;
}
// Ray.MediaQueryが自動的にInputオブジェクトを展開:
// ArticleInput { title: "Hello", author: { name: "John", email: "john@example.com" }}
// ↓
// :title = "Hello", :authorName = "John", :authorEmail = "john@example.com"- シンプルさ - 特別な設定なしで動作
- 型安全性 - PHPの型システムを最大限活用
- 拡張性 - インターフェースベースの設計
- 相互運用性 - 既存のエコシステムとの統合
- テスタビリティ - 各コンポーネントが独立してテスト可能
JSON Schemaはメソッドレベルで指定し、コンテキストに応じたバリデーションを実現します:
class UserController extends ResourceObject
{
#[JsonSchema('schemas/user-create.json')]
public function onPost(UserInput $user): static
{
// 作成時のバリデーション:email, password必須
$id = $this->repository->create($user);
$this->code = 201;
return $this;
}
#[JsonSchema('schemas/user-update.json')]
public function onPut(string $id, UserInput $user): static
{
// 更新時のバリデーション:passwordは任意
$this->repository->update($id, $user);
return $this;
}
}-
コンテキスト依存のバリデーション
- 同じInputクラスでも、作成・更新・参照で異なるルール適用可能
- 権限によって異なるバリデーション(管理者 vs 一般ユーザー)
-
APIドキュメントとの統合
#[JsonSchema('schemas/article-post.json')] #[OpenApi(summary: 'Create new article', tags: ['Articles'])] public function onPost(ArticleInput $article): static
-
段階的なバリデーション
- Ray.InputQuery: 型レベルの基本チェック
- JSON Schema: ビジネスルールのバリデーション
-
再利用性
// 同じInputクラスを異なるコンテキストで使用 final class ProductInput { /* ... */ } #[JsonSchema('schemas/product-admin.json')] // 全フィールド必須 public function adminCreate(ProductInput $product): void #[JsonSchema('schemas/product-draft.json')] // 最小限のフィールド public function saveDraft(ProductInput $product): void
// Ray.MediaQueryやBEAR.Resourceでの統合
class JsonSchemaInterceptor implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation): mixed
{
$method = $invocation->getMethod();
$schema = $method->getAttributes(JsonSchema::class)[0] ?? null;
if ($schema) {
$args = $invocation->getArguments();
// Inputオブジェクトを検証
$this->validator->validate($args, $schema->newInstance()->path);
}
return $invocation->proceed();
}
}HTMLフォームからInputクラスを自動生成:
入力: <form>...</form>
出力: Inputクラスのコード
Ray.InputQueryは、フラットなクエリーデータから型安全なPHPオブジェクトを生成する基盤ライブラリです。
- 型安全性の境界拡張 - HTTPリクエストレベルから型チェック
- 構造の明確化 - フォームとコードの構造が一致
- 開発効率の向上 - ボイラープレートコードの削減
- エコシステムとの統合 - Ray.MediaQuery、BEAR.Resourceとのシームレスな連携
- getArguments() - メソッドの引数リストを生成(主要用途)
- create() - オブジェクトを直接生成(補助用途)
- パラメータレベルでの#[Input]属性によるデータソースの明示
- DIとの自然な統合
- ネストしたオブジェクトの自動解決
- 最小限の依存(ray/diのみ)
これにより、Webアプリケーション開発における「入力」を第一級市民として扱い、より安全で保守性の高いコードベースを実現します。