Laravelを1ヶ月ほど触ってみて

Laravelを使い始めて1ヶ月ほどが経ちました。まだまだ使い込んでる感じではないんですが、今どんな感じで開発してるかとか、やって良かった事・ハマりがちだった事などをまとめます。

今どんな感じで開発しているか

ひとまず落ち着いたフォルダ構成

Laravel5はPSR-4に準拠でフォルダ構成もフレームワークの思想的に強制が弱く自由に構成できます。今はこんな感じになってます。

$ tree -L 3
.
├── Console
│   ├── Commands
│   └── Kernel.php
├── Events
│   └── Event.php
├── Exceptions
│   ├── FatalException.php — (追加)
│   ├── FormRequestValidationException.php — (追加)
│   ├── Handler.php
│   └── ServiceValidationException.php — (追加)
├── Http
│   ├── Controllers
│   │   ├── Admin — (Admin用にフォルダを変更)
│   │   └── Web — (フロント用にフォルダを変更、Api等も別に作成する)
│   ├── Kernel.php
│   ├── Middleware
│   │   ├── xxx.php — (まだあまり触ってない)
│   ├── Requests
│   │   ├── HogeCreateRequest.php — バリデーションは基本ココ
│   │   └── Request.php
│   └── routes.php
├── Jobs
│   └── Job.php
├── Listeners
├── Models
│   └── Hoge.php — (モデルはModelsに置くことにした)
├── Policies
├── Providers
│   ├── AppServiceProvider.php
│   ├── AuthServiceProvider.php
│   ├── CustomHashServiceProvider.php
│   ├── CustomSocialiteServiceProvider.php
│   ├── CustomValidatorServiceProvider.php
│   ├── DevelopmentServiceProvider.php — (debugbar等の開発時に使うものを登録)
│   ├── EventServiceProvider.php
│   └── RouteServiceProvider.php
├── Services — (追加)
│   ├── HogeService.php — (モデルを扱うビジネスロジックを集約する)
│   └── Service.php
├── Support — (追加)
│   ├── Facades
│   │   ├── HogeService.php — (↑を利用するためのFacadeを作成する)
│   │   └── ServiceBaseFacade.php
│   └── helpers.php — (Viewで使うヘルパー関数等)
├── Types
│   └── HogeType.php — (定数化しておくべきコード類を集約する)
└── Utils
├── CustomValidator.php
├── FlashMessage.php
├── Geocoder.php
├── MD5Hasher.php
└── Socialite
├── CustomSocialiteManager.php
└── YahooJpProvider.php — (YahooJpのOpenId連携)

ビジネスロジックの集約

Controllerにビジネスロジックを持つのは普通はバッドプラクティスなのでやらないとして別でビジネスロジックを集約する層をServiceという名称で作りました。具体的な実装方法はLaravelらしくFacadeを使うようにしています。Facade化は思ったより全然簡単なんですね。

Laravel5でカスタムライブラリ使いたい – Qiita

これでFacadeを使うという事でコーディング方法を全体的に統一できますし、Facadeを使ってるのでメソッドの呼び出しの前後に処理を挟むなんて事も簡単にできるようになります。

たとえばこんな感じで間に例外処理を挟むとか。

class ServiceBaseFacade extends Facade
{
    public static function __callStatic($method, $args)
    {
        try {
            return parent::__callStatic($method, $args);
        } catch (ServiceValidationException $e) {
            Log::info($e);
            throw $e;
        } catch (\RuntimeException $e) {
            Log::error($e);
            throw $e;
        }
    }
}

IDEでの補完もFacadeのAliasを設定した上でide-helper:generateをすれば\Hoge::という形であれば補完されるのでコーディング作業上も支障はあまりありません。

バリデーションはFormRequestを利用する

これまでCakePHPとかSails.jsとかを使ってたのでバリデーションはModelに持つイメージだったんですが、LaravelではValidatorは疎になっていてどこで実行しても大丈夫な感じになっています。Laravel5で追加されたFormRequestはバリデーションを行う機能なんですが、これはControllerよりも前で実行されます。なのでちょっと考え方が違いますね。

最初はこのFormRequestがいまいち腹落ちしなくてどうしようかなと思っていたんですが、

  • ルールが用途ごとにまとめられているのは意味がある
  • Controllerの手前に差し込むのがメソッドインジェクションで簡潔にできる(画面とAPIで再利用もし易い)

というメリットがあるのと、ServiceやModelで再利用したい場合もこのFormRequestから取り出せば良いわけなのでFormRequestの作り方次第でどうにでもなるかなと思いました。

ちなみにこのFormRequestなんですが、通常のLaravelのValidatorだとValidationExceptionを送出するんですが、なぜかHttpResponseExceptionを送出します。

FormRequest throws HttpResponseException on Validation errors

ぼくの場合はこんな感じでExceptionの送出処理を変えてます。

abstract class Request extends FormRequest
{
    protected $failedMessage = 'フォームの入力に誤りがあります。';

    protected function failedValidation(ValidatorContract $validator)
    {
        try {
            parent::failedValidation($validator);
        } catch (HttpResponseException $e) {
            if (! $this->ajax() && ! $this->pjax() && ! $this->wantsJson()) {
                Session::flash('flash_message', new FlashMessage($this->failedMessage, 'danger'));
            }
            throw new FormRequestValidationException($this->failedMessage, $validator, $e->getResponse());
        }
    }
}

上記でthrowしてるFormRequestValidationExceptionはLaravelのValidationExceptionを継承したものなのでthrowすると自分でカスタムしなくても元のHandler側でリダイレクトを行ってくれます。

FormRequest後のバリデーションエラー

たとえば楽観的排他制御であったり、二重登録防止処理であったりビジネスロジック上発生するバリデーションエラー的なものを実装する場合は上記のFormRequestValidationExceptionと同じような感じでValidationExceptionを継承したServiceValidationExceptionを送出するようにしました。

class ServiceValidationException extends ValidationException
{
    /**
     * ServiceValidationException constructor.
     *
     * @param string $message
     * @param  \Illuminate\Validation\Validator  $validator
     */
    public function __construct(string $message, Validator $validator = null)
    {
        parent::__construct($validator, null);
        $this->message = $message;
    }
}

こちらのValidationExceptionはRedirect等のHttp層の事は意識したくないので入れてないです。代わりにControllerで再処理してます。

class Controller extends BaseController
{
    use AuthorizesRequests, AuthorizesResources, DispatchesJobs, ValidatesRequests;

    public function callAction($method, $parameters)
    {
        try {
            return parent::callAction($method, $parameters);
        } catch (ServiceValidationException $e) {
            if (! $e->getResponse()) {
                $e->response = $this->buildFailedServiceValidationResponse($e->validator);
            }
            \Session::flash('flash_message', new FlashMessage($e->getMessage(), 'danger'));
            throw $e;
        } catch (ValidationException $e) {
            $failedMessage = 'フォームの入力に誤りがあります。';
            \Session::flash('flash_message', new FlashMessage($failedMessage, 'danger'));
            throw new \App\Exceptions\FormRequestValidationException($failedMessage, $e->validator, $e->getResponse());
        }
    }

    protected function buildFailedServiceValidationResponse(Validator $validator = null)
    {
        $request = request();
        $errors = [];
        if ($validator) {
            $errors = $validator->getMessageBag()->toArray();
        }
        return redirect()->back()
                         ->withInput($request->input())
                         ->withErrors($errors);
    }
}

ページごとのJavaScriptをWebpackでモジュール管理

以前書いたんですが、Laravel ElixirとWebpackを組み合わせて使ってます。

Laravel ElixirをWebpackと組み合わせて使う

今回はSPA的なページもあるのでReact等も使う予定ですが、別でjQueryを使う通常のページも多々ありモジュール管理にWebpackを使って複数のJavaScriptを出力しています。モジュール管理を使うとrequire/importでNPMのモジュールを使いまわしたり、自分で作ったモジュールを再利用したりできるのでとても便利です。

module.exports = {
  web: {
    entry: {
      web: './resources/assets/js/web/web.js',
      page1: './resources/assets/js/web/page1.js',
      page2: './resources/assets/js/web/page2.js',
      vendor: ['jquery', 'bluebird', 'lodash', 'dropzone', 'blueimp-load-image']
    },
    output: {
      filename: 'web/[name].js'
    },
    plugins: [
      new webpack.DefinePlugin({
        SOCKETIO_ENDPOINT: JSON.stringify(env.SOCKETIO_ENDPOINT)
      }),
      new webpack.optimize.CommonsChunkPlugin({
        names: ['vendor']
      })
    ],
    module: {
      loaders: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          loader: 'babel',
          query: {
            cacheDirectory: true,
            plugins: ['transform-runtime'],
            presets: ['es2015']
          }
        }
      ]
    }
  },
  admin: {
	// 省略
  },
  version: [
    'js/web/web.js',
    'js/web/page1.js',
    'js/web/page2.js',
    'js/web/vendor.js',
 	...
  ]
};

View側では下記のような感じでページごとのJSを読み込ませてます。ちなみに共通で読み込む必要があるものはlayoutのほうで読み込ませています。

@section('scripts')
<script src="{{ elixir('js/web/page1.js') }}"></script>
@endsection

使ってよかったもの

  • barryvdh/laravel-ide-helper
  • barryvdh/laravel-debugbar

通常の開発だと上記の2つがとても役立ってます。テストはまだちょっとしか書けてないので、Mockryくらいしかまだ使ってないです。

ハマりがちな事

Laravelで開発してるとたまにモーレツにハマります。Laravelはphp artisan optimizeでクラスローディングを最適化してくれるんですが、この最適化が開発中はソースの実状とズレる事が多々ありクラスが見つからなかったりメソッドが見つからなかったりでドハマリします。たとえば下記のような場合に起こりがちでした。

  • マイグレーションファイルを後から修正(クラス名変更)
  • Model間のリレーションを追加する
  • Providerの処理を追加する

マイグレーションファイルの修正

特にマイグレーションファイルを後から修正しようとするとクラスが見つからない的なエラーになってよくハマってました。今でもハマってます。確認するポイントが幾つかあるので備忘録的にまとめておきます。下記は開発環境での話です。

  • create_hoge_table等のファイル名にクラス名とのズレがないか確認する
  • migrationsテーブルのレコードのcreate_hoge_table等の内容にクラス名とのズレがないか確認する
  • php artisan optimizeをする

今後触ってみたいもの

Middlewareは少し触ったんですがやはり便利なのでここをもう少し利用していきたいのと、あとPoliciesフォルダが空っぽなんですがどう使えるかまだ分かってないので調べておきたいです。