本プロジェクトでは、成瀬允宣氏著の「ドメイン駆動設計入門」の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)
システム固有の値を表現する「値オブジェクト」
Docker環境で開発
あらかじめ、src
フォルダをVisual Studio Codeで開き、Command PaletteのDev Containers: Reopen in Container
でDocker環境に入っておきます。
値オブジェクトとは
値オブジェクトは、ドメインオブジェクトの中でもライフサイクルの無いオブジェクトです。(例えばPythonなどの)イミュータブルなオブジェクトにイメージが近いと思っています。例えば、"1"という整数値そのものは値を変更することができません。すなわち、"1"という値は永遠に"1"であるという意味において、ライフサイクルが存在しません。その意味で、Chapter3のエンティティと比較されます。
Laravelにおける値オブジェクトの実装方針
まずはValue Objectを表すクラスを作成しますが、Laravelのbuilt-inの機能では、Value Objectを作成する機能が備わっていません。ただし、すでにLaravelで値オブジェクトの作成を簡単に行うパッケージを作成している方がいますので、それをありがたく利用させてもらいます。
ただし、値オブジェクトのクラスの作成だけでは不十分です。
値オブジェクトはChapter3のエンティティ、すなわちLaravelのEloquent ORMで使用されることが想定されます。
そして、Eloquent ORMでは、データベースの保存もその責務としています。
従って、値オブジェクトがデータベースへ保存される際の挙動も、値オブジェクトのクラス作成と共に行なっておくと良いです。
具体的には、値オブジェクトがどのようにデータベースに保存されるか(例えばフルネームを表す値オブジェクトであればファーストネームとラストネームは別々のカラムに保存するのか?等)を定義しておきます。
これは、LaravelのCustom Castsを使用することによって簡潔に実装することができます。
値オブジェクトの作成
まずは値オブジェクトを作成していきます。
先述の通り、Laravel Value Objectsを使用するため、まずはパッケージをインストールします。
composer require michael-rubel/laravel-value-objects
次に、書籍Chapter6で使用されるリスト6.2を参考に、UserName、UserIdクラスを作成します。
UserName 値オブジェクトの作成
以下のコマンドでUserNameクラスを作成します。
php artisan make:value-object UserName
app/ValueObjects/UserName.php
に雛形が作られていると思いますので、リスト6.2を参考に実装していきます。
<?php
declare(strict_types=1);
namespace App\ValueObjects;
use MichaelRubel\ValueObjects\ValueObject;
use Illuminate\Support\Facades\Validator;
/**
* @template TKey of array-key
* @template TValue
*
* @method static static make(mixed ...$values)
* @method static static from(mixed ...$values)
*
* @extends ValueObject<TKey, TValue>
*/
class UserName extends ValueObject
{
private string $username;
protected $rules = [
'username' => 'required|min:3|max:20'
];
/**
* Create a new instance of the value object.
*
* @return void
*/
public function __construct(string $username)
{
$this->username = $username;
$this->validate();
}
/**
* Get the object value.
*
* @return string
*/
public function value(): string
{
return $this->username;
}
/**
* Get array representation of the value object.
*
* @return array
*/
public function toArray(): array
{
return [
"username" => $this->username
];
}
/**
* Get string representation of the value object.
*
* @return string
*/
public function __toString(): string
{
return $this->username;
}
/**
* Validate the value object data.
*
* @return void
*/
protected function validate(): void
{
$validator = Validator::make(
$this->toArray(),
$this->rules
);
$validator->validate();
}
}
コードを見ていきましょう。
declare(strict_types=1);
では、PHPのStrict typingを指定しています。
これにより、引数や戻り値に指定した型と矛盾するコードに対して、TypeErrorが投げられます。
基本に忠実に実装していますが、注目したいのはvalidate
メソッドです。
LaravelではValidatorを使用して値のチェックを行うと楽です。
上記のように'username' => 'required|min:3|max:20'
などとruleを記載してあげることにより、簡潔な実装が可能になります。
正しく動作するか簡単にチェックしてみます(ちゃんとしたテストは先のChapter5に記載があるので、ここでは変数をdumpさせてテストします)。
web.php
を以下のように書き換え、php artisan serve
でサーバーを立ち上げてアクセスしてみます。
<?php
use Illuminate\Support\Facades\Route;
use App\ValueObjects\UserName;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/
Route::get('/', function () {
try {
$username = new UserName('');
} catch (Exception $e) {
dd($e->getMessage());
}
return view('welcome');
});
すると、"The username field is required." // routes/web.php:21
が表示され、UserNameが空であったためにエラーが投げられていることがわかります。これは、ruleのところにrequired
を記載していたからです。
UserName('R')
に変更して同様にチェックすると、"The username field must be at least 3 characters." // routes/web.php:21
が出力されており、最低文字数未満の場合にエラーが出力されていることが確認できます。
UserId 値オブジェクトの作成
同様にUserIdの値オブジェクトを、後々必要となる書籍リスト6.2を参考に作成していきましょう。
php artisan make:value-object UserId
<?php
declare(strict_types=1);
namespace App\ValueObjects;
use MichaelRubel\ValueObjects\ValueObject;
use Illuminate\Support\Facades\Validator;
/**
* @template TKey of array-key
* @template TValue
*
* @method static static make(mixed ...$values)
* @method static static from(mixed ...$values)
*
* @extends ValueObject<TKey, TValue>
*/
class UserId extends ValueObject
{
private string $userId;
protected $rules = [
"userId" => "required"
];
/**
* Create a new instance of the value object.
*
* @return void
*/
public function __construct(string $userId)
{
$this->userId = $userId;
$this->validate();
}
/**
* Get the object value.
*
* @return string
*/
public function value(): string
{
return $this->userId;
}
/**
* Get array representation of the value object.
*
* @return array
*/
public function toArray(): array
{
return [
"userId" => $this->userId
];
}
/**
* Get string representation of the value object.
*
* @return string
*/
public function __toString(): string
{
return $this->userId;
}
/**
* Validate the value object data.
*
* @return void
*/
protected function validate(): void
{
$validator = Validator::make(
$this->toArray(),
$this->rules
);
$validator->validate();
}
}
Custom Castの作成
次にUserNameとUserId用のCastクラスを作成していきます。
まずは、そもそもCastとは何かについて説明します。
LaravelのCustom Castsとは?
LaravelのCustom Castsを用いると、Eloquent Modelのattributeに対して型を指定することができます。
ドキュメントの例を参考に、使用例を見てみましょう。
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class Json implements CastsAttributes
{
/**
* Cast the given value.
*
* @param array<string, mixed> $attributes
* @return array<string, mixed>
*/
public function get(Model $model, string $key, mixed $value, array $attributes): array
{
return json_decode($value, true);
}
/**
* Prepare the given value for storage.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
return json_encode($value);
}
}
<?php
namespace App\Models;
use App\Casts\Json;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'options' => Json::class,
];
}
上記のように、Custom CastsはCastsAttributesを継承し、getおよびsetメソッドを実装する必要があります。
getはデータベースの生値を変換する役割を、setは引数をデータベースの生値に変換する役割を持ちます。
作成したCustom Castのクラスを、使用するModelのcastsプロパティに渡してあげると、Custom Castクラスが使用できるようになります。
なお、Custom Castの実装はValue Objectクラス内に入れることも可能です。
しかしながら、データベースへの保存方法の定義はValue Objectの責務ではないと考え、本記事では別クラスで定義することにします。
UserNameのCustom Castの作成
以下のコマンドで、UserName Castを作成できます。
php artisan make:cast UserName
雛形が作成されていますので、修正していきます。
<?php
namespace App\Casts;
use App\ValueObjects\UserName as UserNameValueObject;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class UserName implements CastsAttributes
{
/**
* Cast the given value.
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return new UserNameValueObject($value);
}
/**
* Prepare the given value for storage.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (!$value instanceof UserNameValueObject) {
throw new InvalidArgumentException(
'The given value is not a UserName Object.'
);
}
return [
'username' => $value->toString()
];
}
}
use App\ValueObjects\UserName as UserNameValueObject;
では、Castクラスと値オブジェクトクラスが同じ名前なので、名前を変えてインポートしています。
まず、下のset関数を見てみます。
データベースに保存される前に、$value
の型がUserNameであることをチェックし、予期せぬ動作を防止します。
そして、returnの部分で、username
というDBのカラムにUserNameオブジェクトの値を保存するようにしています。
なお、ここではusername
はDBにstring型で保存されることを想定しています(Chapter3で実際にschemaを定義します)。
次に上のget関数では、DBから取り出した値が扱いやすくなるようUserNameオブジェクトへの変換を行なっています。
動作確認は、Chapter3で行なっていきます。
UserIdのCustom Castの作成
以下のコマンドで、UserId Castを作成できます。
php artisan make:cast UserId
先ほどと同様に、書籍リスト6.2を参考にUserIdクラスを作成します。
<?php
namespace App\Casts;
use App\ValueObjects\UserId as UserIdValueObject;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class UserId implements CastsAttributes
{
/**
* Cast the given value.
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return new UserIdValueObject($value);
}
/**
* Prepare the given value for storage.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (!$value instanceof UserIdValueObject) {
throw new InvalidArgumentException(
'The given value is not a UserId Object.'
);
}
return [
'user_id' => $value->toString()
];
}
}
まとめ
本記事では、UserNameとUserIdの値オブジェクトを作成し、さらにLaravelのCustom Castを作成して、データベースへの保存とデータベースからの取り出しの動作を定義しました。
これらを使ったUserモデルの作成はChapter3で行っていきましょう。