はじめに
シンプルなユーザー一覧をサンプルとし、クリーンアーキテクチャをLaravelに適用します。
ディレクトリ構成
Controller、Request、Eloquentについては、Laravelが提供しているものを使用します。
※サンプルで実装を行わない一部のディレクトリについては記載しておりません。
.
├── app
│ ├── Http
│ │ ├── Controllers # Laravel:コントローラー
│ │ └── Requests # Laravel:リクエスト
│ └── Rules # Laravel:バリデーションルール
├── packages # 境界づけられたコンテキスト毎に各層を管理
│ ├── User
│ │ ├── Adapter
│ │ │ ├── Presenter # Presenter層:Presenter, ViewModel
│ │ │ │ ├── ListPresenterInterface.php
│ │ │ │ └── ListPresenter.php
│ │ │ └── ViewModel
│ │ │ └── ListViewModel.php
│ │ ├── Usecase # Application層:Usecase
│ │ │ ├── ListUsecaseInterface.php
│ │ │ ├── ListUsecaseInteractor.php
│ │ │ ├── ListUsecaseRequest.php
│ │ │ └── ListUsecaseResponse.php
│ │ ├── Domain # Domain層:Entity, Value, Repository
│ │ │ ├── Entity
│ │ │ │ ├── User.php
│ │ │ │ └── Users.php
│ │ │ ├── Value
│ │ │ │ └── Name.php
│ │ │ └── Repository
│ │ │ └── UserRepositoryInterface.php
│ │ └── Infrastructure # Infrastructure層:RepositoryImpl, Eloquent
│ │ ├── RepositoryImpl
│ │ │ └── UserRepository.php
│ │ └── Eloquent
│ │ └── User.php
│ ├── ContextX
│ │ ├── Adapter
│ │ ├── Usecase
│ │ ├── Domain
│ │ └── Infrastructure
└── resources
└── view # Laravel:Viewテンプレート
それでは、各層毎に実装を見ていきます。
Request、Rule
LaravelのRequest、Ruleを利用し、バリデーションとバリデーションルールを定義します。
形式チェックなど、基本的なバリデーションを行います。
また、Repositoryを利用し、テーブルから取得したデータを用いて検証を行う必要がある場合は、UsecaseやDomainServiceでチェックを行い、UsecaseResponseを介して、Presenterにエラー情報を渡します。
Controller
LaravelのRequestをUsecaseRequestに詰め替え、Usecaseを実行します。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\User\ListRequest;
use Package\User\Usecase\ListUsecaseRequest;
use Package\User\Usecase\ListUsecaseInterface;
class UserController extends Controller
{
public function index(ListRequest $request, ListUsecaseInterface $interactor)
{
$usecaseRequest = new ListUsecaseRequest($request);
return $interactor->handle($usecaseRequest);
}
}
Application層
UsecaseRequest
UsecaseRequestを定義し、LaravelのRequestをドメインで利用しやすい形に変換、加工を行います。
<?php
namespace Package\User\Usecase;
use App\Http\Requests\User\ListRequest;
class ListUsecaseRequest
{
private $name = null;
public function __construct(ListRequest $request)
{
$this->name = $request->name;
}
public function getName() : string
{
return $this->name;
}
}
Usecase
ユースケースのインターフェースと実装クラスを定義します。
定義したインターフェースと実装クラスは、AppServiceProviderでサービスコンテナに登録します。
<?php
namespace Package\User\Usecase;
use Illuminate\View\View;
use Package\User\Usecase\ListUsecaseRequest;
interface ListUsecaseInterface
{
public function handle(ListUsecaseRequest $request) : View;
}
実装クラスでは、Repository、DomainServiceの呼び出しやトランザクション制御など、ユースケースを達成するために必要な手続きを実行します。
<?php
namespace Package\User\Usecase;
use Illuminate\View\View;
use Package\User\Adapter\Presenter\ListPresenterInterface;
use Package\User\Usecase\ListUsecaseInterface;
use Package\User\Usecase\ListUsecaseRequest;
use Package\User\Usecase\ListUsecaseResponse;
use Package\User\Domain\Repository\UserRepositoryInterface;
class ListInteractor implements ListUsecaseInterface
{
private const PER_PAGE = 20;
private $repository;
private $presenter;
public function __construct(UserRepositoryInterface $repository, ListPresenterInterface $presenter)
{
$this->repository = $repository;
$this->presenter = $presenter;
}
public function handle(ListUsecaseRequest $request) : View
{
$users = $this->repository->findList($request->getName(), self::PER_PAGE);
$response = new ListUsecaseResponse($users);
return $this->presenter->output($response);
}
}
UsecaseResponse
UsecaseからPresenterへ渡すデータ構造を定義します。
<?php
namespace Package\User\Usecase;
use Package\User\Domain\Entity\Users;
class ListUsecaseResponse
{
private $users = null;
public function __construct(Users $users)
{
$this->users = $users;
}
public function getUsers() : Users
{
return $this->users;
}
}
Domain層
Entity
ビジネスデータやビジネスルールを定義します。
<?php
namespace Package\User\Domain\Entity;
use Package\User\Domain\Value\Name;
class User
{
private $id = null;
private $name = null;
public function __construct(int $id, Name $name)
{
$this->id = $id;
$this->name = $name;
}
public function getId() : int
{
return $this->id;
}
public function getName() : Name
{
return $this->name;
}
}
<?php
namespace Package\User\Domain\Entity;
use Illuminate\Support\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
class Users implements \IteratorAggregate
{
private $users = null;
private $paginator = null;
public function __construct(Collection $users, LengthAwarePaginator $paginator) {
$this->users = $users;
$this->paginator = $paginator;
}
public function getIterator() : \ArrayIterator
{
return new \ArrayIterator($this->users->toArray());
}
public function getPaginator() : LengthAwarePaginator
{
return $this->paginator;
}
}
ValueObject
値固有の制約やビジネスルールを定義します。
<?php
namespace Package\User\Domain\Value;
class Name
{
private $value = null;
public function __construct(string $value)
{
if (is_string($value) && mb_strlen($value) <= 50) {
$this->value = $value;
} else {
throw new \InvalidArgumentException();
}
}
public function value() : string
{
return $this->value;
}
}
Repository
Repositoryのインターフェースです。
ORMでのデータ操作はフレームワーク固有のものになりますので、ドメイン層にインターフェースを定義し、Infrastructure層で実装します。
Usecase同様、AppServiceProviderでサービスコンテナに登録します。
<?php
namespace Package\User\Domain\Repository;
use Package\User\Domain\Entity\Users;
interface UserRepositoryInterface
{
public function findList(string $name, int $perPage) : Users;
}
Infrastructure層
RepositoryImpl
Repositoryの実装クラスです。
LaravelのEloquentを利用して、データベースのデータの取得や更新を行います。
処理結果はEntityに入れてUsecaseに渡します。
<?php
namespace Package\User\Infrastructure\RepositoryImpl;
use Package\User\Domain\Repository\UserRepositoryInterface;
use Package\User\Domain\Entity\Users as UsersEntity;
use Package\User\Domain\Entity\User as UserEntity;
use Package\User\Infrastructure\Eloquent\User;
class UserRepository implements UserRepositoryInterface
{
public function findList(string $name, int $perPage) : UsersEntity
{
$pagenator = User::where('name', 'LIKE', "%${name}%")
->paginate($perPage);
$users = $pagenator->map(function (User $user) {
return new UserEntity($user->id, new Name($user->name));
});
return new UsersEntity($users, $pagenator);
}
}
Eloquent
LaravelのORMであるEloquentの実装になります。
<?php
namespace Package\User\Infrastructure\Eloquent;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = ['name'];
}
Adapter層
ViewModel
UsecaseResponseをViewでの表示に合わせて変換、加工を行います。
<?php
namespace Package\User\Adapter\ViewModel;
use Illuminate\Pagination\LengthAwarePaginator;
use Package\User\Usecase\ListUsecaseResponse;
class ListViewModel
{
private $users = [];
private $pagenator = null;
public function __construct(ListUsecaseResponse $response)
{
foreach($response->getUsers() as $user) {
$this->users[] = [
'id' => $user->getId()->value(),
'name' => $user->getName()->value(),
];
}
$this->pagenator = $response->getUsers()->getPagenator();
}
public function getUsers() : array
{
return $this->users;
}
public function getPagenator() : LengthAwarePaginator
{
return $this->pagenator;
}
}
Presenter
Presenterのインターフェースと実装クラスになります。
ViewModelを生成し、生成したViewに渡します。
Usecase同様、AppServiceProviderでサービスコンテナに登録します。
<?php
namespace Package\User\Adapter\Presenter;
use Illuminate\View\View;
use Package\User\Usecase\ListUsecaseResponse;
interface ListPresenterInterface
{
public function output(ListUsecaseResponse $response) : View;
}
<?php
namespace Package\User\Adapter\Presenter;
use Illuminate\View\View;
use Package\User\Adapter\Presenter\ListPresenterInterface;
use Package\User\Adapter\ViewModel\ListViewModel;
use Package\User\Usecase\ListUsecaseResponse;
class ListPresenter implements ListPresenterInterface
{
public function output(ListUsecaseResponse $response) : View
{
$viewModel = new ListViewModel($response);
return app('view')->make('users.index', compact('viewModel'));
}
}
ServiceProvider
インターフェースと実装を結合し、サービスコンテナに登録します。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot() {}
public function register()
{
$this->app->bind(
\Package\User\Adapter\Presenter\ListPresenterInterface::class,
\Package\User\Adapter\Presenter\ListPresenter::class
);
$this->app->bind(
\Package\User\Usecase\ListUsecaseInterface::class,
\Package\User\Usecase\ListInteractor::class
);
$this->app->bind(
\Package\User\Domain\Repository\UserRepositoryInterface::class,
\Package\User\Infrastructure\RepositoryImpl\UserRepository::class
);
}
}
以上がLaravelへのクリーンアーキテクチャの適用になります。