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

Laravelでつくる、深くて表現豊かなモデル

More than 1 year has passed since last update.

概要

本記事は、Laravel/Vue勉強会#1 - connpass での LT 用につくったスライドの内容を、もう少し詳しく文書化したものです。

スライドはこちら。

Implementing deep and eloquent models with Laravel 5.4 // Speaker Deck

ドメイン駆動設計の話もでてきますが、あまり関係はなくて、モデルをどう実装するか、という話です。

プレゼンテーションの趣旨

Laravel で言うところのモデルの大半は、

  • Eloquent\Model を継承した、テーブルと対になったクラス

ですが、それに加えて、

  • Eloquent\Model を継承はするが、テーブルと一対一で対にならないクラス
  • Eloquent\Model を内包するクラス
  • 値オブジェクトなクラス
  • Enum ライクなクラス
  • Fluent を使った動的プロパティを持つクラス
  • フロントエンドとのデータの受け渡しに使う ViewModel クラス

もどんどん使っていきましょう、という趣旨のプレゼンテーションでした。

環境

PHP 7.0.14
Laravel 5.4.25

詳細

深いモデルとは

深いモデルは、ドメインエキスパートの主要な関心事と、それに最も深く関連した知識に関する明快な表現を提供するが、一方で、ドメインの表面的な側面は捨て去るのだ。

「ドメイン駆動設計」Eric Evans

引用はしてみたものの、本記事ではあまり関係はなくて、「明快な表現」(原文では "lucid expression")というのが、eloquent (英単語としての)と近いなぁと思ったので載せたかっただけです。

eloquent [adj.]

expressing what you mean using clear and effective language

Macmillan Dictionary

当初ひとつの概念だったものが、ドメインエキスパートとの会話(要件定義)を進めていく中で、複数の異なる概念で構成されていると分かったとき、深いモデルを手に入れた、ということになるんだと思います。

「ドメイン駆動設計」に中には、例として、貨物輸送のためのアプリケーション開発において、分析の結果、「行程」という概念には「ドア行程」という特化された概念が存在することが明らかになるくだりがあります。

そういった概念は、実装する際に leg.isDoor のようにフラグで管理するようにしてしまうと、あまり明るい未来にはならないよなぁ、と思うわけです。

サンプルコードの仕様

さて、ここでは、とあるアプリケーションで、User モデルを扱うケースを考えてみます。

ユーザー同士を「ペア」という概念で関連付けするようなアプリケーションがあったとします。

それぞれのユーザーは、

・ホスト - もてなす側
・ゲスト - もてなされる側

という概念で区別されますが、永続化されるテーブルはどちらも users テーブルだとします。

Eloquent\Model を継承した、テーブルと対になったクラス

Eloquent\Model を継承して、テーブルと対になったクラスをつくるのは簡単です。

php artisan make:model Hoge とすればスケルトンをつくってくれます。

ここでは、最初から存在している User クラスを例にとります(実際には親クラスは Authenticatable だったりするのですが、分かりやすくするために Model にしています)。

また、デフォルトでは app ディレクトリ直下に配置されますが、ここでは app/Models 以下に配置するようにします( php artisan make:model Models/Hoge と親ディレクトリを指定してやればいいです)。

User.php
<?php

namespace App\Models;

class User extends Model
{
}

たったこれだけ書くだけで、以下の操作が可能になります。

$user = User::find(1); // ID=1 のユーザーを取得
$user->email; // users.email の値を取得

Eloquent\Model を継承はするが、テーブルと一対一で対にならないクラス

$user->isGuest のような制御をあちこちに入れないようにするために、クラスをつくってしまいましょう。

<?php

namespace App\Models;

class Guest extends Model
{
    protected $table = 'users';

    protected static function boot()
    {
        parent::boot();
        static::addGlobalScope('guest', function (Builder $builder) {
            $builder->whereExists(function ($builder) {
                $builder->from('pairs')->whereRaw('pairs.guest_id = users.id');
            });
        });
    }
}

このクラスは、users テーブルのレコードを表現しつつも、過去にゲストとしてペアになったことがある(pairs テーブルにレコードがある)という制約を入れています。

ゲストになったことのないユーザーは、Guest インスタンスにはなれないのです。

この例が適切かどうかはさておき、テーブルと一対一にする必要はないことはお分かりいただけるのではないかと思います。

Eloquent\Model を内包するクラス

一方で、特化された概念ではあるが、継承はさせたくない、というシチュエーションもあるかもしれません(たとえば、データベースからコレクションの取得はできないようにし、プロパティの取得だけに制限したい、など)。

<?php

namespace App\Models;

class Guest
{
    private $user;

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

    public function __get($key)
    {
        return $this->user->$key;
    }
}

この場合の Guest クラスは、User のインスタンスを持っていて、プロパティへのアクセスのみ処理を委譲しています。

値オブジェクトなクラス

上の例は、いわゆるエンティティと呼ばれるモデルですが、ここからはエンティティでないモデルについて見てみます。

値オブジェクトは、プリミティブ型(数値、文字列、日時等)をドメインの要件にしたがって拡張したイミュータブルなクラスです。

ここでは、最終ログイン日時を「N日前、N時間前、N分前、たった今」の4種類で表現するクラスをつくってみます。

ElapsedTime.php
<?php

namespace App\Models;

use Carbon\Carbon;

class ElapsedTime
{
    private $from;
    private $to;

    public function __construct(Carbon $from, Carbon $to)
    {
        $this->from = $from;
        $this->to = $to;
    }

    public function __toString()
    {
        $diff = $this->from->diff($this->to);
        if ($diff->d > 0) {
            return $diff->d . '日前';
        } elseif ($diff->h > 0) {
            return $diff->h . '時間前';
        } elseif ($diff->m > 0) {
            return $diff->m . '分前';
        } else {
            return 'たった今';
        }
    }
}

これを、User クラスから、以下のように生成します。

User.php
    public function getLastLoggedInAtAttribute($value)
    {
        $dt = new ElapsedTime($value, Carbon::now());
        return (string)$dt;
    }

アクセスするときは、

$user->last_logged_in_at

となります。

Enum ライクなクラス

上記で、User というクラスに対して、Guest, Host という特化したクラスをつくる方法を書きましたが、それでも User のインスタンスに対して、どちらのロールなのか、問い合わせたいことがあろうかと思います。

そこで、Enum の出番です。
以下のように、Enum 抽象クラスを実装し、UserRole というクラスをつくって、判別できるようにしてみます。

Enum.php
<?php

namespace App\Models;

abstract class Enum
{
    protected static $list = [];
    protected $value;

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

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

    public static function all()
    {
        return static::$list;
    }

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

これを継承して UserRole クラスをつくります。

UserRole.php
<?php

namespace App\Models;

class UserRole extends Enum
{
    const HOST = 1;
    const GUEST = 2;

    protected static $list = [
        self::HOST  => 'ホスト',
        self::GUEST => 'ゲスト'
    ];
}

Host クラスからは以下のよう使います。

Host.php
    public function getRoleAtAttribute()
    {
        return new UserRole(UserRole::HOST);
    }

hacklang には標準で存在する enum (列挙型)ですが、PHP に搭載されるのはいつになるんでしょうか(SplEnum あるけど)。

Fluent を使った動的プロパティを持つクラス

意外と知られていないようですが、Laravel には Fluent というクラスがありまして、Eloquent\Model でも実装されている(5.4 からは HasAttributes トレイトに分離されましたが)マジックメソッドを使った動的なプロパティを手軽につくれます。

ここでは、テンプレートやAPIのレスポンスへデータを渡すための ViewModel クラスをつくってみます。

namespace App\Http\ViewModels;

use Illuminate\Support\Fluent;

abstract class ViewModel extends Fluent
{
    abstract public function render();
}

これで、ViewModel を継承したクラスをつくると、以下のように使うことができます。

$vm = new ConcreteViewModel(['foo' => $foo, 'bar' => $bar]);

$vm->foo // 実体は $vm->attributes['foo']

フロントエンドとのデータの受け渡しに使う ViewModel クラス

実際に、ViewModel をどのように使うか例を示します。

たとえば、users テーブルには family_name, given_name というふたつのアトリビュートに分割されている氏名を、ビューに渡すときには半角スペースで連結してひとつのプロパティとして渡したい、というケースです。

User クラスにアクセサをつくります。

User.php
    public function getFullNameAttribute()
    {
        return $this->family_name . ' ' . $this->given_name;
    }

このままだと、$user->toArray() あるいは $user->toJson() したときにフルネームは取得できないので、Model の $appends というプロパティに追加してやる必要があります。

それを以下のように ViewModel でやってみます。

UserView.php
<?php

namespace App\Http\ViewModels;

class UserView extends ViewModel
{
    public function render()
    {
        return $this->user->append(['full_name']);
    }
}

これで、レスポンス用のデータに連結された氏名が含まれるようになりましたので、コントローラーでは以下のようにするだけです。

UserController.php
    return (new ViewModel(['user' => $user]))->render();

まとめ

アプリケーションをつくる上で、テーブルと一対一になったモデルだけでなくさまざまなモデルクラスをつくる必要がありますが、Eloquent や Fluent などを活用して、ドメインのモデルを明確に、シンプルに実装していけるといいですね。

例ではシンプルすぎてあまり伝わらなかったかもしれませんが、ドメインが複雑になってくるとその恩恵をビシビシ感じるのではないかと思います。

こんな種類のモデルもあるよ、などありましたらコメントいただけるとうれしいです :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
ユーザーは見つかりませんでした