Help us understand the problem. What is going on with this article?

Laravel で Fat Controller を防ぐもうひとつの Tip

この記事について

先日書いたこちらの記事の補遺です。

Laravel で Fat Controller を防ぐ 5 つの Tips - Qiita

Fat Controller になる主な要因として、Model 層が貧弱すぎる、ということが考えられます。本来 Model に書くべき処理を Controller に書いてしまっており、結果的に可読性が悪くなったり、ひとつのユースケースに対する変更が他のユースケースに影響を及ぼしたり、Controller 間でのコピペコードを量産したり、ということが起きます。

と書いておきながら、「本来 Model に書くべき処理を Controller に書いてしまって」いる例を書きそびれたので、それについて補足します。

はじめに

「Model に書くべき処理」とは

本記事では「Model に書くべき処理」を以下のように定義します。

  1. Model の状態を変更する処理
  2. Model の状態による条件分岐
  3. クエリビルディング

1. Model の状態を更新する処理, 2. Model の状態による条件分岐

1, 2はセットで解説します。

いちばん簡単なのは、以下のようなオブジェクトの生成においてひとつずつプロパティをセットする書き方です。プロパティ分行数が増えますので、これをまとめるのは効果があります。

$user = new User();
$user->name = $request->name;
$user->email = $request->email;
// ....
$user->save();

よりは、バリデーションルールを適切に設定した上で以下のようにまとめるのがいいでしょう。

User::create($request->all());

データの変換が必要なら、FormRequest 側に関数をつくって、それにやらせるといいと思います。

もっとドメインに近い例だと、たとえば、TODO アプリケーションでタスクの生成をする処理で、「タスクの初期状態は 'todo' である」というルールがあったとき、

$task = Task::create(['status' => 'todo'] + $request->all());

みたいな処理は、以下のように Model に専用のメソッドをつくって、そこに持たせることができます。

// Controller
$task = Task::createTodo($request->all());

// Model
public static function createTodo(array $attributes) {
    return parent::create([
        'status' => 'todo',
    ] + $attributes);
}

行数自体は減りませんが、Controller のコードを見ると「TODO をつくる」役割に特化した名前があるので、識別しやすくなると思います。

また、同じく TODO アプリケーションで、「完了しているタスクのみ削除可能である」というドメインルールがあったとき、

if ($task->is_complete) {
    $task->delete();
}

みたいに書くよりは、

// Controller
$task->delete();

// Model
public function delete() {
    if ($this->is_complete) {
        parent::delete();
    }
    throw new \DomainException('...');
}

とすると、行数が少なくなります(これには副次的な作用があって、削除可能な条件をさらにメソッド化することによって、条件が変わったときに対処しやすくなります)。条件を満たさない場合にどう振る舞うか、というのはアプリケーションの要件によると思いますが、422 Unprocessable Entity なり、404 Not Found なりを Controller で返すようなフローが必要になるかもしれません。

いまはひとつのプロパティだけなので恩恵は薄いですが、条件が複雑になってくると Model 内に閉じ込めておくほうがコードの重複(と、それによる更新漏れ)を防ぎやすくなると思います。

個人的には、ある処理が行われる条件を満たしているか、という判定はバリデーションで行うのでもいいかな、と思いますが、実行される処理と実行可能条件が近くにあるほうがいいとも思うので、上の例では Model 側に入れています(これが、Controller に対する入力パラメータの状態によって、複数のモデルにまたがる処理全体の実行可否を左右するような場合には、バリデーション側で実装するほうがいい気がします)。

3. クエリビルディング

単純な WHERE , ORDER BY あたりを組み合わせたものはそのままでもいいですが、複雑な条件句があるようなやつはメソッド化して Model に入れたほうがいいと思います。ちょっと例が書きづらいんですが、たとえば、サブタスクを含めた複数の条件に一致するクエリだと、下記のようにずらずらとクエリビルディングが続くことになるでしょう。

$tasks = Task
    ::whereHas('sub_tasks', function ($builder) {
        $builder->where('...')
        // 他にもずらずらと条件がある
        ;
    })
    ->where('...')
    // 他にもずらずらと条件がある
;

これを、

// Controller
$tasks = Task::inSomeState()->get();

// Model
public function scopeInSomeState(Builder $builder) {
    return $builder
        ->whereHas('sub_tasks', ...)
        ->where(...)
    ;
}

みたいにスコープにします。

あるいは普通のメソッドにして、それを呼んでもいいでしょう。

// Controller
$tasks = Task::getInSomeState();

// Model
public static function getInSomeState() {
    return static::whereHas(...)->where(...)->get();
}

個人的にはスコープ化しておくほうが好みではありますが、IDE での補完やジャンプの問題で(PHPDoc に書くことで認識はされますが、定義元にジャンプできるわけではない)避けたい、という方もいると思います。そこらへんはお好みで。

おわりに

「Model に書くべき処理」は他にもあるかと思いますが、どの場合でも他の Tips と同じく「重複することによって不具合の原因になる」ことが問題なので、どこに書くべきかというよりも、意味のある処理のまとまりに名前をつけ、局所化しておく、ということが大事かな、と思います。

他にも「Model に書くべき処理」のパターンがあれば、コメント欄にて教えていただけると助かります :bow:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした