laravelアーキテクチャ再考と中規模以上のノウハウ(年末特大号)

Posted: 2014-12-31 02:02 |  laravel PHP全般 
年末なので、今年一年laravelを個人規模からそこそこ大規模まで利用したノウハウと、
個人的なポイント等を紹介したいと思います
若干主観もありますが、実際に使った時のものを混ぜて紹介します

実務で使う方や、企業で導入しようと思ってる方にも参考になる様に頑張ります
新原さんの自分流 Laravel 4 アプリケーションアーキテクチャ も是非参考にしてみてください

規模による考え方の違い

まずはlaravelはそもそも何向きなのかという事ですが、
開発規模は実際のところは問いません
高速なレスポンス等が要求される場合は、ある程度の規模でしたらPhalconがオススメですが、
お気に入りのフレームワークでしたら何でもいいでしょう!
って事にしたらこのエントリの意味がなくなってしまうので、
Laravelを使う場合の話ですね

今まで使っていたフレームワークから移行する場合は、
これまでのノウハウを活かしたいと思うのは当たり前ですが、
チームで使うにはいくつかポイントがあります

1.autoloadを変える

Laravelはcomposerでインストールすると基本的にはデフォルト構成でそのまま使えます
個人などで小さめな開発する場合は、実際のところ、構造なんてどうでも良いのです
使い易い様にして頂くだけでOKでしょう。
この使い易い、デフォルト 
というのは個人、小規模までと考えておいた方が良いかもしれません
デフォルトの状態のままで開発する場合、
classmapになっていますのでチーム内の開発者がそれぞれdump-autoloadしなければなりません
複数人でやっていると、この手順が無駄になってきます
また、チームにはcomposerに慣れていない人もいるでしょう
classmapからPSRに変更するのは作業効率を上げる為にも一つのポイントになります
次期バージョンの5からは標準でPSR-4になりますので、
クラス追加したんですけど、動かないッス!問題は解消されます

2.namespaceを意識する

PSRにすると公式サイトの記述方法と少し変わってしまいますね
といっても頭に\を付けて完全修飾名にするか、useでファサードをインポートしてあげるだけです
これにする事で何が変わるのでしょうか?
この後のポイントにも関わってきますが、メンテナンス性を上げる為には
構造化が必要不可欠になります
namespaceがあるのと無いのではこの部分で大きな差が出てきます
それに実際にあるケースなんですが、Laravelのこの便利な記述のおかげで(良い意味で)
メンバーに実装を頼むと、全部staticで実装されたりしますので
教育、メンバーのスキルアップ的な意味も込めてnamespace利用をお勧めします
他に、企業で利用する場合は、過去のライブラリを利用する場合も多くあります
レガシーなコードなども含まれますので、namespaceを利用しないで進めていた為、
動かねー!!
となったケースももちろんありました
これを回避する場合はapp/config.phpでエイリアスを変更すれば衝突はありませんが、
それまでに実装していたところも変更して、テストコードも直す事になります
    'aliases' => [

        'App'             => 'Illuminate\Support\Facades\App',
        'Artisan'         => 'Illuminate\Support\Facades\Artisan',
        'Auth'            => 'Illuminate\Support\Facades\Auth',
        'Blade'           => 'Illuminate\Support\Facades\Blade',
        'Cache'           => 'Illuminate\Support\Facades\Cache',
        'ClassLoader'     => 'Illuminate\Support\ClassLoader',
        'Config'          => 'Illuminate\Support\Facades\Config',
        'Controller'      => 'Illuminate\Routing\Controller',
        'Cookie'          => 'Illuminate\Support\Facades\Cookie',
        'Crypt'           => 'Illuminate\Support\Facades\Crypt',
        'DB'              => 'Illuminate\Support\Facades\DB',
        'Eloquent'        => 'Illuminate\Database\Eloquent\Model',

メンテナンス性とは一体・・・ となりますので1, 2のポイントは十分に価値があります
namespaceを利用しても、オートローダーを作ってエイリアス経由で・・
などとするケースもあるでしょう
その場合、IDEの補完などはどうでしょうか?
ide_helperの様に作らないければチームに取ってはただ使い辛くなってしまいます
(Laravelはそうじゃん!というのは置いておいて・・)
それに車輪の云々・・ですね
それでもまだチームでデフォルトで利用しますか?

3.構造化

laravelを導入される方や、企業の方が検討するポイントとしては、
恐らくそれまで利用していたフレームワークによって色々考える所があると思いますが、
laravel3がリリースされて少ししてからcodeigniterからの乗り換えが海外で多かった様に、
日本ではfuelPHPが流行っているので、そこからの移行を考えている方も多いでしょうし、
何よりも

記述方法が似ているから

というのも少なからずあると思います
実際に先ほども触れますが、staticで実装してしまうケースや、モジュール機能が欲しい!
というのも良く聞くのでこのパターンが多いのかもしれません
構造化をする、というのはすでに1, 2のポイントを踏まえていれば単純な事だと思いますが、
fuelPHPの様なモジュールも構造化にあたり、
たとえば
    "autoload": {
        "classmap": [
            "app/database/migrations",
            "app/database/seeds"
        ],
        "psr-4": {
            "App\\": "app/App"
        }
    },
と変更した場合は、app/App配下は規約に準拠してさえいればどこに何があろうと
実装するときに意識する事はありません
モジュールっぽくしたい場合は次の様な感じにするだけです

■controller

namespace App\Modules\Controller;

use App\Modules\Models\Sample;
use App\Controllers\BaseController;

class SampleController extends BaseController
{

    protected $sample;

    /**
     * @param Sample $sample
     */
    public function __construct(Sample $sample)
    {
        $this->sample = $sample;
    }

    /**
     * @return \Illuminate\View\View
     */
    public function index()
    {
        return \View::make('sample.index');
    }
}

■モデル層など

 
namespace App\Modules\Models;

class Sample
{

    public function get()
    {
        return "hello";
    }
}

■views

viewのパスはいくらでも追加する事が出来ます
    'paths' => array(
        __DIR__.'/../views',
        app_path('App/Modules/views')
    ),
config/view.phpに追加した例です
実装の工夫次第でなんでもできるはずです

■ルーティング

\Route::group(['namespace' => 'App\Modules\Controller'], function () {
        \Route::get('sample', ['uses' => 'SampleController@index', 'as' => 'sample.index']);
    }
);
これだけですので、実際のところそれ用のパッケージを入れる程のものではありませんし、
1, 2のポイントを踏まえていればどうってことはありません
autoloadを変更していない場合はかなり辛くなります
モジュール単位でメンバーが実装する場合はこの手法をおすすめします
また新原さんの記事にもある様に、例えばappではなく、ほかのディレクトリに置いてもいいわけで、
composer.jsonにそれを記述しておけば補完もそのまま使えます
他にも色々考えられるポイントはありますが、このくらいにしておきましょう
あとで実際に自分が利用している構造と考えを紹介します

4.データベース関連の実装

LaravelにはEloquentという他のフレームワークユーザーが見ても十分魅力的な
データベースアクセスコンポーネントがありますね
ただしこれも複数人規模の開発になると利用はオススメしません
実際に実務で利用する場合は、保守を除き、仕様変更等は良くある事で、
DB設計によっては拡張しづらいまま開発の終盤になった頃に

こうしたいので変えてホシイナー

という悪魔の声が聞こえてきます
複雑なSQLになった場合や、JOIN増やさないとダメだね〜とかよくあります
この複雑なSQLになってしまった場合などで、
Eloquentではどうしても対応し辛くなるケースが結構あります
またその記述方法を実装する為にEloquentは処理が遅いという欠点もあります
この部分は例えば、発行するクエリーをキャッシュする様に実装したり、
色々と手はありますが、面倒くさいケースが多々あります
最速なのはPDOラップの最もベーシックなものです
またはクエリービルダーを利用するのをオススメします
そこそこの規模になるとこの実装を入れ替えただけで大きく変わる場合もあります
なので、使い分けとしては

Eloquent: 小規模、個人開発 速度はそんなに求めない
クエリービルダー/ベーシック: 中規模以上

が良いかもしれません
構造化の話にも繋がりますが、例に良く挙げるリポジトリーパターンで実装する事の最大の利点としては、
それぞれを疎結合に、という点に付きます
中規模くらいの開発からは、負荷テストをしてMySQLじゃなくてやっぱりOracleにしようとか、
ここの検索はDBじゃなくてSolrとかElasticsearchにしよう!
という話になるのはごく一般的な話です
この場合コントローラーにデータベースアクセスが記述されていたり、
インターフェースではなく、クラスそのものがnewされていたら・・・
変更だけで数週間かかるかもしれません
ドメイン駆動設計は偉大です
それに規模が大きくなるとDB数個使うとかも良くある話です

5.インターフェースの徹底

これはLaravelの話ではありませんが、Laravelにはサービスロケータ、DIコンテナ機能が実装されています
先ほどのリポジトリパターンしかり、
テストで都度DBに接続するのもいいですが、実際に実務で行う場合は、
ローカル環境でユニットテスト-> developにPR jenkinsで再びテスト 
というパターンが多いはずです
その度にインターフェースを実装せずにDIも使わずPRの度に関連テストコードを変更しますか?
しないと思います
また突然の仕様変更に対応する為にもインターフェースの無いクラスを
実装するのはLaravelをある程度の規模で利用する場合は良い実装とは言えません

6.キャッシュを制する

これもLaravelの話ではありませんが、
都度DBアクセスする様な実装だったり、レンダリングが常に走る処理だったり、
どこでcacheを利用するか、どこで削除するか等
便利なコンポーネントがありますので利用しない手はありません
また、これもある程度の規模になると、発生するケースですが、
ユーザーデータ系はriakでcacheさせて、
クエリーはmemcached, redisなど
1システムで混在している場合や、そういったアーキテクチャになっている事も多々あります
Laravelにはドライバーがありますので、利用する場所によって変更する事が出来ます
 
        \Cache::driver('memcached1')->get('hoge');
        \Cache::driver('redis1')->get('fuga');

7.Authは自由

Auth関連の拡張等は、新原さんや川瀬さんのエントリにもあります
個人開発以外ではぶっちゃけデフォルトのままで使う事はほとんどありません
(自分だけ?)
この辺りからはマニュアルではなく、実際にフレームワークのソースを読んだりする事が多くなりますが、
実際はパッケージを利用するほどでもありませんので安心してください
複数のユーザー認証パターンや、それぞれ利用するDBが違うなど、なんでも出来ます
Laravel応用 複数の認証クラスを利用

8.コアは直接触らない

composerに慣れしたんでいる方は絶対にやりませんが、
チームの中にフレームワークのコアのソースを直接書き換えて実装してしまう方もいるかもしれません
Laravelにはサービスプロバイダーという、フレームワークの実装そのものを入れ替えてしまう
すばらしい仕組みが用意されています
ある程度の規模の開発になるとサービスプロバイダーなしでは実装できないくらい
入れ替えが必要になるケースがあります
この為にもデフォルトのみで利用するのではなく、きちんと学習する必要が出てきます
チーム全員ではなく、リーダーのエンジニアが教えていくか、
その部分は理解しているエンジニアが実装して、
それからチームのエンジニアが実装していくパターンがベストでしょう
なれてくるとみんな理解していきますので、焦らずに

9.テストコードの無いパッケージは使わない

ide_helperやデバッガーの様にフレームワークそのもののに影響を与えないパッケージは除き、
認証系や拡張系のパッケージにはテストコードの無いものが多くあります
複数人開発で、パッケージで発生するエラーに悩まされるケースも多くあり、
テストコードの無いライブラリはcomposer.jsonも正しく記述されていない事も多いので、
一定のリスクが伴います
テストコードがあるライブラリでも一度テストをしてから導入するのをオススメします


そろそろ疲れてきたところで、自分のディレクトリの考えなどを紹介します
しょぼい内容ですが、参考になるものがあれば盗んでみて下さい!

MVCにこだわらない

Laravelはデフォルトの構造は、あくまで他のフレームワークを使っていた人も簡単にすぐ使えるようになっているだけです
これまでの内容からも分かる通り(くどい?)、
デフォルトのままでは辛くなりますし、生産性もメンテナンス性も厳しくなるでしょう
また構造を変更するのは悪ではなく、作者も

『モデルなんか削除しなよ!』

という位です
Laravelは構造や考え方からユーザーに委ねられているフレームワークです
といってもこれはLaravelに限った話ではありませんし、ある一定のレベルに達したエンジニアの方は
皆さん同じ様に思うはずです(自分は達していませんが・・)
ここで、一般的なデフォルトの構造で実装した場合のパターンを考えてみました


実装イメージとしては大体こうなっていると思います(一般的な実装イメージ)
ルーターがあり、ビフォーフィルターを通ってから
コントローラーでバリデートしたり、モデルでバリデートしたり
色んなパターンがありますが、コントローラーがあり、
コントローラーからモデルを利用して、アフターフィルターを経由してレンダリングをして
コントローラーがレスポンスを返す形です
小さい規模の開発では問題になる事もありませんが、
ある程度の規模になるとファットなコントローラーや、モデルもファットになり、
それに関連する色々なクラスや謎の実装が増えていきます
フレームワークの知識がないメンバーもいるでしょうから、
機能としてはあるものを実装してしまったり、メンバー独自のカスタマイズ機能等も増えるでしょう
実装途中で構造化などを考えるのは結構大変だったりします

ここで最近爆発しているflux等の新しい考えと比較してみます

すでにFluxパターンを調べている方はわかるとおもいますが、
流れが一方向であり、action経由でdispatcherが呼ばれ、
コールバックでストアが作用してイベントが・・
詳しく書くと違う方向に行ってしまうのでこの辺にしますが、
要は新しいデザインパターンでもなんでもなく、処理の流れに名前をつけただけというニュアンスが強い様に思えます
実際にfluxのリポジトリはnode.jsで実装されたdispatcherしかありません
この流れをそのままPHPなどのサーバサイドにもってくるのは試した訳ではありませんが、
同じ様に処理の流れや、LaravelにはEventコンポーネント(dispatcher)がありますので
それを使わない手はないと、再認識するきっかけになりました
バージョン5のフォームリクエストはまさにそのような流れで実装されています
イメージとしては次の様に考えてみました



これまで自分が利用していたリポジトリーパターン風なものも加えると、
dispatcherがrouterからビジネスロジックを記述するサービスレイヤ、
フィルターはもちろんバリデートやviewなども機能として監視されているので、
それを認識して実装する時のイメージです
それぞれが依存し合うのではなく疎結合でありながらもdispatcherで支配するニュアンスです
これを踏まえて自分がここ最近しばらく利用している構造のパターンとしては下記の様になります

app/Appがメインの実装ディレクトリになっていますが、
このようになっています
5にも似た感じになっています(発表されてから良いなーと思って変えました)
元からPSRで利用しているので、5のディレクトリが変更されても全く影響がありません
この場合、それぞれのディレクトリは

Authenticate: 認証関連の実装なので無い場合もあります
Commands: artisanコマンドの実装 登録はサービスプロバイダーのみ
Controllers: コントローラー
Dispatcher: それぞれのdispatcherです
Exception: エクセプション
Middleware: フィルターなど
Providers: 各種サービスプロバイダー
Repositories: リポジトリ・エンティティなど
Requests: バリデータなど(5のフォームリクエストの様なものだと思って下さい)
Service: サービスレイヤー

大体ほぼこの形で開発しています
先ほどのイメージを実際に実装に起こすとサンプルとしては下記の様な実装になります

■ルーター

\Route::group(['namespace' => 'App\Controllers', 'before' => 'request.middleware'], function () {
リクエストでURIがマッチした場合にrouter.matchedイベントが起こります
デフォルトでLaravelに監視されているわけですね
ビフォーフィルターはこの後にイベントとして実行されます

■バリデートイベントを起こす

ビフォーで指定しているので必ず実行されるイベントです
namespace App\Middleware;

use App\Requests\ResponseTrait;

class RequestMiddleware extends AbstractFilter
{

    use ResponseTrait;

    /** @var array  */
    protected $exclude = [
        '_token',
    ];

    /**
     * @return $this
     */
    public function filter()
    {
        if(\Input::get('_return')) {
            return \Redirect::route($this->redirectForm())->withInput();
        }
        return \Event::fire('request.validate', [\Input::except($this->exclude)], true);
    }

}
tokenなど、除外したいものがあればバリデート対象から外します
実装時にルールとして定めているものは、
フォームの戻るボタンは "_return"
登録・確認・実行はそれぞれ
"*.form", "*.confirm", "*.apply"としています
traitではルーターのパターンからその辺りに従って
指定のフォームに戻したりする処理が記述してあるだけです
これでビフォーからイベントを発火させてリクエストに関してバリデートを行う様に指示します

■バリデートdispatcher

class ValidateDispatcher extends AbstractDispatcher
{

    use ValidateRuleTrait, ValidatorTrait, ResponseTrait;

    /**
     * @param array $data
     * @param array $append
     * @return $this
     * @throws \Exception
     */
    public function handle(array $data, array $append = [])
    {
        $rule = $this->getRule();
        $current = \Route::currentRouteName();

        if(array_has($rule, $current)) {
            $validate = $this->validate($data, $current);
            if(!$validate) {
                $target = $this->redirectForm();
                if(\Route::getRoutes()->hasNamedRoute($target)) {
                    return \Redirect::route($target, $append)
                        ->withErrors($this->getErrors())
                        ->withInput();
                }
                throw new \Exception;
            }
        }
    }

}
あとはdispatcherに先ほどのフォームの流れに従って任意の処理を行う様に
実装していきます
ルールだけが記述されたtraitを利用して、
traitでルールのみ定義して、
ルール判定のキーは名前が付けられたルーティングと一致したものが対象として実行されます
これでコントローラーの処理が走る前に任意のルーティングにだけバリデート処理を
一定の流れで実行させます
ほかにもtokenが一致するかどうか、しない場合は任意の画面にリダイレクトさせたり、
システムのイベントにそって管理者にメールを送ったりなど、
分離させて、処理の流れの全体像を作っていきます
これにより、コントローラーの記述は簡単なフォームでは下記の例ぐらいの記述になります

■コントローラー

    /**
     * @return \Illuminate\View\View
     */
    public function getForm()
    {
        $this->title('お申し込み');
        return $this->view('form');
    }

    /**
     * @return \Illuminate\View\View
     */
    public function postConfirm()
    {
        $this->title('お申し込み確認');
        return $this->view('confirm');
    }


    public function postApply()
    {
        $input = \Input::only([
            "name", "company_name", "email"
        ]);
        $this->entry->added($input);
        return $this->view('apply');
    }
セッションの破棄などはアフターフィルターなどでイベントとして処理させたりしていくと
複雑な処理でもサービスレイヤで実装していくので、
見通しもよく、テストも簡単で疎結合になっていきます
view実行時もイベントがありますので、そこでさらに細かく実装する場合もあります

簡単に規模による考え方と、それに関連する事を書かせていただきました
5に移行しなくても、Laravel4でも十分すぎるほどの機能があります
みなさんも自分にあったアーキテクチャをLaravelと一緒に考えてみてください


おわり
comments powered by Disqus

GitHub

Social Links

Author


クリエイティブ・コモンズ・ライセンス
Yuuki Takezawa 作『Ytake Blog』はクリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンス で提供されています。

© ytake/comnect All Rights Reserved. 2014

Fork me on GitHub