##はじめに
DDDはScalaやJavaで使われることが多く、PHPでは使われてる例が少なかったため、
試行錯誤しながら実装した結果を記載することにしました。
本来では、コードを書く前にユビキタス言語やICONIX、コンテキストマップ等の設計から入るべきですが、シンプルな題材にすることで省略しました。
##なぜLaravelでDDD?
昔はコード設計そこまで意識しておらず、フレームワークに従ってMVCが出来ていればいいやぐらいの気持ちだったのですが、大規模なサービスや変更が多いシステムでは限界を感じることが多々ありました。
ScalaでDDDを経験した際に、システムの拡張が容易で変更に強い設計だと感じてからDDDに興味を持ち、最近使う機会が多いLaravelで実践してみようと思い今回記事にしました。
##題材
今回はユーザを登録するだけのアプリケーションを作ります。
これを元に今後機能を追加していく予定です。
実装
環境
- PHP 7.3
- Laravel 5.8
本記事に記載されているコードはGitHubに置いてます。
ユーザテーブルの作成
ユーザは名前、メールアドレス、パスワードの情報を持ちます。
**マイグレーションのイメージ**
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->timestamps();
});
}
コントローラ
コントローラはリクエストを受け取りサービスに投げるだけに留めてます。
フォームリクエストにバリデーションに委ねることができるため、とてもシンプルになります。
**コントローラーの作成**
/**
* @return View
*/
public function index(): View
{
$users = (new UserService())->list();
return view('user.list', ['users' => $users]);
}
/**
* @return View
*/
public function create(): View
{
return view('user.create', ['user' => \Session::get('_old_input')]);
}
/**
* @param StoreUserPost $request
* @return RedirectResponse
*/
public function store(StoreUserPost $request): RedirectResponse
{
$user = $request->all();
if ((new UserService())->store($user)) {
return redirect('/user/')->with('success_message', trans('message.success_save'));
}
return redirect('/user/')->with('error_message', trans('message.failed_save'));
}
フォームリクエスト
ユーザ入力画面からのリクエストをバリデーションします。
この際に値オブジェクトの定数を使い、ドメイン知識の分散を防ぎます。
ドメイン知識をフォームリクエストで確認するか、値オブジェクトで確認するかはプロジェクト次第なところがあるので、他で確認している場合には省略してもいいかと思います。
**フォームリクエストの作成**
public function up()
/**
* @return array
*/
public function rules(): array
{
$name_between = User\Name::MIN_LENGTH . ',' . User\Name::MAX_LENGTH;
$pass_between = User\Password::MIN_LENGTH . ',' . User\Password::MAX_LENGTH;
return [
'name' => 'required|string|between:' . $name_between,
'email' => 'required|unique:users|email',
'password' => 'required|string|between:' . $pass_between,
];
}
/**
* @return array
*/
public function messages(): array
{
return [
'name.required' => trans('validation.required', ['attribute' => 'ユーザ名']),
'name.string' => trans('validation.string', ['attribute' => 'ユーザ名']),
'name.between' => trans('validation.between', [
'attribute' => 'ユーザ名',
'min' => User\Name::MIN_LENGTH,
'max' => User\Name::MAX_LENGTH
]),
'email.required' => trans('validation.required', ['attribute' => 'メールアドレス']),
'email.unique' => trans('validation.unique', ['attribute' => 'メールアドレス']),
'email.email' => trans('validation.email'),
'password.required' => trans('validation.required', ['attribute' => 'パスワード']),
'password.string' => trans('validation.string', ['attribute' => 'パスワード']),
'password.between' => trans('validation.between', [
'attribute' => 'パスワード',
'min' => User\Password::MIN_LENGTH,
'max' => User\Password::MAX_LENGTH
]),
];
}
値オブジェクト
ユーザの属性を定義します。
エンティティの生成時にタイプヒンティングによって誤設定を防ぎます。
**値オブジェクトの作成**
class Email implements BaseValueObject
{
/**
* @var string
*/
private $email;
/**
* Email constructor.
* @param string $email
* @throws \Exception
*/
public function __construct(string $email)
{
if (!$this->isEmail($email)) {
throw new \Exception(trans('validation.email'));
}
$this->email = $email;
}
/**
* @return string
*/
public function get(): string
{
return $this->email;
}
/**
* @param string $email
* @return bool
*/
private function isEmail(string $email): bool
{
return \Validator::make([$email], ['email'])->passes();
}
}
class Name implements BaseValueObject
{
const MIN_LENGTH = 1;
const MAX_LENGTH = 255;
/**
* @var string
*/
private $name;
/**
* Name constructor.
* @param string $name
* @throws \Exception
*/
public function __construct(string $name)
{
if (!$this->isName($name)) {
throw new \Exception(trans('validation.name'));
}
$this->name = $name;
}
/**
* @return string
*/
public function get(): string
{
return $this->name;
}
/**
* @param string $name
* @return bool
*/
private function isName(string $name): bool
{
$name_between = self::MIN_LENGTH . ',' . self::MAX_LENGTH;
return \Validator::make([$name], ['between:' . $name_between])->passes();
}
}
class Password implements BaseValueObject
{
const MIN_LENGTH = 4;
const MAX_LENGTH = 10;
/** @var string */
private $password;
public function __construct(string $password)
{
$this->password = $this->hash($password);
}
/**
* @return string
*/
public function get(): string
{
return $this->password;
}
/**
* @param string $password
* @return bool
*/
public function isRawPassword(string $password): bool
{
$pass_between = self::MIN_LENGTH . ',' . self::MAX_LENGTH;
return \Validator::make([$password], ['between:' . $pass_between])->passes();
}
/**
* @param string $password
* @return string
*/
public function hash(string $password): string
{
return self::isRawPassword($password) ? Hash::make($password) : $password;
}
}
サービス
リポジトリへのアクセスやビジネスロジックのやりとりに利用します。
**サービスの作成**
class UserService
{
/** UserRepository */
private $user_repos;
public function __construct()
{
$this->user_repos = new UserRepository;
}
/**
* @return Collection
*/
public function list(): Collection
{
return $this->user_repos->list();
}
/**
* @param array $request_params
* @return bool
*/
public function store(array $request_params): bool
{
try {
$user = $this->user_repos->new($request_params);
return $this->user_repos->store($user);
} catch (\Exception $e) {
report($e);
return false;
}
}
}
リポジトリ
ファクトリやモデルへのアクセスに利用し、各サービスからのモデルアクセス方法を統一します。
**リポジトリの作成**
class UserRepository
{
/**
* @param array $request_params
* @return Entities\User
* @throws \Exception
*/
public function new(array $request_params): Entities\User
{
return (new UserFactory)->make(
$request_params['name'],
$request_params['email'],
$request_params['password']
);
}
/**
* @return Collection
*/
public function list(): Collection
{
return User::all();
}
/**
* @param Entities\User $user
* @return bool
*/
public function store(Entities\User $user): bool
{
return (new User([
'name' => $user->getName(),
'email' => $user->getEmail(),
'password' => $user->getPassword()
]))->save();
}
}
ファクトリ
ユーザエンティティを生成するために利用します。
ファクトリがあることで、多少のエンティティの変更はここで吸収できます。
**ファクトリの作成**
class UserFactory
{
/**
* @param string $name
* @param string $email
* @param string $password
* @return User
* @throws \Exception
*/
public function make(string $name, string $email, string $password): User
{
return new User(
new Name($name),
new Email($email),
new Password($password)
);
}
}
エンティティ
同一性を識別するモデルになります。
登録や更新をする場合は、ここで生成されたエンティティを使用することで、不正なデータ登録を防ぎます。
**エンティティの作成**
class User
{
/**
* @var Name
*/
private $name;
/**
* @var Email
*/
private $email;
/**
* @var Password
*/
private $password;
/**
* UserEntity constructor.
* @param Name $name
* @param Email $email
* @param Password $password
*/
public function __construct(Name $name, Email $email, Password $password)
{
$this->name = $name;
$this->email = $email;
$this->password = $password;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name->get();
}
/**
* @return string
*/
public function getEmail(): string
{
return $this->email->get();
}
/**
* @return string
*/
public function getPassword(): string
{
return $this->password->get();
}
}
書いてみた感想
ユーザ登録だけでどんだけコード書くんだ。。と始めは思いましたが、各メソッドのコードはとてもシンプルで改修しやすい作りにできたと思います。
結構考えて実装したのですが、記事を書いている途中でもっといい表現の仕方がありそうだと感じたので、思ったより苦戦してます。
正解がなくおもしろい題材なので、今後も考察を続けていきます。