Laravelを1年ほど使って得た知見

[Laravel Advent Calendar 2017](https://qiita.com/advent-calendar/2017/laravel) 24日目の記事になります。

# Laravelを1年ほど使って得た知見

- Laravelは使いやすいフレームワーク
- でも、使いやすさに甘んじると途端にパフォーマンス劣化して死ぬぞ
- レイヤの分離はしっかりと出来ているので、すべきことを意識して使えば回避は可能
- 個別のコードは読みやすいと思います。F/Wのコードを読んで適切に使いましょう

## はじめに

- 主に5.2系での知識となります
    - 前日に滑り込みしたこともあり、5.3系以降を調べてというのは断念しました

----
## Middlewareはアクセス管理に便利

- みなさん、Middleware使っていますか？
- [@mpywさんが書かれていました](https://qiita.com/mpyw/items/13b04f80f6355b30215a)が、Routingのグルーピングはとても便利です。
- `Routing::group`は特定の権限グループを表現することに非常に適しています
- 同一の権限を有する場合は認証処理の後に適切な権限をチェック
    - 実際にロジックを持つのはPolicy層やService層であるでしょう
    - また、データの取得結果は`Request::merge`を使うことで後続処理に引き継げます
    - `$request`を持ちうるMiddleware層ならではのパワフルな機能です

```php
$request->merge([
    'foo' => 'bar',
]);
```

- 架空のマーケットプレイスで例示すると下記の様な4つの権限が出現するでしょう

### 管理者

- 新規店舗の認証
- 各店舗の売り上げの把握
- 悪質な店舗の取り締まり

### 店舗管理者

- プロフィールの編集
- 商品の陳列

### 登録ユーザ

- 商品の閲覧
- 購買
- 購買履歴の閲覧

### 未登録ユーザ

- 商品の閲覧
- ユーザ登録

----

## Request層にバリデーションを任せる

- コントローラ層へのアクセスをする前にバリデーションを終わらせましょう
    - ただし、リクエスト層のパフォーマンス劣化はベンチに引っかかりにくいものです
- 今年は下記の2つのバリデーションルールでパフォーマンス劣化を経験しました
    - いずれもPOSTされたデータが巨大であったためです

### existsルール

- existsルールは都度レコードが存在するかチェックを行います
- そのため、発行回数が予測できない場合は避けた方がよいでしょう
- 回避策として`Request::validationData`で既存のIDのリストを作成してinルールに置き換えるといったことを行いました

### sometimesルール

- これはパフォーマンス劣化を起こしそうにありませんが、下記の理由で劣化を招きます
    1. `sometimes`メソッド呼ばれる
    2. `explodeRules`メソッドが経緯によらず呼ばれる
    3. パラメータのキーがワイルドカード指定されている場合に`each`が呼ばれる
    4. `each`メソッドが`Arr::dot`を利用する
    5. `Arr::dot`は再帰処理を行うため、階層が深い場合にハングする
- 回避策としてワイルドカードの指定を避け、ループカウンタを用いてバリデーションルールを適用するといったことを行いました

----
## Controller層はシンプルに保つ

- ファットコントローラは避けましょう
- 自身のコントローラの役割は下記の様なものだけとしています
    - サービス層からデータを取得
    - 更新処理であればトランザクションの制御とデータの更新
- 同じアクションだが権限が違うといったケース
    - 継承でなく、ルーティングを用いて権限チェックだけを変えるといったことをしました
- ちなみに`Connection::transaction`は複数DBのロールバックをサポートしません
    - 個別にトランザクション処理を行うメソッドを用意すると幸せになれるでしょう

----
## Facadeのスタティックアクセスでは参照渡しが使えない

- Facadeを通して提供されたメソッドは下記の理由で参照渡しでは無くなります
    - `Facade::__callStatic` が参照渡しをせずに元のメソッドを呼び出す
    - 結果、与えられた引数への変更は`__callStatic`で止まってしまう
    - オブジェクトであれば問題ありません
    - 自分がハマったのはRDBMSの取得結果をキャッシュする配列を渡していたため

----
## Service層はビジネスロジックを置き、一つのことをうまくやる

- データストアへのアクセス、ビジネスロジックだけを置く様に心がけています
    - リポジトリ層があればデータストアへのアクセスはそちらに任せられますね
- とはいえ、サービス層の肥大化や複雑化は相変わらず難しい問題です
- これらの問題を解決するために一つのことをうまくやることを心がけています
- `Eloquent`は遅いので、パフォーマンスがシビアに要求される箇所では`Connection::select`を使いましょう
    - 3倍程度の高速化が見込めます
    - ただし、モデルのコレクションでなくstdClassの配列が返されます

----
## Model層にデータ自身が持つべき振る舞いを閉じ込める

- モデル層はエンティティであることを自覚しましょう
    - EloquentとつながっているのでDBを意識してしまうが、振る舞いの置き場と限定してしまえば幸せになれます
- `Model::newCollection`をオーバーライドしてカスタムコレクションを返却できます
- カスタムコレクションにソートやフィルタ等のメソッドを置くといったことを行っています
- `updating`や`updated`は呪いと化す恐れがあるので使わない様にしています
    - 便利ですが、ストアド同様に気付きにくいバグを生む恐れがあるためです
- また、`Model::insert`に配列を食わすとバルクインサートを発行してくれます
    - saveは不要なクエリ発行をしないのでありがたいですが、大量データのINSERTのみ発生するのであれば、適用を検討する価値はあると思います。

----
## Collectionは便利だが遅い

- 配列の方がパフォーマンスははるかに高いです
- パフォーマンスが要求される場面では配列を使いましょう
- `Collection::push`し続ける処理は配列を作成して`collect($foo)`に置き換えると3枚程度、処理が高速になります
    - `Connection::select`が高速なのはこのため
- たとえば下記の様な処理な置換が可能です 

```php
// これは遅い
$list = collect();
foreach (range(1, 100) as $i) {
    $list->push($i);
}

// こうしたほうが高速
$list = [];
foreach (range(1, 100) as $i) {
    $list[] = $i;
}
$list = collect($list)
```

----
## 最後に

- 前日に空きが出ているのに気付いて駆け足になりましたが参加できてよかったです