6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel+CleanArchitecture

Last updated at Posted at 2022-09-05

はじめに

シンプルなユーザー一覧をサンプルとし、クリーンアーキテクチャを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を実行します。

UserController.php
<?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をドメインで利用しやすい形に変換、加工を行います。

ListUsecaseRequest.php
<?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でサービスコンテナに登録します。

ListUsecaseInterface.php
<?php

namespace Package\User\Usecase;

use Illuminate\View\View;
use Package\User\Usecase\ListUsecaseRequest;

interface ListUsecaseInterface
{
    public function handle(ListUsecaseRequest $request) : View;
}

実装クラスでは、Repository、DomainServiceの呼び出しやトランザクション制御など、ユースケースを達成するために必要な手続きを実行します。

ListInteractor.php
<?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へ渡すデータ構造を定義します。

ListUsecaseResponse.php
<?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

ビジネスデータやビジネスルールを定義します。

User.php
<?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;
    }
}
Users.php
<?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

値固有の制約やビジネスルールを定義します。

Name.php
<?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でサービスコンテナに登録します。

UserRepositoryInterface.php
<?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に渡します。

UserRepository.php
<?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の実装になります。

User.php
<?php

namespace Package\User\Infrastructure\Eloquent;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = ['name'];
}

Adapter層

ViewModel

UsecaseResponseをViewでの表示に合わせて変換、加工を行います。

ListViewModel.php
<?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でサービスコンテナに登録します。

ListPresenterInterface.php
<?php

namespace Package\User\Adapter\Presenter;

use Illuminate\View\View;
use Package\User\Usecase\ListUsecaseResponse;

interface ListPresenterInterface
{
    public function output(ListUsecaseResponse $response) : View;
}
ListPresenter.php
<?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

インターフェースと実装を結合し、サービスコンテナに登録します。

AppServiceProvider.php
<?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へのクリーンアーキテクチャの適用になります。

6
3
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
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?