konbu's blog

PHP/Ruby/Python あたりが仕事で使っている言語です。プログラミング、学習や教育ネタを書いていきます。

Laravel で save メソッド実行時に保存前処理を挟む方法

Laravel 5.6.36 での話。

DB のカラムデフォルト値設定が NULL 且つ、NOT NULL な制約のあるカラムはデフォルトの Laravel と非常に相性が悪く、困る時がある。 DB の設定の問題は DB で解決すべきという事も考えられるが、開発現場の状況によっては (政治的、現場的) 制約が多くあるため、今回は制約を守った状態で対応する場合の話。

Laravel はリクエストを受けつけた時に、未入力 (空文字) のフォームのデータは NULL に変換を掛ける。今回の問題点はこの機能にもある。 詳細は下記ページ「入力のトリムとノーマライゼーション」を参照。

readouble.com

Laravelのデフォルトグローバルミドルウェアスタックには、TrimStringsとConvertEmptyStringsToNullミドルウェアが含まれています。これらのミドルウェアは、App\Http\Kernelクラスにリストされています。これらのミドルウェアは自動的にリクエストの全入力フィールドをトリムし、それと同時に空の文字列フィールドをnullへ変換します。これにより、ルートやコントローラで、ノーマライズについて心配する必要が無くなります。

Laravel の middleware を触れる権限があったり、影響が少ない最初期の開発フェーズなら middleware から外す事で NULL に変換される問題は解決する。

この振る舞いを無効にするには、App\Http\Kernelクラスの$middlewareプロパティからこれらのミドルウェアを削除することにより、アプリケーションのミドルウェアスタックから外してください。

内容

前述した方法で解決できない場合、Controller, Model 等の層で解決する事を求められる。 この問題を解決する方法を考えてみると、いくつか方法が考えられる (清濁考えない場合)。

  1. Controller でそれぞれのリクエストパラメータを取得、加工後 update メソッドなどで流し込む
  2. FormRequest で加工
  3. Model の save メソッドでの保存前に加工する

1. Contoller で加工する

この対応は一番泥臭いやり方かなと思う。 メリットは手が込んでなくて、特別な知識を必要としないため、Laravel の Controller と Request を使った処理が書ければ対応できると思う ($request から input を取得して加工するだけ)。

デメリットは特定の Controller@function に依存するため、似た処理を必要とした際に色々な所に書く必要があり、変更が入ると全てを修正する必要が出る場合があること。

後々の事を考えず、とりあえずの対応をするならこれでも問題はない。

2. FormRequest で加工

日本語ドキュメントでは「フォームリクエスト」と表現されている (FormRequest ではググってもでない...)。

readouble.com

FormRequest は本来バリデータとしての役割を持つが、リクエスト処理を受けつけている都合上、Controller に $request が渡される前に加工する事が可能。 メリットは Controller 内でリクエストデータの確認と加工処理を書き散らす必要がないため、Controller@function で実現したいコードのみが記述され、やりたい事が分かり易くなる (読み易いコードになる)。

デメリットはリクエストデータを勝手に加工するため、第 3 者から見ると単なるバリデータだと思えるのに、実際はデータが変質してしまっているように見える事。

今回のケースで言えばフォームが未入力の場合は NULL が入ると思っていたが、実際は空文字列だったというものだったりする。 意味的には大きく変わりがある訳ではなく、本来 Laravel が勝手に NULL に変更していたものを戻しているだけに思えるが、この変更によって is_null 関数などの明示的な NULL のチェックにかからなくなる。

qiita.com

NULL と空文字の些細な違いではあるが、第 3 者がコードを書く際にどういう考えで書くかを保証しきれない以上、他者が触る可能性のあるデータを、通常の意識外の場所で変更するべきではない。

その意味では、Controller で直接対応している方がまだマシに思える。

3. Model の save メソッドの保存実行前に加工する

今回の問題では保存される際に NULL が明示的に渡される場合が困るという事 (DB 側で制約がかかっており、NULL が入るとエラーが発生するのが困り種) なので、保存時のデータに指定のカラムが NULL で無ければあとはどうでも良いと言える。

プログラミングのテクニックには「フック (Hook)」というものがある。これはフレームワークやライブラリなど、第 3 者に利用を提供するようなものに拡張性を持たせる用途で用意される。

ja.wikipedia.org

フック(Hook)は、プログラム中の特定の箇所に、利用者が独自の処理を追加できるようにする仕組みである。また、フックを利用して独自の処理を追加することを「フックする」という。

処理を追加できる箇所は、元のプログラムの開発者によって、あらかじめ決められている。初期化処理や入出力処理などの直前・直後が対象としてよく選ばれる。

この機能は Laravel にも用意されており、Eloquent (エロクエント) モデルの機能として使用できる。

「イベント」を参照。 readouble.com

Eloquentモデルは多くのイベントを発行します。creating、created、updating、updated、saving、saved、deleting、deleted、restoring、restored、retrievedのメソッドを利用し、モデルのライフサイクルの様々な時点をフックすることができます。イベントにより特定のモデルクラスが保存されたりアップデートされたりするたび、簡単にコードを実行できるようになります。

モデルに dispatchesEvents を設定し、呼び出される Event と Listener を定義して加工処理を書けば終わりとなる。 やり方はオブザーバーを作る場合とイベント、リスナの場合で少し異なるが、大枠は同じ。

結論

この手の問題が起こったときは大元で対処できるならその方が確実 (DB の設定で解決されるものはなるべくアプリに持ち越したくない) なんだけど、そうも行かない気もある。

その際はなるべく現行の実装に影響がない範囲且つ、既存の実装が考慮を漏らしてる場合のケアが一緒にできる位置に入れるとうまくはまる。 ただ、データをアグレッシブに加工するタイプのものの場合はわざと局所的な実装にした方が良いということもあるので、自分が実装するコードがどの範囲に影響を与えるべきか考えた上で、適切な範囲で効力を持つ場所に実装するべきとなる。

また、hook という概念は Laravel 以外にも色々なところで使われるため、何かの処理の前後に処理をねじ込みたいときはまず hook の存在を確かめるようにるといいと思う。