Laravel Advent Calendar 2014 の 9 日目です。
今年の Advent Calendar では、Laravel 5 リリース目前ということで、Laravel 5 の話題が多いのですが、それは他の方にお任せして、ここでは、Laravel 4 でのアプリケーション実装について書いてみます。
Laravel は自由度の高いフレームワークですので、アプリケーションも自由な構成にすることができます。ただ、この「自由さ」が故に、どういう構成が良いのかというのが悩ましい点でもあります。
このエントリでは、私が実際に構築したプロダクトをベースに構成例をご紹介します。Laravel アプリケーションを構築する上での参考になれば嬉しいです。
1. ディレクトリ構成
まずは、大枠のディレクトリ構成から。アプリケーションや開発環境用の Vagrantfile やプロビジョンファイルなどは、Git リポジトリで管理しています。
ディレクトリ構成としては以下になります。
Dockerfile は、CI サーバで Docker を使って、自動テストを行っているので、そのためです。
Vagrantfile は、開発環境用の VM を管理するためのものです。アプリケーションディレクトリにルートに Vagrantfile を置いておくと、アプリケーション内のどのディレクトリからでも vagrant コマンドできるので便利です。
provision/ は、開発環境、上記 CI サーバでの実行環境、ステージング環境用のプロビジョンファイルが入っています。プロビジョンには、Ansible を使っています。本番環境は、構成管理はこちらでは行わないので、デプロイ用の playbook が格納されています。
src/ が、Laravel アプリケーションのコードが入っているディレクトリになります。以下では、この src/ について見ていきます。
. ├── Dockerfile ├── README.md ├── Vagrantfile ├── docker/ ├── provision/ └── src/
1-1. src/ ディレクトリ
src/ の構成は以下です。
このアプリケーションは、Laravel で REST API を実装して、AngularJS で実装したクライアント側と連携して動作するアーキテクチャになっています。また、クライアント側のコンポーネントの管理に、bower を使っており、そのためのファイルやディレクトリがあります。
Laravel 自体は、ベーシックな構成ですが、アプリケーション独自のコンポーネントを格納するために、package というディレクトリを作っています。これについては、後述します。
. ├── CONTRIBUTING.md ├── Gruntfile.js ├── _ide_helper.php ├── app/ ├── artisan ├── bootstrap/ ├── bower.json ├── bower_components/ ├── composer.json ├── composer.lock ├── karma.conf.js ├── package/ ├── package.json ├── phpunit.xml ├── public/ ├── readme.md ├── server.php ├── spec/ └── vendor/
2. Laravel アプリケーション の役割
Laravel アプリケーションでは、主に以下の役割を担っています。
- HTML の生成、出力
- JavaScript(AngularJS アプリケーション) / CSS などの生成、出力
- REST API の提供(AngularJS アプリケーションと連携)
初期のビューは、Laravel で生成、出力して、あとは、AngularJS + REST API で連携して動作する SPA(Single Page Application) の形です。このアプリケーションでは、特定ユーザのみが利用するものなので、認証を行い、認可されたユーザのみがアプリケーションを利用できるように、ビューの出力も Laravel で行っています。
ほとんどの機能は、REST API にて提供されるので、以降はこの部分について書いていきます。
3. 処理フロー
REST API への HTTP リクエストは、概ね、以下のフローで処理されます。
- Routing – リクエストされた URI から、処理すべきハンドラを決定します。通常は、コントローラのメソッド、簡単な処理ならネイティヴクロージャに処理を委ねます。
- Controller – 処理の起点となります。バリデーションを行い、正常値であれば、サービスクラスのメソッドに値を渡して、実行します。
- Service – 実処理を行う部分です。ビジネスロジックを実行し、Model(Eloquent)を使って、データベースへの操作などを行います。
- Model(Eloquent) – Eloquent を継承したクラスです。データベースへのアクセスを担うとともに、エンティティ固有の処理なども行います。
各フェーズ(レイヤ)では自身の責務に注力すればよく、上位のフェーズを関知する必要はありません。(必要な情報は受け渡されます。)
HTTP レスポンスボディは、JSON で返すので、blade などのビューテンプレートは、ここでは利用しません。
4. アプリケーションクラスの構成(namespace, autolaod)
ルーティング以外のクラスは、app/ 以下ではなく、package/以下に配置しています。また、namespace で、独立したパッケージとして分離しています。
アプリケーションをフレームワークの構成から独立したパッケージとすることで、フレームワークの一部としてアプリケーションを構成するのではなく、アプリケーションの一部としてフレームワークを利用することをイメージしています。
package/
└── Foo/
├── DomainA
│ ├── Command/ <--- CLI Command
│ ├── Controller/ <--- Controller
│ ├── Model/ <--- Model(Eloquent)
│ ├── Service/ <--- Serivice
│ ├── Test/ <--- Test
│ └── Validation/ <--- Validation
├── DomainB
│ ├── Command/
│ ├── Controller/
│ ├── Model/
│ ├── Service/
│ ├── Test/
│ └── Validation/
OneByOne/
└── Laravel
├── Controller/
├── Exception/
├── Service/
└── Test/
namespace のトップレベルには、アプリケーション名(Foo)を、次に、ドメインパッケージ名(DomainA / DomainB)が続きます。アプリケーション内でも業務などのドメイン毎にパッケージを分けています。さらに、それぞれのドメインごとに、Controller や Model など役割に応じた namespace を割り当てています。
例えば、Foo システムの DomainA の Controller にある SampleController であれば、Foo\DomainA\Controller\SampleController となります。
ここでは、Foo システムの他に、汎用コンポーネントを集積した OnebyOne システムも同梱しています。
アプリケーションクラスは、composer.json にて、PSR-4 を使って、オートロードで読み込めるようにしています。Laravel におけるアプリケーションは、オートローダで読めれば、どこに配置しても良いので、こうした柔軟な構成が可能です。
- composer.json
{
"autoload": {
"psr-4": {
"Foo\\": "package/Foo"
"OneByOne\\": "package/OneByOne"
}
},
}
5. Routing
ルーティングでは、URI と処理を行うコントローラのメソッドを Route::verbs() で連結するだけです。
一部、単純にデータベースから値を読み込んで、JSON として返す程度のものは、ネイティヴクロージャで実装しています。
認証が必要な API については、認証用のフィルタを実装しておいて、それを、Route::group() で指定します。Route::group() のネイティヴクロージャ内に認証が必要な API の定義を記載します。
Route::when() では、POST, PUT, DELETE リクエスト場合は、csrf フィルタを適用するようにしています。
Route::pattern() では、/foo/{id} の {id}を受け入れるパターンを定義していまます。こうしておけば、各ルーティングで、正規表現を定義する必要がなく、便利です。
- app/config/routes.php
<?php
use FooDomainAHogeController;
$apiPrefix = '/api/v1';
// csrf filter
Route::when('*', 'csrf', ['POST', 'PUT', 'DELETE']);
// patterns
Route::pattern('id', '[0-9]+');
Route::group(
['before' => 'auth'],
function () use ($apiPrefix) {
$controller = HogeController::class;
Route::get($apiPrefix . '/foo', $controller . '@read');
Route::post($apiPrefix . '/foo', $controller . '@create');
Route::put($apiPrefix . '/foo/{id}', $controller . '@update');
Route::delete($apiPrefix . '/foo/{id}', $controller . '@delete');
}
);
6. Controller
コントローラでは、入力値のバリデーション、サービスクラスの実行、そして、HTTP レスポンスを構築します。
下記は、典型的なコントローラの例です。
コントローラクラスでは、フレームワークの Contoller クラスを継承しています。あと、API 共通処理(JSON レスポンス等)を ApiControllerTrait というトレイトに実装しているので、これを use しています。
- package/Foo/DomainA/Controller/SampleController.php
<?php
namespace FooDomainAController;
use FooDomainAServiceSampleService;
use FooDomainAValidationSampleValidatorBuilder;
use OneByOneLaravelControllerApiControllerTrait;
class SampleController extends Controller
{
use ApiControllerTrait
/**
* @var SampleService
*/
protected $service;
/**
* @param SampleService $serivice
*/
public function __contruct(SampleService $service)
{
$this->service = $service;
}
public function create()
{
// validation
$validator = (new SampleValidatorBuilder())->create(Input::all());
if ($validator->fails()) {
return $this->validationError($validator->messages());
}
$this->service->create(Input::all());
return $this->created();
}
}
下記が、ApiControllerTrait の一部です。ここでは、JSON レスポンスを返すメソッドをまとめています。
- package/OneByOne/Laravel/Controller/ApiControllerTrait.php
<?php
namespace OneByOneLaravelController;
use Response;
use SymfonyComponentHttpFoundationResponse as Res;
trait ApiControllerTrait
{
/**
* ok
*
* @param mixed $message
* @return IlluminateHttpJsonResponse
*/
public function ok($message = '')
{
return Response::json($message);
}
/**
* validationError
*
* @param mixed $messages
* @return IlluminateHttpJsonResponse
*/
public function validationError($messages = [])
{
return $this->badRequest('validation', $messages);
}
/**
* BadRequest
*
* @param string $error
* @param array $messages
* @return IlluminateHttpJsonResponse
*/
public function badRequest($error = null, $messages = [])
{
$response = [
'error' => $error,
'messages' => $messages,
];
return Response::json($response, Res::HTTP_BAD_REQUEST);
}
(snip)
}
6-1. サービスクラスのインジェクト
SampleController に戻ります。コンストラクタでは、処理を担うサービスクラスを引数に取ります。コントローラは、フレームワークによって、インスタンス化されるので、Laravel の IoC コンテナにより、SampleService クラスのインスタンスが自動でインジェクトされます。
もちろん、テストなどの際は、自らインスタンス化して、モックオブジェクトを差し込むことも可能です。
6-2. バリデーション
ルーティングによりリクエスト URI に合致したメソッドが実行されます。
ここでは create メソッドが実行されるとします。コントローラのメソッドでは、まず、入力値のバリデーションを行います。
SampleValidatorBuilder クラスにて、バリデーションクラスのインスタンスを生成します。
SampleValidatorBuilder の実装例は下記です。バリデーションクラスの生成(ルールの設定)をクラス化しておくことで、コントローラからバリデーションルールの設定を逃がすことができ、同じルールを異なる箇所で共有できます。当然、テストからも、バリデーションクラスが生成できるので、バリデーションのテストが書くのが容易です。
このバリデーションビルダのインターフェースとテスト用のトレイとをまとめて、packagist に公開しています。
shin1x1/laravel-validator-builder
- package/Foo/DomainA/Validation/SampleValidatorBuilder.php
<?php
namespace FooDomainAValidation;
use Shin1x1ValidatorBuilderValidatorBuilder;
use Validator;
/**
* Class SampleValidatorBuilder
* @package FooDomainAValidation
*/
class SampleValidatorBuilder implements ValidatorBuilder
{
/**
* @param array $inputs
* @return IlluminateValidationValidator
*/
public function create(array $inputs)
{
return Validator::make(
$inputs,
[
'name' => 'required',
'email' => 'required|email',
]
);
}
}
6-3. サービスクラスの実行
入力値が妥当なものであれば、サービスクラスのメソッドを実行します。
サービスクラスのインスタンスは、コントローラのコンストラクタでセットされているので、これを利用します。
処理が正常であれば、created メソッド(ApiControllerTrait に実装)を呼び出して、201 Created を返します。
現在は、コンストラクタインジェクションでサービスクラスをインジェクトしているので、1コントローラ=1サービスの形が多いです。(App::make() などで、メソッド内で、サービスクラスのインスタンスを取得する場合もあります。)
6-4. Laravel 5 でのメソッドインジェクション
今後の話ですが、Laravel 5 では、コントローラのメソッドについても、IoC コンテナによる DI が可能になります。これにより、各アクションメソッド毎にサービスやバリデーションビルダがインジェクトできるので、より使いやすくなります。
7. Service
サービスクラスでは、ビジネスロジックなどのドメイン固有の処理を実行します。フレームワークのクラスは継承せず、単なる POPO として実装しています。
ユースケースに対応して実装することが多いので、名称から内容が想起できるもにしています。例えば、予約を扱うアプリケーションなら、ReservationService をクラス名とし、各メソッド名は、予約業務のユースケースである「予約する(book)」「予約を更新する(update)」「予約を取り消す(cancel)」としています。
下記では、Bar クラスのインスタンスを作って、入力値をデータベースに保存しています。
入力値のバリデーションは、コントローラで終わっているので、このクラスで処理する場合は、引数は正当な値であるとみなします。ただ、値の整合性(一意制約などのアプリケーション制約)など、事前条件のチェックはここで行います。もし事前条件に違反している場合は例外を投げて処理を中断します。
- package/Foo/DomainA/Service/SampleService.php
<?php
namespace FooDomainAService;
use Foo/DomainA/Model/Bar;
class SampleService
{
/**
* @param array $inputs
*/
public function create(array $inputs)
{
$bar = new Bar();
$bar->name = array_get($inputs, 'name');
$bar->email = array_get($inputs, 'email');
$bar->save();
}
}
8. Model(Eloquent)
モデルは、フレームワークの Eloquent クラスを継承しており(正確には、Eloqent を継承した AppModel クラスを継承)、通常の Laravel アプリケーションで利用するものと同じです。
エンティティ固有の処理などは、こちらに実装します。
- package/Foo/DomainA/Model/Bar.php
<?php
namespace FooDomainAModel;
use OneByOneLaravelModel;
class Bar extends AppModel
{
}
さいごに
Laravel 4 で構築したアプリケーションの構成について見てきました。
考え方の基本は、本文でも書いたとおり、アプリケーションが主で、フレームワークはそれを実現するためのものツールに過ぎないということです。そのため、アプリケーションはフレームワークとは、独立したパッケージとして構成しています。
この考え方をより進めるなら、package/ 以下のクラスについては、フレームワーククラスを利用する箇所はより抽象化していく(例えばリポジトリパターンを利用して、Eloquent と分離するなど)方法もあるのですが、抽象化を進めすぎて、かえって複雑になるのを避けるために、現在の状態に留めています。
構築するアプリケーションにもよるとは思いますが、これまでは、この形がしっくり来ています。(もちろん、今後変わる可能性もあります。)
こうしたアプリケーションを主体にした構成にしておけば、フレームワークのバージョンが変わっても、柔軟に対応できそうです。Laravel 5 では、アプリケーションディレクトリの構成が変わりますが、そもそも別構成にしているので、オートローダで読み込めれば問題ありません。
まだまだ改良していく余地はありますが、こうした柔軟な構成が取れるのも Laravel の面白さですね。
Laravel Advent Calendar 2014、明日は neronplex さんです。お楽しみに!
- Older: Composer を倍速にした、たった 1 行のコード