こんにちは。やまゆです。
この記事は PHP アドベントカレンダー 2022 18 日目のものとなります。
前日の 17 日目の記事は @tanakahisateru さんのちょうぜつ設計とはです。
翌日の 19 日目の記事は @ippey_s さんの GitHub CodespacesでリモートPHP開発環境を整えてPhpStormで使う3ステップです。
リクエストのバリデーション
Laravel には FormRequest というクラスが存在します。
このクラスを利用することで、ユーザーからリクエストされた内容が正しい形式で送られてきているかをメイン処理の前にバリデートすることが可能です。
public function rules()
{
return [
'title' => ['required', 'unique:posts', 'max:255'],
'body' => ['required'],
];
}
このようにルールを配列で設定することにより、 Controller に入る前に自動でバリデートを行うことが出来ます。
public function __invoke(ArticleRequest $request)
{
/** @var array{title: string, body: string} $input */
$input = $request->validated();
// 信頼された値だけをここで利用可能
$article = Article::create($input);
$article->save();
return ['status' => 'created'];
}
バリデーションエラーになった場合は HTTP Status Code 422 Unprocessable Entity
が返却されます。
{
"message": "The title must be a string.",
"errors": {
"title": [
"The title must be a string.",
"The title must be at least 1 characters."
],
}
}
$request->validated()
の戻り値に型を付ける
現状だとこのように型を表記しています。
/** @var array{title: string, body: string} $input */
$input = $request->validated();
しかし、与えられる型は Request 側で定義したいですね。
その場合は、 @template
アノテーションと @extends
アノテーションを用いることで、静的解析ツールで判断してくれるようになります。
- psalm の場合: https://psalm.dev/r/8e26718f38
- phpstan の場合: https://phpstan.org/r/d9054a8f-59a0-405e-ba86-0ecac06ad0df
<?php
/**
* @copyright 2022 Masaru Yamagishi
* @license Apache-2.0
*/
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Foundation\Http\FormRequest;
/**
* @link https://laravel.com/docs/9.x/validation#form-request-validation
* @template T of array
*/
abstract class Request extends FormRequest
{
/**
* {@inheritDoc}
* @return T
*/
public function validated($key = null, $default = null)
{
return parent::validated($key, $default);
}
}
<?php
/**
* @copyright 2022 Masaru Yamagishi
* @license Apache-2.0
*/
declare(strict_types=1);
namespace App\Packages\Account\Http;
use App\Http\Controllers\Request;
/**
* @extends Request<array{
* email: string,
* password: string
* }>
*/
final class RegisterAccountRequest extends Request
{
/**
* {@inheritDoc}
*/
public function rules(): array
{
return [
'email' => ['required', 'email'],
'password' => ['required', 'password'],
'password_confirmation' => ['required', 'confirmed'],
];
}
}
このように定義することで、静的解析ツールで $request->validated()
の型を解決してくれるようになります。
レスポンスのバリデーション
Laravel は様々な手段でレスポンスを返すことが可能です。
public function __invoke()
{
// 配列でもok
return ['hello' => 'world'];
// Responsable なオブジェクトでもok
// @link https://laravel.com/api/9.x/Illuminate/Contracts/Support/Responsable.html
return response()->json(['hello' => 'world']);
}
しかし、エンジニアごとにレスポンスの生成方法が異なると、一貫性に問題がありますね。なので、 Response もクラスとして用意して、常にそれを利用するようにした方が保守性が上がります。
ついでに FormRequest
型と同じように、レスポンスの実装が間違っていないかバリデート出来るようにすればさらに安全です。
レスポンスの型が間違っている場合、クライアント側でエラーが発生することになりますが、サーバとクライアントが別のチームで開発している場合、「(C)レスポンスの型が違います」「(S)確認します」「(S)修正します」とかなりテンポが悪くなってしまいます。なのでサーバ側の実装時に出来るだけ感知できるようになっているべきです。
<?php
/**
* @copyright 2022 Masaru Yamagishi
* @license Apache-2.0
*/
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use JsonSerializable;
/**
* @link https://laravel.com/docs/9.x/validation#manually-creating-validators
*/
abstract class Response implements JsonSerializable, Responsable
{
/**
* Constructor
*
* @param array|null $result 後でバリデートするので、入力は array と広くとる
*/
public function __construct(private readonly ?array $result = null)
{
}
/**
* Gets validation rules
* ここで FormRequest と同じ形式でバリデーションルールを決める
*
* @return array
*/
abstract public function rules(): array;
/**
* {@inheritDoc}
*/
public function jsonSerialize(): mixed
{
if (is_null($this->result)) {
// 特に返すものがない場合は、空でも良いですが中身があった方が嬉しい場合があります
// ※ `[]` こう返ってしまうとクライアントのパースに問題が出る可能性があるなど
return ['ok' => true];
}
return $this->result;
}
/**
* {@inheritDoc}
*/
public function toResponse($request)
{
$payload = $this->jsonSerialize();
$rules = $this->rules();
// バリデータを生成
$validator = Validator::make($payload, $rules);
// ※ `$validator->validate()` メソッドを使うと失敗時に 422 を返してしまうので
// 代わりに `fails()` を使います
if ($validator->fails()) {
// バリデーションに失敗した場合は \LogicException の派生クラスを返却する
// ※レスポンスが異常=サーバ側の実装にミスがあるということなので
throw new ResponseValidationException($validator->errors());
}
return new JsonResponse($payload);
}
}
<?php
/**
* @copyright 2022 Masaru Yamagishi
* @license Apache-2.0
*/
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Response;
final class HomeResponse extends Response
{
/**
* {@inheritDoc}
*/
public function rules(): array
{
// FormRequest::rules と書き方は同じ
return [
'hello' => ['required', 'string'],
];
}
}
これで無事 Response もバリデーションすることが可能です。
HTTP 関連クラスの自動生成
OpenAPI などの API 定義規格で JSON HTTP リクエスト・レスポンスの型を定義しておけば、そこから Controller/Request/Response クラスを自動生成することが可能です。
openapi: 3.0.3
info:
title: Laravel Schema sample
version: 1.0.0
paths:
/api:
get:
operationId: Index
responses:
'200':
description: OkResponse
content:
application/json:
schema:
type: object
properties:
hello:
type: string
example: world
# x- prefix は自由に記述できる
x-laravel-validation:
- required
- string
required:
- hello
/api/account/register:
post:
operationId: RegisterAccount
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
type: string
format: email
x-laravel-validation:
- required
- email
password:
type: string
x-laravel-validation:
- required
- min:8
password_confirmation:
type: string
x-laravel-validation:
- required
- confirmed
required:
- email
- password
- password_confirmation
responses:
'200':
description: OkResponse
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
example: true
x-laravel-validation:
- required
- boolean
required:
- ok
こういった定義を行って、 schema などをパースして自動生成させることが可能です。