2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

この記事誰得? 私しか得しないニッチな技術で記事投稿!

「ドメイン駆動設計入門」のLaravelによる実装例:Chapter 2-システム固有の値を表現する「値オブジェクト」

Last updated at Posted at 2023-06-11

本プロジェクトでは、成瀬允宣氏著の「ドメイン駆動設計入門」のLaravel 10を用いた実装例を示します。

当該書籍では、ドメイン駆動設計(Domain Driven Deplopment, DDD)を用いたWEBアプリケーションの開発例が示されており、素晴らしい著作であることは言うまでもありません。
しかしながら実装はC#で示されているため
、例えばPHPフレームワークの代表例であるLaravelなどでDDDによる実装を行いたい場合には、具体的な実装例が分かりづらくなってしまいます。

そこで本プロジェクトでは、「ドメイン駆動設計入門」のLaravelによる実装例を示すことで、Laravel開発者のDDD実装の手助けとなることを目指します。あくまでLaravelの実装例を示すことを目的としていますので、ドメイン駆動設計とは何かについては書籍をお読みください。

これが皆様の開発の手助けとなれば幸いです。ご意見等ございましたら、コメント欄やIssueでお伝えいただければと思います。

目次

Githubにソースコードはアップ済みです。

システム固有の値を表現する「値オブジェクト」

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を参考に実装していきます。

app/ValueObjects/UserName.php
<?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でサーバーを立ち上げてアクセスしてみます。

routes/web.php
<?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
app/ValueObjects/UserId.php
<?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に対して型を指定することができます。
ドキュメントの例を参考に、使用例を見てみましょう。

app/Casts/Json
<?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);
    }
}
app/Models/User.php
<?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

雛形が作成されていますので、修正していきます。

app/Casts/UserName.php
<?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クラスを作成します。

app/Casts/UserId.php
<?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で行っていきましょう。

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?