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

Laravelで作るアプリケーションとドメインを(ついでにinfraも)独立させたパターン

こんにちは、kazuheiです。Laravel Advent Calender 2019 8日目の記事です。

この記事について

2日目の@nunulkさんが Laravel のモデルクラスをどこに配置するか問題について考えてみるという記事を書いていらして、そこに「パターン4: アプリケーションと独立した Domain」というのが紹介されており、メリデメを書いて欲しいとありました。ちょうど自社で開発するM&Aクラウドというサービスがそのような設計方針を採用していたので紹介させていただきます。

なぜそのような設計にしているのか

このような設計にしたい理由は2点です。

  • データベースへのアクセスをするクラスとロジックを持つクラスを明確に分ける
  • LaravelのDIコンテナを利用して外部のデータソースへのアクセスを抽象化し、レイヤーを分けた設計をする

スクリーンショット 2019-12-08 10.34.06.png

従来のPHPフレームワークでのMVCでは、viewに依存しないロジックを共有するための置き場が必然的にModelになってしまい、Modelがデータアクセスとロジックの2つの役割を持ってしまいます。さらに、複数のModelに関係のあるロジックを書くときに、どのModelにロジックを書けばよいかわらないといった問題も起きます。また、Model自身がデータをすべて持ってしまっているため、Model内のロジックを更に他のクラスに分解するということが難しいため、Modelがどんどん大きくなっていきます。Model内のロジックを別々のクラスに切り出すことができないので、複数のModel同士でのロジックの共有がしづらくなります。ModelをDomainとしてアプリケーションのロジック上必要な単位に組み直すことで、細かくクラス分けができ、ロジックの共有をしやすくなります。

データソースへのアクセスを抽象化すると、EloquentModelからQueryBuilderに実装を変更する。DBに直セスアクセスするかCacheを通すようにするか選べるようにする。アクセスするデータソースを安全に切り替えられる、などのメリットがあります。

スクリーンショット 2019-12-08 10.34.17.png

これにより当初の目的が達成されます。

概念的なモデルの方はドメインという名前をつけドメインクラスと呼ぶことにし。Modelの方はEloquentModelとしてデータベースにアクセスする便利な機能として位置づけます。

実現方法

実現方法の例として、Userというドメインのみがあるアプリケーションを考えます。実用性はないですが…。appにはユーザーに対するアプリケーションとしてのインターフェースを、domainにはロジックを、infraにはデータソースへのアクセスの実装を書いていきます。実際の開発しているサービスはappの部分にさらに細かい役割のクラス郡がありますが、今回はモデルをどうしているかという話なので割愛します。

ディレクトリ配置例

./app
├── Console
│   └── Commands
├── Exceptions
├── Http
│   ├── Controllers
│   ├── Middleware
│   └── Requests
└── Providers
    └── RepositoryServiceProvider.php

./domain
├── Base
│   ├── BaseId.php
│   ├── BaseStringValue.php
│   └── BaseTime.php
├── Common
│   ├── Email.php
│   ├── RawPassword.php
│   └── HashedPassword.php
└── User
    ├── User.php
    ├── UserDomainService.php
    ├── UserId.php
    ├── UserList.php
    ├── UserMailer.php
    └── UserRepository.php

./infra
├── EloquentModel
│   └── UserModel.php
└── EloquentRepository
    ├── Cached
    │    └── UserRepository.php
    └── UserRepository.php

またnamespaceを効かせるためにcomposer.jsonに設定を追加しています。

composer.json
{
    ... 省略 ...
    "autoload": {
        "classmap": [
            "database/seeds",
            "database/factories"
        ],
        "psr-4": {
            "App\\": "app/",
            "Domain\\": "domain/",
            "Infra\\": "infra/"
        }
    },
    ... 省略 ...
}

autoloadのpsr-4にDomainとInfraを指定し、namespaceを追加します。

app

WebアクセスかConsoleのコマンドかに関係なくDomainのロジックを呼び出して、ユーザーのリクエストに答えるのがappです。

RepositoryServiceProviderでドメインのインターフェースに対してどの実装を使うかを設定できます。

app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Domain\User\UserRepository;
use Illuminate\Support\ServiceProvider;
use Infra\EloquentRepository\UserRepository as EloquentUserRepository;

class RepositoryServiceProvider extends ServiceProvider
{
    public function boot()
    {

    }

    public function register()
    {
        $this->app->bind(UserRepository::class, EloquentUserRepository::class);
    }
}

domain

domainはロジックを書く場所としていて、その実現方法はinfraに任せるように実装します。
domain内はフレームワークにも依存しない純粋なPHPのクラスのみになっています。baseに基本的なクラスのabstractクラスを、Commonに共通で使うクラスを、置いています。クラスをEmailやRawPassword、HashedPasswordなどの細かいクラス単位に分けることによって、メールアドレスの整合性やPasswordの検証などのロジックを細かいクラス単位に持たせることができます。ユーザーの種類が複数あるアプリケーションの場合は当然ここらへんのロジックを使い回せるわけです。

domain/User/User.php
<?php

namespace Domain\User;

use Domain\Common\Email;
use Domain\Common\HashedPassword;

class User {

    private $id;
    private $email;
    private $hashedPassword;

    public function __construct(
        UserId $id,
        Email $email,
        HashedPassword $hashedPassword
    )

    public function checkPassword(Hasher $hasher, RawPassword $rawPassword): bool
    {
        return $this->hashedPassword->check($hasher, $rawPassword);
    }
}

domain/Common/HashedPassword.php
<?php

namespace Domain\Common;

use Domain\Base\BaseStringValue;
use Illuminate\Contracts\Hashing\Hasher;

class HashedPassword extends BaseStringValue
{
    public function check(Hasher $hasher, RawPassword $rawPassword): bool
    {
        return $hasher->check($rawPassword->rawValue(), $this->rawValue());
    }
}

また、リポジトリーパターンを導入し、データソースへのアクセスはInterfaceをDomain側に、実装をInfra側に置いています。

domain/User/UserRepository.php
<?php

namespace Domain\User;

use Domain\Common\Email;

interface UserRepository
{
    public function get(UserId $id): User;

    public function getByEmail(Email $email): User;
}

infra

infraには外部のデータソースにアクセスする実装を置きます。実装するそれぞれのクラスはDomain側のinterfaceを実装しています。

infra/EloquentModel/UserRepository.php
<?php

namespace Infra\EloquentRepository;

use Domain\Common\Email;
use Domain\User\User;
use Domain\User\UserId;
use Domain\User\UserRepository as UserRepositoryInterface;


class UserRepository implements UserRepositoryInterface
{

    public function get(UserId $id): User
    {
        $model = UserModel::where('id', $id->rawValue())
            ->first();
        if (!$model) {
            throw new NotFoundException();
        }

        return $model->toDomain();
    }

    public function getByEmail(Email $email): User
    {
        $model = UserModel::where('email', $email->rawValue())->first();
        if (!$model) {
            throw new NotFoundException();
        }

        return $model->toDomain();
    }
}

EloquentModelには自分自身をDomainクラスに変換する関数をもたせています。

infra/EloquentModel/User.php
<?php

namespace Infra\EloquentModel;

use Domain\User\User as UserDomain;

class User {

    public function toDomain(): UserDomain;
    {
        return new UserDomain(
            new UserId($this->id),
            new Email($this->email),
            new HashedPassword($this->password),
        );
    }
}

メリット、デメリット

この設計でのメリットとデメリットを紹介します。

メリット

当初の目的通り、ロジックとデータアクセスを分離して、ロジックのみのクラスを作ることができることが大きいです。これにより細かいクラスを作ってロジックを分解し、使い回すことができます。

また、レイヤーが分かれることにより、データアクセスについて実装が抽象化されるので、複数の実装を持つことができ、コンテキストによりすげ替えたり、実装を安全に移行したりできます。例えば、ユーザーからのアクセスではCacheを使ったアクセスをしたいが管理画面からはCacheを使わないデータアクセスをしたい、などのニーズにコンテキストに合わせてRepositoryServiceProviderで使うRepositoryの実装クラスを代えることで対応できます。

デメリット

デメリットは

  • EloquentModelからDomainクラスへのデータをマッピング大変
  • EloquentModelがただのDaoになっているので、機能を活かしきれない

といったようなところだと思います。

アプリケーションが複雑になり、Domain側のロジックが増えれば増えるほど、Infra側の面倒くささは相対的に少なくなってくるので、こっちについては諦めていいかなと思っています。あと、正直このパターンだったらもはやEloquentは使わなくても良いともいえます。

おわりに

Laravelは規約のないフレームワークなので、自分でディレクトリ分けやレイヤー分けを設計しなければいけず、その割にはこういうふうにしたほうが良いよ、という情報がまだまだ少ないと思うので、もっと色んな人にこのテーマについて書いて欲しいな〜と思っています。自分も初めてLaravelを使ったときはだいぶ手探りでした。

以前これに近いテーマでLTしたときの資料があるので、ぜひ見ていただければと思います。
https://speakerdeck.com/kazuhei0108/sabisukontenafalseshi-jian-de-nahuo-yong?slide=23

設計の参考にした書籍

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
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