本プロジェクトでは、成瀬允宣氏著の「ドメイン駆動設計入門」のLaravel 10を用いた実装例を示します。
当該書籍では、ドメイン駆動設計(Domain Driven Deplopment, DDD)を用いたWEBアプリケーションの開発例が示されており、素晴らしい著作であることは言うまでもありません。
しかしながら実装はC#で示されているため
、例えばPHPフレームワークの代表例であるLaravelなどでDDDによる実装を行いたい場合には、具体的な実装例が分かりづらくなってしまいます。
そこで本プロジェクトでは、「ドメイン駆動設計入門」のLaravelによる実装例を示すことで、Laravel開発者のDDD実装の手助けとなることを目指します。あくまでLaravelの実装例を示すことを目的としていますので、ドメイン駆動設計とは何かについては書籍をお読みください。
これが皆様の開発の手助けとなれば幸いです。ご意見等ございましたら、コメント欄やIssueでお伝えいただければと思います。
目次
Githubにソースコードはアップ済みです。
- Chapter 0: 開発環境のセットアップ
- Chapter 1: ドメイン駆動設計とは -> 書籍をお読みください。
- Chapter 2: システム固有の値を表現する「値オブジェクト」
- Chapter 3: ライフサイクルのあるオブジェクト「エンティティ」
- Chapter 4: 不自然さを解決する「ドメインサービス」
- Chapter 5: データにまつわる処理を分離する「リポジトリ」->ここ
- Chapter 6: ユースケースを実現する「アプリケーションサービス」(TDD)
- Chapter 7: 柔軟性をもたらす依存関係のコントロール (TDD)
- Chapter 8: ソフトウェアシステムを組み立てる (TDD)
- Chapter 9: 複雑な生成処理を行う「ファクトリ」(TDD)
- Chapter 10: データの整合性を保つ (TDD)
- Chapter 11: アプリケーションを1から組み立てる (TDD)
- Chapter 12: ドメインのルールを守る「集約」(TDD)
- Chapter 13: 複雑な条件を表現する「仕様」(TDD)
- Chapter 14: アーキテクチャ (TDD)
- Chapter 15: ドメイン駆動設計のとびらを開こう (TDD)
Chapter 5: データにまつわる処理を分離する「リポジトリ」
Docker環境で開発
あらかじめ、src
フォルダをVisual Studio Codeで開き、Command PaletteのDev Containers: Reopen in Container
でDocker環境に入っておきます。
リポジトリの作成はLaravelに必要か?
「ドメイン駆動設計入門」ではリポジトリを作成しています。
リポジトリの責務は「データの永続化と再構築」で、データベースの操作などを行います。
これらの操作を抽象化したクラスをリポジトリとして用意することで、例えばMySQLからSQLiteへの変更などが生じた際のソフトウェア全体でのコード変更を最小限にとどめることができるようになり、ソフトウェアに柔軟性をもたらします。
ところで、Laravelでアプリケーションを構築する際にはリポジトリクラスを作成する必要があるのでしょうか?
このような疑問が生じる背景には、LaravelがEloquent ORMを採用しているためにデータベース操作が既に抽象化されているという事実があります。
Laravelでは、すでにEloquent ORMによってデータベース操作が抽象化されており、例えば開発中でMySQLからSQLiteへの変更など、データベースの変更があったとしてもコードの変更は不要です。
したがって、リポジトリの作成は不要でしょう。
同じような意見はこちらでも指摘されています。
テスト用のデータベースを作成する
本説では、テスト用のデータベースを用意し、テストを行なっていきます。
テストの実行方法
あらかじめ、web.phpの中身を元に戻しておきます(Exampleのテストで/
へのroutingがテストされるため)。
Route::get('/', function () {
return view('welcome');
});
データベースファイルなどを変更している場合には、以下でマイグレーションを行なっておきます。
php artisan test --migrate-configuration
以下で、もともと用意されているtests/Feature/ExampleTest.php
とtests/Unit/ExampleTest.php
が実行されます。
php artisan test
PASS Tests\Unit\ExampleTest
✓ that true is true 0.02s
PASS Tests\Feature\ExampleTest
✓ the application returns a successful response 0.57s
Tests: 2 passed (2 assertions)
Duration: 0.82s
SQLiteを使用したテスト環境のセットアップ
「ドメイン駆動設計入門」で示されているように、インメモリデータベースであるSQLiteを使用してテストを行うための設定を行なっていきましょう。
まず、phpunit.xml
を開き、<env name="DB_DATABASE" value=":memory:"/>
と<env name="DB_CONNECTION" value="sqlite"/>
を追加します。
phpunit.xml
に作成された環境変数(<env name="foo" value="bar"/>
等)は、テスト実行時に読み込まれます。
ここで、DB_DATABASE
の値を:memory:
とすることによって、データベース用のファイルを作成せずにインメモリのデータベースでテストを動作させることができます。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage/>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
</phpunit>
UserName 値オブジェクトのテスト
Chapter 2で作成したUserName 値オブジェクトの動作確認を簡易なもので済ませてしまっていたので、書籍に記載はありませんが、ここでしっかりとテストを作成しておきましょう。
以下でテストを作成します。
php artisan make:test UserNameTest
ここでは、テストをfeature testとして作成しています。
プロジェクトでデフォルトで作成されているように、Laravelのテストフォルダはtests/Unit
とtests/Feature
の2つが用意されています。
make:test
で-u
オプションをつけると、tests/Unit
フォルダの方にテストが作成されますが、特に指定しない場合はtests/Feature
フォルダの方に作成されます。
一般的に、ユニットテストでは1つのクラスのみをテスト対象とする場合に使用し、フィーチャーテストは複数のクラスが使用される複雑なテストを対象とする場合に使用します。
ここで、値オブジェクトは他のクラスやデータベースには依存しないと判断して、ユニットテストを作成すると問題が生じます。
Laravelのテストでは、Facade(Laravelの機能群)を使用する場合はfeature test、しない場合はunit testで使い分けます。
デフォルトではunit testでFacadeが使われない設定になっているため、Facadeを使用するか否かでテストディレクトリを分けた方がいいでしょう。
今回の値オブジェクトはFacadeを使用しているので、フィーチャーテストとして作成します。
作成されたtests/Feature/UserNameTest.php
を以下のように編集します。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\ValueObjects\UserName;
use Exception;
class UserNameTest extends TestCase
{
public function test_empty_input(): void
{
$this->expectException(Exception::class);
new UserName('');
}
public function shortNames(): array
{
return [
['a'],
['ab']
];
}
/**
* @dataProvider shortNames
*/
public function test_short_name(string $name): void
{
$this->expectException(Exception::class);
new UserName($name);
}
public function longNames(): array
{
return [
['UE69KHLM3Q8BzwM6ZDAYa'], // 21 letters
['UE69KHLM3Q8BzwM6ZDAYad'], // 22 letters
];
}
/**
* @dataProvider longNames
*/
public function test_long_name(string $name): void
{
$this->expectException(Exception::class);
new UserName($name);
}
public function validNames(): array
{
return [
['ReiRev'],
['abc'], // 3 letters
['eFaNtc7hZMRXn8Ud7y2g'], // 20 letters
['れいれぶ']
];
}
/**
* @dataProvider validNames
*/
public function test_valid_name(string $name): void
{
$username = new UserName($name);
$this->assertEquals($name, $username->toString());
$this->assertEquals($name, $username->value());
$this->assertEquals(['username' => $name], $username->toArray());
}
}
いくつかピックアップして内容を見ていきましょう。
テストを実行する関数は、publicな関数にし、名前はtest_*
とするようにします。
基本的には$this->assertEquals
を使用して、ある値が所望の値かどうかをチェックします(test_valid_name
関数など)。
もし、エラーが生じるかどうかを確かめたい場合には、エラーが生じる箇所の前に$this->expectException(Exception::class);
を書いておけば良いです。
Exception
のインポートは忘れずにしておきましょう。
また、複数の入力パターンに対してテストを実行したい場合には、PHP Unit Testのdata providerを使用することができます。
まず、複数の入力パターンをshortNames
のように関数の形で定義し、それを入力として受けるテストの関数(ここではtest_short_name
)に、@dataProvider shortNames
のようなアノテーションをつけることで、複数の入力に対するテストを記述することができます。
php artisan test
で、テストが全てパスすることを確認しましょう。
ユーザー作成処理のテスト
書籍リスト5.17のユーザー作成処理のテストを作成してみましょう。
以下のコマンドでUserTest.php
を作成します。
php artisan make:test UserTest
作成されたtests/Feature/UserTest.php
を以下のように編集します。
書籍に加えて、必要そうなテストを追加で作成しています。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
use App\ValueObjects\UserName;
use Exception;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_create(): void
{
$username = new UserName('ReiRev');
$user = User::create(['username' => $username]);
$head = User::first();
$this->assertEquals($user['username'], 'ReiRev');
$this->assertEquals($head['username'], 'ReiRev');
}
public function test_duplicate_user_create(): void
{
$username = new UserName('ReiRev');
$user = User::create(['username' => $username]);
$this->expectException(Exception::class);
$user = User::create(['username' => $username]);
}
}
use RefreshDatabase;
をクラス内に記述しておくことで、各テストごとに、すなわち各テストの関数の実行ごとにデータベースがリセットされます。
以下でテストを実行し、問題なくテストが動作することを確認します。
php artisan make:test