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

Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る (7) Eloquent ORM編

More than 1 year has passed since last update.

はじめに

このエントリーについて

この記事は「Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る」シリーズの一編です。
他の記事は目次からアクセスしてください。

Eloquent は色々できることが多すぎて、とても1エントリーで済ませられそうになかったので、概略だけまとめました。

環境

  • PHP 5.6
  • Laravel 5.5

公式リファレンス

Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans

Eloquent: Relationships - Laravel - The PHP Framework For Web Artisans

Eloquent: Mutators - Laravel - The PHP Framework For Web Artisans

詳細

ガイドライン

  • 特に方針がないのであれば、モデルクラスは Models ディレクトリに置きましょう
  • テーブル名、フィールド名、およびクラス名はできるだけ Laravel の規約に従ってつけましょう
  • 複数件のデータを取得する際は、できるだけスコープメソッドを使いましょう
  • 1件のデータを取得する際は、できるだけ orFail をつけましょう
  • データの追加や登録をする際は、できるだけ fillable または guarded を設定して、一括代入 (mass assignment) するようにしましょう
  • 関連テーブルのデータをループで取得する際は、理由がない限り with メソッドを使って eager loading しましょう
  • 属性を変化させて取得するときは、できるだけアクセサを使いましょう
  • 属性を変化させて更新するときは、できるだけミューテータを使いましょう

テーブル名とクラス名の対応は、基本的には、テーブル名=複数形、クラス名=単数形となります (例: users テーブル、User クラス)が、イレギュラーにも対応していて、(people, Person) とか (children, Child) みたいな組み合わせも可能です (詳しくは inflector/Inflector.php at master · doctrine/inflector · GitHub を参照してください)。

ディレクトリ構造

デフォルトでは、 app 直下に User.php がつくられていて、そのままだと、モデルクラスが多くなってきたときに視認性が悪くなってしまうので、app/Models というディレクトリをつくってモデルクラスはそこに配置するようにします。

Laravel4 では models というディレクトリが存在していたんですが、「モデル」という概念は多岐に渡るため、開発者の、あるいは、アプリケーションの特性によって好きに配置できるよう、撤廃されたという経緯があります。

しかし、そのせいで、app 直下に大量の php ファイルが置いてしまって、IDE などのツリービューでディレクトリ間の移動がやや面倒になってしまったりして、あまりいいことがありません。

それなら、いっそ、Models ディレクトリを復活させて、そちらに入れていくのがいいかな、と思います。

もちろん、設計時にコンテキストなり役割なりで分割し、適切にディレクトリを切って配置するのであれば、その限りではありません。

もし、User.php も Models ディレクトリに移動するなら注意すべきは、他のファイルで参照している箇所があるので、そちらを変更するのを忘れないことです。

参照元ファイルだけ載せておきます。

  • app/Http/Controllers/Auth/RegisterController.php
  • config/auth.php
  • config/services.php
  • database/factories/UserFactory.php
  • routes/channels.php

生成時のコマンドにディレクトリを付与するのを忘れないようにしてください。

$ php artisan make:model Models/Hoge

命名に関するサンプルコード

マイグレーションを使うなら、以下のように実行すると、モデルクラスとセットで生成できます。

$ php artisan make:model Person -m
Model created successfully.
Created Migration: 2016_12_05_151532_create_people_table

Person が people になっています。

生成されたマイグレーションファイルを見ると、

2016_12_05_151532_create_people_table.php
    public function up()
    {
        Schema::create('people', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
        });
    }

主キーが id になっています (よほどの理由がない限り、このままがいいと思います。主キーが id であることによって、様々な場面で記述を省略できるようになりますので)。

timestamps メソッドは、created_at, updated_at を TIMESTAMP 型で登録します。この2つも特に理由がなければこの名前にしておくのがいいでしょう。これらのフィールドが必要なければこの行は削除してください。

その他にも、外部キーの命名規約として、テーブル名の単数形 + _id (上記例では、person_id => people.id と自動的にペアリングされます) というのがありますので、これもできるだけ合わせておくのがいいでしょう。

複数レコード取得のサンプルコード

Eloquent の便利さのひとつにクエリビルダがありますが、柔軟すぎるために、ついついあちこちにクエリメソッドを書いてしまいがちです。

下記の例は、現在ログインしているユーザーと同じ組織に属している別のユーザーを取得する例です。

UserController.php
    public function index()
    {
        $colleagues = User::where('organization_id', Auth::user()->organization_id)->orderBy('created_at', 'desc')->get();
        // $colleagues を使って何かやる
    }

もし、「同僚」を取得する処理が複数ある場合、すべての箇所でこのように書いてしまうと、もし、並び順を変更する必要が生じたときに複数箇所を変えなくてはならなくなります。

なので、この場合は、

UserController.php
    public function index()
    {
        $colleagues = User::ofSameOrganization(Auth::user()->organization)->get();
        // $colleagues を使って何かやる
    }

みたいにして、処理をモデル側に閉じ込めておく方がいいでしょう。

モデル側は、

User.php
    public function scopeOfSameOrganization(Builder $query, Organization $organization)
    {
        return $query->where('organization_id', $organization.id)->orderBy('created_at', 'desc');
    }

となります。

単一レコード取得のサンプルコード

主キーで特定できるレコードは find、それ以外は first というメソッドが用意されていますが、いずれも、見つからなかった場合に 404 HTTPステータスコードを持つ例外 (これがスローされると、即座に 404 のページが表示されます) をスローすることができます。特別なエラーハンドリングをする必要がなければ、積極的に使っていきましょう。

/users/1 という URL が指定されたとき、users.id = 1 のユーザーがデータベースに存在しなければ、404 エラーの画面を表示する、という仕様を満たすには、コントローラー側では find の代わりに findOrFail を呼ぶだけでいいです。

UserController.php
    public function show($id)
    {
        $user = User::findOrFail($id);
        // $user を使って何かする
    }

他にも、firstOrFail, findOrNew, firstOrNew, firstOrCreate などがありますので、状況に応じて適宜使いこなしていけるといいですね。

レコード追加と更新のサンプルコード

レコードの追加と更新は、できるだけ一括代入を使うのがいいと思っていまして、使わないパターンだと以下のようなかんじになるかと思います。

UserController.php
    public function store(Request $request)
    {
        $user = new User;
        $user->name = $request->get('name');
        $user->email = $request->get('email');
        // ...
        $user->save();
    }

このやり方だと、FormRequest と Model のインピーダンスミスマッチをコントローラーで解消する羽目になる上に、仕様変更の際に複数箇所を直すことになる可能性も高くなるので、fillable または guarded プロパティをセットした上で、以下のように一括代入するのをおすすめします。

UserController.php
    public function store(Request $request)
    {
        $user = User::create($request->except(['_token']));
        // ...
    }

create は内部で new -> save しています。

入力パラメータをあれこれ処理してレコード追加の処理を行う必要がある場合は、別途登録用のメソッドを用意するか、変換用のクラスを別途用意するのがいいと思います。

User.php
public function createFromRequest(Request $request)
{
    $params = $request->except(['_token']);
    $attrs = [];
    // あれこれ処理して $params -> $attrs をつくる
}

あるいは、

UserController.php
    public function store(Request $request, UserTransformer $transformer)
    {
        // transform の中であれこれ処理して $request -> $attributes をつくる
        $user = User::create($transformer->transform($request));
        // ...
    }
}

また、更新についても同様で、何らかのプロパティを更新して保存する処理を以下のように書くのではなく、

UserController.php
    public function save(Request $request, int $id)
    {
        $user = User::findOrFail($id);
        $user->name = $request->input('name');
        $user->email = $request->input('email');
        // ...
        $user->save();
    }

同様に fillable あるいは guarded をセットした上で、

UserController.php
    public function store(Request $request)
    {       
        $user = User::findOrFail($id);
        $user->update($request->except(['_token']));
    }

とするのがいいでしょう (update は内部で fill -> save を行っています)。

追加に関しては、通常それほど多くのユースケースはないと思うのでいいとしても、変更に関しては、様々なユースケースで行われることになると思うので、より慎重に、

Controller.php
/**
 * 退会
 */
public function leave(Request $request)
{
    $user = Auth::user();
    $user->leave();
    // ...
}
User.php
public function leave()
{
    $attrs = ['left_at' => Carbon::now()];
    $this->update($attrs);
}

のように、明確な意図を持ったインタフェースを用意して処理を隠蔽することが重要と思います。

eager loading のサンプルコード

いわゆる N+1 問題を避けるために、ループ内で関連テーブルのフィールドを参照する際は、eager loading を使いましょう。

UserController.php
public function index()
{
    $users = User::with('organization')->get();
    // $users を使って何かする
}

users テーブルに紐付く organizations レコードを WHERE IN で取得します。

select * from `users`;
select * from `organizations` where `organizations`.`id` in ('1', '2', '3');

ただ、これでもまだクエリが2回発行されているので、これを1回にしたい、という希望もあるかと思います。その場合は、JOIN を使うのがいいでしょう。

ただし、同一のフィールドがあった場合は一方しか取得できないので、以下のように、取得されるフィールド名にエイリアスを与えてやる必要があります (ここでは users.name, organizations.name という同名のフィールドがあり、両方を取得したいので organizations.name に organization_name という別名を与えています)。

User.php
public function scopeWithOrganization(Builder $query)
{
    return $query->addSelect(['users.*', 'organizations.name as organization_name'])->join('organizations', 'users.organization_id', '=', 'organization.id');
}

これでクエリを1回にできました。

select `users`.*, `organizations`.`name` as `organization_name` from `users` inner join `organizations` on `users`.`organization_id` = `organizations`.`id`;

ちなみに呼び出し側は、

UserController.php
public function index()
{
    $users = User::withOrganization()->get();
    // $users を使って何かする
}

となります。

クエリを2回から1回にすることのもたらすインパクト次第かなぁとは思いますが、要件に応じて、使い分けていきましょう。

アクセサとミューテータのサンプルコード

Eloquent の便利機能のひとつに、柔軟なアクセサとミューテータがあります。

便利は便利なんですが、めったやたらに使うと混乱を招くので、使い所は限定しておくのが無難だと思います。

アクセサ

よくあるコードと名称のペアになっているようなもの、たとえば都道府県や性別などをラップしたクラスをつくり、アクセサではそのインスタンスを取得するようにする、といった場面で効果を発揮するんじゃないかと思います。

Gender.php
<?php

namespace App;

class Gender
{
    private static $list = [0 => '男', 1 => '女'];
    private $code;

    public function __construct($code)
    {
        $this->code = $code;
    }

    public function __toString()
    {
        return self::$list[$this->code];
    }

    public function toCode()
    {
        return $this->code;
    }
}

モデル側では、

User.php
public function getGenderAttribute()
{
    return new Gender($this->attributes['gender']);
}

のように、get + アッパーキャメルケースにしたプロパティ名 + Attribute という名前を与えます。

テンプレートでは、

show.blade.php
性別: {{ $user->gender }}

と書くと、「男」あるいは「女」と表示されます。

コードにアクセスしたい場合は、

show.blade.php
{{ $user->gender->toCode() }}

とすればオッケーです。

他にも、よくある例として、下記のような姓と名を組み合せて氏名を返すようなメソッドをアクセサにする例もありますが、あまりメリットがないと思うので、メソッドでいい気がします。

User.php
public function getFullName()
{
    return $this->last_name . $this->first_name;
}

ミューテータ

反対にミューテータは、値をセットする側の処理で、たとえば、日時型 (データベースでは DATETIME あるいは TIMESTAMP) のデータに Carbon のインスタンスが入れられます。

User.php
public function leave()
{
    $attrs = ['left_at' => Carbon::now()];
    $this->update($attrs);
}

これは、

User.php
public function leave()
{
    $this->left_at = Carbon::now();
    $this->save();
}

と書くのと同じですが、内部で Carbon のインスタンスに対して 'Y-m-d H:i:s' の形式でフォーマットしてから保持します。

まとめ

  • 特に方針がないのであれば、モデルクラスは Models ディレクトリに置きましょう
  • テーブル名、フィールド名、およびクラス名はできるだけ Laravel の規約に従ってつけましょう
  • 複数件のデータを取得する際は、できるだけスコープメソッドを使いましょう
  • 1件のデータを取得する際は、できるだけ orFail をつけましょう
  • データの追加や登録をする際は、できるだけ fillable または guarded を設定して、一括代入 (mass assignment) するようにしましょう
  • 関連テーブルのデータをループで取得する際は、理由がない限り with メソッドを使って eager loading しましょう
  • 属性を変化させて取得するときは、できるだけアクセサを使いましょう
  • 属性を変化させて更新するときは、できるだけミューテータを使いましょう

Eloquent は便利な反面、暗黙の変換やマジックメソッドによって混乱を招くことがあるので、使う際にはチームで方針を決めておくといいんじゃないかと思います。

上のガイドラインは、基本的には、

  • 記述を減らすために Laravel の作法にできるだけ合わせる
  • ドメインルールの意図を明確にするために、処理をモデル側に隠蔽する
  • プロパティの値とそのプリミティブな型をラップするようなクラスとの相互変換のために、アクセサ・ミューテータを使う

という3点に集約されます。

eager loading 以外はお好みでどうぞというかんじですが、ドメイン駆動設計のような、ドメインの複雑さを凝集させるための設計および実装において必ず威力を発揮すると思いますので、有効に活用していきたいですね。

他にもこんなベストプラクティスがあるよ、などコメントや編集リクエストいただけると助かります :bow:

nunulk
PHP, Laravel, オブジェクト指向プログラミング, デザインパターン, リファクタリング, 関数プログラミング, etc.
http://nunulk.hatenablog.com
phper-oop
ペチオブはオブジェクト指向ワーキンググループです。様々なエンジニアの方に参加頂いております。
https://phper-oop.connpass.com/
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
ユーザーは見つかりませんでした