本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。
#目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践
#3.HTTPリクエストとレスポンス
#3.3 レスポンス
Laravelでレスポンス処理を受け持つのはResponseクラスであり、Responseファサードがあらかじめ用意されているのが、その実態は、Illuminate\Contracts\Routing\ResponseFactoryクラスである。ファクトリークラスであるため、呼び出す生成メソッドで実際に生成されれうResponseクラスは異なるため、アプリケーションからユーザーに返却するデータの種類で使い分ける必要がある。
### 文字列の返却
シンプルな文字列を返却したい場合は、そのまま文字列を与える。デフォルトではContent-Type:text/htmlが返却されるので、text/plainなどに変更する場合を下記に示す。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Illuminate\Support\Facades\Response;
use function response;
class TextAction extends Controller
{
public function __invoke(Request $request):IlluminateResponse
{
$response = Response::make('hello wolrd');
// ヘルパー関数を利用する場合
$response = response('hello world');
//content-typeを変更
$response = response('hello world', IlluminateResponse::HTTP_OK,
[
'content-type' => 'text/plain'
];
);
return $response;
}
}
### テンプレート出力
Bladeテンプレートなどを出力する場合、viewヘルパー関数やViewファサード使ってテンプレートを指定して、そのまま返却することでルーターの処理でレスポンスを生成する。viewヘルパー関数などを用いてIlluminate\View\Viewインスタンスを生成した場合、レスポンス返却時にフレームワークが内部でIlluminate\Http\Responseクラスを利用する仕組みになっている。テンプレート返却とレスポンスヘッダを利用する場合は、Responseファサードなどを介してviewメソッドを指定するか、responseヘルパー関数などを利用する。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Illuminate\Support\Facades\Response;
use function response;
use function view;
class ViewAction extends Controller
{
public function __invoke(Request $request)
{
$response = Response::view('view.file');
// 上記のメソッドと同じ結果が得られる。(viewヘルパー関数)
$response = view('hello world');
//responseヘルパー関数でステータスコードを変更し、ビューを出力
$response = response(view('view.file'), IlluminateResponse::HTTP_ACCEPTED);
return $response;
}
}
### JSON出力
APIレスポンスに利用されるJSONやクロスドメイン対応のJSONPの場合は、それぞれに対応するメソッドを利用でき、標準ではContent-type:application/jsonで返却されるので、任意のcontent-typeに変更することも可能である
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Illuminate\Support\Facades\Response;
use function response;
class JsonAction extends Controller
{
public function __invoke(Request $request):JsonResponse
{
$response = Response::json(['status' => 'success']);
// ヘルパー関数を利用する場合
$response = response()->json(['status' => 'success']);
return $response;
}
}
Jsonpの場合
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Illuminate\Support\Facades\Response;
use function response;
class JsonAction extends Controller
{
public function __invoke(Request $request):JsonResponse
{
$response = Response::jsonp(['status' => 'success']);
// ヘルパー関数を利用する場合
$response = response()->jsonp(['status' => 'success']);
return $response;
}
}
任意のメディアタイプ指定
response()->json(['message' => 'laravel'], Response::HTTP_OK,
[
'content-type' => 'application/vnd.laragel-api+json'
]
);
### ダウンロードレスポンス
ファイルのダウンロードなどを指定する場合はdownloadメソッドを利用する。ファイル名を指定してダウンロードする場合は、第2引数と第3引数に任意の指定を行う。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use function response;
final class DownloadAction extends Controller
{
public function __invoke(Request $request):BinaryFileResponse
{
$response = Response::download('/path/to/file.pdf');
// ヘルパー関数を利用する場合
$response = response()->download('/path/to/file.pdf');
// content-typeを指定
$response = response()->download('/path/to/file.pdf', 'filename.pdf',
[
'content-type' => 'application/pdf',
]);
return $response;
}
}
### リダイレクトレスポンス
リダイレクト先を指定してリダイレクトを実行する。HTTPリクエストのパラメータを渡す場合はwithputメソッド
、リダイレクトさせてエラーメッセージなどを一度だけ利用したい場合にはwithメソッド
も利用する。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Illuminate\Support\Facades\Response;
use function redirect;
use function response;
final class RedirectAction extends Controller
{
public function __invoke(Request $request):RedirectResponse
{
$response = Response::redirectTo('/');
$response = response()->redirectTo('/');
$response = redirect('/');
// リダイレクト時に様々な動作を行う例
$response = redirect('/')->withInput($request->all())->with('error', 'validation error');
return $response;
}
}
### Server-Sent Events実装
SSE(Server Sent Events)とはHTML5で新たに追加された機能でサーバ側からプッシュ型データ通信を利用できる。Websocketとは異なるため、SSEはHTTPプロトコルを利用するため、特別に実装することなく利用できるが、webSocketのように双方向の通信はできない。
'streamメソッド'を利用する。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use function flush;
use function ob_flush;
use function rand;
use function usleep;
final class StremAction extends Controller
{
public function __invoke(Request $request):StreamedResponse
{
return $response->stream( function() {
while (true) {
echo 'data:'.rand(1 ,100)."\n\n";
ob_flush();
flush();
unsleep(200000);
}
},
Response::HTTP_OK,
[
'content-type' => 'text\event-stream',
'X-Accel-Buffering' => 'no',
'Cache-Control' => 'no-cache',
]);
}
}
上記に示すコード例では定期的にランダムな数値を返却したがアプリケーションに合わせて様々なデータを扱える。
リソースクラスを組み合わせたREST APIレスポンスパターン
APIレスポンスでは、idやtitleを持たせてもlinkを持たせないと別のページに遷移できない。また、直接URLに表示する場合、データベースのカラム名がURLに含まれてしまうのでセキュリティ的にもよろしくない。またデータベースのカラム変更があった場合の影響が大きい。
→jsonフォーマットを用いて、データベースとレスポンスを切り離さなければならない。(HAL)
下記にHALの適用例を示す。
{
"userId": 10,
"lastOrderDate": "Mon, 07 Jul 2014 18:26:21 GMT",
"_links": {
"self": { "href": "/users/10/orders" },
"next": { "href": "/users/10/orders?page=2" }
},
"_embedded": {
"orders": [{
"orderId": 12,
"orderDate": "Mon, 01 Jul 2014 18:26:21 GMT",
"_links": {
"self": { "href": "/users/10/orders/12" }
}
}, {
"orderId": 11,
"orderDate": "Mon, 01 Jul 2014 18:26:21 GMT",
"_links": {
"self": { "href": "/users/10/orders/11" }
}
}]
}
}
##リソースクラスの基本
リソースクラスはEloquentモデルと組み合わせることで、データベースの値をAPIで必要なリリース情報に変換できる。
※必ずEloquentモデルでなくてもよい
では実際にリソースクラスを使ってみる。
### 1.リソースクラスの作成
まずは、下記のコマンドを実行して、app/Http/Resourcesディレクトリ配下に4つのファイルを生成する。
php artisan make:resource UserResource
php artisan make:resource CommentResource
php artisan make:resource CommentResourceCollection
php artisan make:resource ArticleResource
ここで作成したクラス名の末尾をCollectionにすると、Illuminate\Http\Resources\Json\ResourceCollection
を継承したリソースクラスが生成される。
→リソースとして渡された配列がCollectionインスタンスとなるため、resourceプロパティまたは、collectionプロパティとして操作できる。
2. コントローラの作成
ブログ情報を返却するAPIリソースはブログがルートとなるため、ArticleResourceをuseしているArticlePayloadActionクラスをコントローラクスとしている。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Resources\ArticleResource;
use Illuminate\Http\Request;
final class AtriclePayloadAction extends Controller
{
public function __invoke(Request $request)
{
$resource = new ArticleResource([
'id' => 1,
'title' => 'Laravel',
'comments' => [
[
'id' => 2134,
'body' => 'awesome',
'user_id' => 12345,
'user_name' => 'Applicatoin Developer',
]
],
'user_id' => 3213,
'user_name' => 'User1',
]);
return $resource->response($request)->header('content-type', 'application/hal+json');
}
}
今回はHALを適用したjsonレスポンスなのでcontent-typeにapplication\hal+jsonを利用している。
ブログの記事情報をルートのリソースとしてAtricleResourceクラス、そのブログの埋め込み情報として作成者であるユーザー情報をUserResoureクラス、ブログ記事のコメントの情報をCommentResourceクラスが管理する。なお、コメントは複数記述されることもあるので、CoomentResourceCollectionクラスとしている。
###3. Resourceクラスの実装
①ArticleResourceクラス
リソースのメインであるブログ情報を扱う。ブログ情報と関連するリソース情報で構成されているため、必要なリソースをnewして呼び出している。ブログリソース自体へのリンクはself
を用いてURLを示す。
下記に実装例を示す。
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use function sprintf;
class AtricleResource extends JsonResource
{
public static $wrap = '';
public function toArray($request):array
{
return [
'id' => $this->resource['id'],
'title' => $this->resource['title'],
'_embedded' => [
'comments' => new CommentResourceCollection(
new Collection($this->resource['comments'])
),
'user' => new UserResource(
[
'user_id' => $this->resource['user_id'],
'user_name' => $this->resource['user_name'],
]),
],
'_links' => [
'self' => [
'href' => sprintf('https://exapmple.com/articles/%s', $this->resource['id'])
]
]
];
}
}
上記コード例では、ArticleResourceクラスのコンストラクタに渡された配列を、構成する各リソースクラスへ渡し、ブログ情報を示すものとして値を変換・整形している。コンストラクタに渡された配列は、リソースクラス内ではresouceプロパティ
を利用して操作できる。
②UserResourceクラス
ユーザー情報に関するリソースを扱う。ユーザーを識別するidとユーザー名、ユーザー情報にアクセスするリンクを示すselfで構成されている。
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use function sprintf;
class UserResource extends JsonResource
{
public static $wrap = '';
public function toArray($request):array
{
return [
'id' => $this->resource['id'],
'name' => $this->resource['name'],
'_links' => [
'self' => [
'href' => sprintf('https://exapmple.com/users/%s', $this->resource['user_id'])
]
]
];
}
}
③CommentResourceクラス
コメントに関するリソースを扱う。コメント情報とその埋め込み情報として、内部でユーザー情報のリソースを利用する。
HALでは埋め込み情報は_embedded
を利用するので、ユーザー情報を埋め込み情報として、先ほどUserResourceクラスを利用する。
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use function sprintf;
class CommentResource extends JsonResource
{
public static $wrap = '';
public function toArray($request):array
{
return [
'id' => $this->resource['id'],
'body' => $this->resource['name'],
'_links' => [
'self' => [
'href' => sprintf('https://exapmple.com/comments/%s', $this->resource['id'])
]
],
'_embedded' => [
'user' => new UserResource(
[
'user_id' => $this->resource['user_id'],
'user_name' => $this->resource['user_name']
])
],
];
}
}
④CommentResourceCollectionクラス
コメントリソースのコレクションを表現するために利用。コメントコメント投稿をリソースコレクション1つで利用するのではなく、あくまでも1つ1つのリソースの集合と考えて実装するのがポイント。
下記に示すコードでは、リソースの集合、例えばコメントリストのみの情報などアクセスさせえるリンク情報は持たないため、リソース構築のみを実行している。
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CommentResourceCollection extends ResourceCollection
{
public function toArray($request):array
{
return $this->resource->map(function ($value) {
return new CommentResource($value);
})->all();
}
}
4. ルータへの登録
Route::get('/payload', 'ArticlePayloadAction');
構成する全リソースクラスを実装し、ArticlePayloadActionクラスをルータに登録している。
HTTPリクエストを送信することで、HALが適用したJSONが返却される。