はじめに
こんばんは。
Laravelの開発を始めております。
APIでLaravelを使用してるのですが、Request/Responseのチェックをできないものかと考えていて、jsonSchema使えばいいじゃんと思い早速探してみました。
packagistを調べてると、 それっぽいライブラリ を発見したのですが、なんか思ってたものと違うのと、 ServiceProviderでやってたので、どちらかというと Middlewareの仕事かな?と思い、Middlewareで作ってみました。
検証環境は以下
- php: 7.3.2
- Laravel: 5.8.4
コード
1. JsonSchemaValidate.php(Middleware)
<?php declare(strict_types=1); namespace App\Http\Middleware; use App\Exceptions\JsonSchemaException; use Closure; use Illuminate\Support\Facades\Route; use JsonSchema\Validator; class JsonSchemaValidate { const TYPE_REQUEST = 'Request'; const TYPE_RESPONSE = 'Response'; /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @throws JsonSchemaException * @return mixed */ public function handle($request, Closure $next) { $routeName =$this->getRouteName($request); if ($routeName === null) { return $next($request); } $validator = new Validator(); $this->validateRequest($validator, $request, $routeName); $response = $next($request); $this->validateResponse($validator, $response, $routeName); return $response; } /** * @param Validator $v * @param \Illuminate\Http\Request $request * @param string $routeName * @throws JsonSchemaException */ public function validateRequest(Validator $v, \Illuminate\Http\Request $request, string $routeName): void { $requestSchema = $this->getSchema($routeName, self::TYPE_REQUEST); if ($requestSchema !== null) { $v->check((object)$request->all(), $requestSchema); if ($v->numErrors() >= 1) { throw new JsonSchemaException(serialize($v->getErrors())); } } } /** * @param Validator $v * @param \Illuminate\Http\JsonResponse $response * @param string $routeName * @throws JsonSchemaException */ public function validateResponse(Validator $v, \Illuminate\Http\JsonResponse $response, string $routeName): void { $responseSchema = $this->getSchema($routeName, self::TYPE_RESPONSE); if ($responseSchema !== null) { $v->check($response->getData(), $responseSchema); if ($v->numErrors() >= 1) { throw new JsonSchemaException(serialize($v->getErrors())); } } } /** * @param string $routeName * @param string $type * @return null|array */ public function getSchema(string $routeName, string $type): ?array { $className = $this->getJsonSchemaClassName($routeName, $type); return class_exists($className) ? $className::getSchema() : null; } /** * @param string $routeName * @param string $type * @return string */ public function getJsonSchemaClassName(string $routeName, string $type): string { return "App\\Http\\Schema\\{$type}\\{$routeName}Schema"; } /** * @param \Illuminate\Http\Request $request * @return null|string */ public function getRouteName(\Illuminate\Http\Request $request): ?string { $route = Route::getRoutes()->match($request); return $route->getName(); } }
2.MemberListSchema.php(RequestパラメータのJsonSchema)
<?php declare(strict_types=1); namespace App\Http\Schema\Request; class MemberListSchema { /** * @return array */ public static function getSchema(): array { return [ '$schema' => 'http://json-schema.org/draft-07/schema#', 'required' => ['page'], 'type' => 'object', 'properties' => [ 'page' => ['type' => 'string'], ], ]; } }
3.MemberListSchema.php(ResponseデータのJsonSchema)
<?php declare(strict_types=1); namespace App\Http\Schema\Response; class MemberListSchema { /** * @return array */ public static function getSchema(): array { return [ '$schema' => 'http://json-schema.org/draft-07/schema#', 'required' => ['total', 'data'], 'type' => 'object', 'properties' => [ 'total' => ['type' => 'number'], 'data' => [ 'type' => 'array', 'items' => [ 'required' => [ 'id', 'lastName', 'firstName', 'mailAddress', 'roles' ], 'type' => 'object', 'properties' => [ 'id' => ['type' => 'number'], 'lastName' => ['type' => 'string'], 'firstName' => ['type' => 'string'], 'mailAddress' => ['type' => 'string'], 'roles' => ['type' => 'array'], ], ], ], ], ]; } }
4.api.php(routes)
<?php declare(strict_types=1); Route::group(['middleware' => ['json_schema']], function (): void { Route::get('/member', 'MemberController@list')->name('MemberList');
ルートの name()
で判定しています。
なので、ルートの名前が設定されてないと何も出来ません...
例えばリクエストで、pageというパラメータを送らない場合、以下のようなレスポンスになります。
とりあえず動いているみたいです。
終わりに
最初はschemaをjsonファイルで作ってたのですが、毎回 file_get_contents()するのがダサいなと思ったので、schemeクラスにしました。
もしかしたら自分が調べきれてないだけで、Laravel自体にこういう機能ってあるのかな?
もし知ってたら誰か教えてください。
簡単になりますが、現場からは以上です。
追記
レスポンスの型チェックは、正常系のレスポンスのときでないと、別のExceptionが投げられた際も型チェックしてしまうことに気づきました...
レスポンス部分は以下のように変えたほうが良さそうです。
public function validateResponse(Validator $v, \Illuminate\Http\JsonResponse $response, string $routeName): void { $responseSchema = $this->getSchema($routeName, self::TYPE_RESPONSE); if ($responseSchema !== null) { if ($response->isSuccessful()) { $v->check($response->getData(), $responseSchema); if ($v->numErrors() >= 1) { throw new JsonSchemaException(serialize($v->getErrors())); } } } }