1
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?

More than 3 years have passed since last update.

【Laravel】簡単なアプリをクリーンアーキテクチャっぽくしてみた

Posted at

この記事は、PHPのフレームワークLaravelを使って作ったかんたんなサンプルアプリを、クリーンアーキテクチャの原則にできるだけしたがって書き直してみることで、クリーンアーキテクチャの理解を深めることを目的にしています。

クリーンアーキテクチャに登場する概念を、具体的なコードを見ることでざっくり理解することを目指す記事です。

サンプルアプリの画面と説明

index.png

今回サンプルとしてつくったアプリはこちらです。
https://github.com/yatsurugi55/laravel-clean-architecture

このサンプルアプリはLaravelで作りまして、「データの作成、編集、削除ができて、データが表として表示される」、というだけのかんたんなものです。

一応、製品の在庫を管理するようなものをイメージしてつくったので、**「name」には商品名が、「stock」**にはその在庫数が表示されるようなイメージです。

編集したいときは**「Edit」を押して編集画面に遷移し、在庫数なり商品名なり編集し、削除したいときは「Delete」を押して削除画面に遷移後、さらに「Enter」**を押して削除する、といった使い方を想定しています。

新しい行を追加したいときは**「Create」**をおして新規作成画面から作成できます。

サンプルとしてつくったこのアプリは処理はコントローラーだけでやっており、もちろんこの規模であればそれで問題ないわけですが、このコードをクリーンアーキテクチャっぽくかきかえてみよう、というのが今回の試みです。

編集画面

edit.png
さきほどの画像はデータを表示する画面ですが、こちらは編集画面。

削除画面

delete.png
こちらは削除画面。

作成画面

create.png
こちらは作成画面。

コード(修正前)

ここでは、クリーンアーキテクチャなど特に意識せず、テキトーにつくった場合のコードをはります。

まずルートは以下のような感じ。

routes/web.php
Route::get('/product', 'App\Http\Controllers\ProductController@index');
Route::get('/edit/{id}', 'App\Http\Controllers\ProductController@edit');
Route::post('/update/{id}', 'App\Http\Controllers\ProductController@update')->name('update');
Route::get('/create', 'App\Http\Controllers\ProductController@create');
Route::post('/store', 'App\Http\Controllers\ProductController@store')->name('store');
Route::get('/delete/{id}', 'App\Http\Controllers\ProductController@delete');
Route::post('/remove/{id}', 'App\Http\Controllers\ProductController@remove')->name('remove');

コントローラーも以下で全量です。
DBを参照したり更新したりして、Viewを呼び出すだけなのでシンプルです。

app/Http/Controllers/ProductController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;
use DB;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::all()->toArray();

        return view('product', compact('products'));
    }

    public function edit(Request $request)
    {
        $product = Product::find($request->id);

        return view('edit', compact('product'));
    }

    public function update(Request $request, int $id)
    {
        $product = Product::find($id);

        $product->name = $request->name;
        $product->type = $request->type;
        $product->made_in = $request->made_in;
        $product->stock = $request->stock;
        $product->save();

        return redirect(url('/product'));
    }

    public function create()
    {
        return view('create');
    }

    public function store(Request $request)
    {
        Product::create([
            'name' => $request->name,
            'type' => $request->type,
            'made_in' => $request->made_in,
            'stock' => $request->stock,
        ]);

        return redirect(url('/product'));
    }

    public function delete(Request $request)
    {
        $product = Product::find($request->id);

        return view('delete', compact('product'));
    }

    public function remove(Request $request)
    {
        $product = Product::find($request->id);

        $product->delete();

        return redirect(url('/product'));
    }
}

以下はViewから一部抜粋です。

resources/views/product.blade.php
<h2>Index</h2>
<table border="1">
    <tr>
        <th>id</th>
        <th>name</th>
        <th>type</th>
        <th>made_in</th>
        <th>stock</th>
        <th>edit</th>
        <th>delete</th>
    </tr>
    @foreach ($products as $product)
        <tr>
            <td>{{ $product['id'] }} </td>
            <td>{{ $product['name'] }}</td>
            <td>{{ $product['type'] }}</td>
            <td>{{ $product['made_in'] }}</td>
            <td>{{ $product['stock'] }}</td>
            <td>
                <a href="/edit/{{ $product['id'] }}">
                    <button type="button" class="btn btn-primary">Edit</button>
                </a>
            </td>
            <td>
                <a href="/delete/{{ $product['id'] }}">
                    <button type="button" class="btn btn-primary">Delete</button>
                </a>
            </td>
        </tr>
    @endforeach
</table>
resources/views/edit.blade.php
<h2>Edit</h2>
<form action="{{ route('update', ['id' => $product->id]) }}" method="POST"/>
    @csrf
    <div class="form-group row">
        <label class="col-sm-2">id</label>
        <div class="col-sm-10">{{ $product->id }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">name</label>
        <input type="text" name="name" class="form-control col-sm-10" value="{{ $product->name }}">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">type</label>
        <input type="text" name="type" class="form-control col-sm-10" value="{{ $product->type }}">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">made_in</label>
        <input type="text" name="made_in" class="form-control col-sm-10" 
value="{{ $product->made_in }}">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">stock</label>
        <input type="number" name="stock" class="form-control col-sm-10" 
value="{{ $product->stock }}">
    </div>
    <button type="submit" class="btn btn-primary">Enter</button>
</form>
resources/views/delete.blade.php
<h2>Delete</h2>
<form action="{{ route('remove', ['id' => $product->id]) }}" method="POST"/>
    @csrf
    <div class="form-group row">
        <label class="col-sm-2">id</label>
        <div class="col-sm-10">{{ $product->id }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">name</label>
        <div class="col-sm-10">{{ $product->name }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">type</label>
        <div class="col-sm-10">{{ $product->type }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">made_in</label>
        <div class="col-sm-10">{{ $product->made_in }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">stock</label>
        <div class="col-sm-10">{{ $product->stock }}</div>
    </div>
    <button type="submit" class="btn btn-primary">Enter</button>
</form>
resources/views/create.blade.php
<h2>Create</h2>
<form action="{{ route('store') }}" method="POST"/>
    @csrf
    <div class="form-group row">
        <label class="col-sm-2">name</label>
        <input type="text" name="name" class="form-control col-sm-10">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">type</label>
        <input type="text" name="type" class="form-control col-sm-10">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">made_in</label>
        <input type="text" name="made_in" class="form-control col-sm-10">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">stock</label>
        <input type="number" name="stock" class="form-control col-sm-10">
    </div>
    <button type="submit" class="btn btn-primary">Enter</button>
</form>

主なコードは上記ですべてですので、非常にコンパクトですね。

ここからは、クリーンアーキテクチャなるものを理解するため、そちらに話にうつります。

クリーンアーキテクチャとは

https___qiita-image-store.s3.amazonaws.com_0_293368_f9f3dee0-8ea8-8cc9-2f9b-311f238b3db5.jpeg
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

上はクリーンアーキテクチャを示す有名な図だそうですが、クリーンアーキテクチャとは、コードを読みやすく、修正やテストをしやすくするためのソフトウェア設計方針のことで、具体的にはコードを役割ごとにわけ、それらのつながりを密なものではく疎なものにし、依存関係をつける場合にもその対象と方向には制限をつけます。

コードを役割ごとにわけるとはつまり、「これは何をするクラスですか?」ときかれたときに「これは〇〇するクラスです」と端的にこたえられる状態にすることだとおもいます。

オブジェクト指向などともつながりの深い概念ではないかとおもいます。

僕はこの図を始めてみたとき、**「ControllersはMVCフレームワークのコントローラーのことだろうな」とか、「WebやUIというのはViewのことなんだろうな」**といったことはイメージできましたが、この絵が全体としてなにを意味しているのかよくわからず、実際の設計につかうのは難しそうだな、とおもっていました。

そんな折、以下の画像をみつけ(※参考記事1)、個人的にはこちらのほうが実際のコードとの対応がわかりやすく非常に参考になったので、この記事でもこの画像を土台にして説明していきたいとおもいます。

https___qiita-image-store.s3.amazonaws.com_0_293368_905ab8b4-4c6b-5fd8-0446-f1473be5cc00.jpeg

#クリーンアーキテクチャなかんじにする
ここからはさきほどのコードを修正していきます。

具体的には先ほどのコントローラーを、DBの処理を担当する部分(Infrastructure)や、アプリケーションを担当する部分(UseCase)などにわけて、またインタフェースをつくってできるだけ具体的なものに依存しないようにする、などといったことをやっていきます。

上記の画像で箱の右上に <I> とかかれているのはインタフェースを示しており、 コントローラーがアプリケーションである UseCaseInteractor を呼び出すときや、またそこからDBにアクセスする DataAccess を呼び出すときはそれぞれインタフェースに依存するよう実装することを示します。

箱の右上に <DS> とかかれているのはデータオブジェクトを示しており、レイヤーをまたいでデータを受け渡しするときにはこの箱にいれてやりとりします。

まずは、これからつくる Entities や UseCase などといったものを配置する場所を用意するため、参考記事※1を真似てプロジェクトディレクトリの直下に packages というディレクトリを用意します。

Entities

Entities2.png

Entitiesは以下のディレクトリに配置します。

  • packages/Domain/Domain

今回は、製品にあたる Product をエンティティとすることにし、上記ディレクトリ配下に Product ディレクトリを作成します。

図をみると、Entities に対して2本の線がつながっており、これらはアプリケーションの実装である UseCaseInteractor と、DBアクセスを担当する DataAccess のインタフェース DataAccessInterface が依存していることを示します。

この依存関係はたとえば、**「Product を生成する」という処理を担当する UseCaseInteractor が Product に依存したり、「Product をInsertする」**という処理を担当するDataAccessのインタフェースが Product に依存したりする様子をあらわしています。

ここでは、Product は id や name などの属性を持つクラスとして定義します。

packages/Domain/Domain/Product/Product.php
<?php

namespace packages\Domain\Domain\Product;

class Product
{
    private $id;
    private $name;
    private $type;
    private $made_in;
    private $stock;

    public function __construct(
        int  $id,
        string $name,
        string $type,
        string $made_in,
        int $stock)
    {
        $this->id = $id;
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMadeIn(): string
    {
        return $this->made_in;
    }

    public function getStock(): int
    {
        return $this->stock;
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function setType(string $type)
    {
        $this->type = $type;
    }

    public function setMadeIn(string $made_in)
    {
        $this->made_in = $made_in;
    }

    public function setStock(int $stock)
    {
        $this->stock = $stock;
    }
}

DataAccessInterface

DataAccessInterface1.png

次に、Entities に依存しているものをみていきます。

これは、DBアクセスを担当するDataAccessのインタフェースです。
今回のアプリでは、DB上の Product を生成したり編集したりといった機能を実装するため、それらメソッド名をこのインタフェースに書きます。

このインタフェースを実装する DataAccess では、SQL を書くなり、クエリビルダなどをつかってDBに関する記述をしていきます。

図をみると、UseCaseInteractor がこの DataAccessInterface に依存しています。
コントローラーからは直接つながっていないので、コントローラーは UseCaseInteractor を呼び、そのUseCaseInteractor の中から DataAccessInterface を通じて DataAccess が呼ばれるという流れが確認できます。

UseCaseInteractor が直接 DataAccess を呼び出すのではなく、そのインタフェースを呼び出すかたちにすることで依存のしかたに制限をかけているわけですね。

今回は以下のような名前にします。

  • packages/Domain/Domain/Product/ProductRepositoryInterface.php
packages/Domain/Domain/Product/ProductRepositoryInterface.php
<?php

namespace packages\Domain\Domain\Product;

interface ProductRepositoryInterface
{
    public function index(): array;

    public function findById(int $id): Product;

    public function update(Product $product);

    public function store(Product $product);

    public function remove(int $id);

    public function createId(): int;
}


#DataAccess

DataAccess1.png

次は上記の DataAccessInterface を実装する DataAccess です。

該当するのは以下です。

  • packages/Infrastructure/Product/ProductRepository.php

ここではクエリビルダを使ってDBにアクセスしています。
たとえば findById() メソッドは、指定された id をプロダクトをDBから取得し、その Product を返します。
store() は新規作成されるときに呼ばれますが、こちらは Product をうけとって、それを登録します。

packages/Infrastructure/Product/ProductRepository.php
<?php

namespace packages\Infrastructure\Product;

use DB;
use packages\Domain\Domain\Product\Product;
use packages\Domain\Domain\Product\ProductRepositoryInterface;

class ProductRepository implements ProductRepositoryInterface
{
    public function index(): array
    {
        $results = DB::table('products')->get()->toArray();

        $products = array_map(
            function ($product) {
                return new Product(
                    $product->id,
                    $product->name,
                    $product->type,
                    $product->made_in,
                    $product->stock
                );
            },
            $results
        );
        return $products;
    }

    public function findById(int $id): Product
    {
        $product = DB::table('products')->find($id);

        return new Product(
            $product->id,
            $product->name,
            $product->type,
            $product->made_in,
            $product->stock
        );
    }

    public function update(Product $product)
    {
        DB::beginTransaction();

        DB::table('products')
            ->where('id', $product->getId())
            ->update([
                'name' => $product->getName(),
                'type' => $product->getType(),
                'made_in' => $product->getMadeIn(),
                'stock' => $product->getStock(),
            ]);

        DB::commit();

        return;
    }

    public function store(Product $product)
    {
        DB::beginTransaction();

        DB::table('products')->insert([
            'id' => $product->getId(),
            'name' => $product->getName(),
            'type' => $product->getType(),
            'made_in' => $product->getMadeIn(),
            'stock' => $product->getStock(),
        ]);

        DB::commit();

        return;
    }

    public function remove(int $id)
    {
        DB::beginTransaction();

        DB::table('products')->where('id', $id)->delete();

        DB::commit();

        return;
    }

    public function createId(): int
    {
        $id = DB::table('products')->max('id');

        return $id + 1;
    }
}

UseCaseInteractor

UseCaseInteractor.png

次は、Entities に依存しているもうひとつの、 UseCaseInteractor です。
このひとはたくさん線がつながっていてややこしそうです。

まず UseCaseInteractor がなにとつながっているのか整理しましょう。

  • Entities に依存
  • DataAccessInterface に依存
  • InputData に依存
  • OutputData に依存
  • InputBoundary に抽象化
  • OutputBoundary に依存(今回は利用していない)

UseCaseInteractor がやっていることは、コントローラからデータを InputData として受け取り、Entities を生成し、それを参照したり登録したりするために DataAccessInterface を介して DataAccess に処理を依頼することです。
処理した結果は OutputData というかたちで、OutputBoundary を呼び出すことで Presenter に渡されます。

コントローラから UseCaseInteractor を呼ぶときには、これを抽象化した InputBoundary というインタフェースを通じて呼ばれます。

今回作成した UseCaseInteractor は以下です。
「Productを取得する」とか、「Productを新規登録する」などの処理ごとに UseCaseInteractor を作成しました。

  • packages/Domain/Application/Product/ProductIndexInteractor.php
  • packages/Domain/Application/Product/ProductGetInteractor.php
  • packages/Domain/Application/Product/ProductUpdateInteractor.php
  • packages/Domain/Application/Product/ProductStoreInteractor.php
  • packages/Domain/Application/Product/ProductRemoveInteractor.php

まず ProductIndexInteractor です。

これは Product の一覧を取得する UseCase です。
リポジトリから Product の配列を取得して、それを ProductModelの配列として OutputData にのせてコントローラーに返します。

packages/Domain/Application/Product/ProductIndexInteractor.php
<?php

namespace packages\Domain\Application\Product;

use packages\Domain\Domain\Product\Product;
use packages\Domain\Domain\Product\ProductRepositoryInterface;
use packages\UseCase\Product\Commons\ProductModel;
use packages\UseCase\Product\Index\ProductIndexUseCaseInterface;
use packages\UseCase\Product\Index\ProductIndexOutputData;

class ProductIndexInteractor implements ProductIndexUseCaseInterface
{
    private $repository;

    public function __construct(ProductRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function handle()
    {
        $products = $this->repository->index();

        $productModels = array_map(
            function ($product) {
                return new ProductModel(
                    $product->getId(),
                    $product->getName(),
                    $product->getType(),
                    $product->getMadeIn(),
                    $product->getStock());
            },
            $products
        );

        return new ProductIndexOutputData($productModels);
    }
}

次は ProductGetInteractor です。
これは編集画面を表示するときに呼ばれます。

packages/Domain/Application/Product/ProductGetInteractor.php
<?php

namespace packages\Domain\Application\Product;

use packages\Domain\Domain\Product\Product;
use packages\Domain\Domain\Product\ProductRepositoryInterface;
use packages\UseCase\Product\Get\ProductGetUseCaseInterface;
use packages\UseCase\Product\Get\ProductGetOutputData;

class ProductGetInteractor implements ProductGetUseCaseInterface
{
    private $repository;

    public function __construct(ProductRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function handle(int $id)
    {
        $product = $this->repository->findById($id);

        return new ProductGetOutputData(
            $product->getId(),
            $product->getName(),
            $product->getType(),
            $product->getMadeIn(),
            $product->getStock()
        );
    }
}

次は ProductUpdateInteractor です。

これは編集を実行するときに呼ばれます。
リポジトリから Product をうけとり、それを編集してリポジトリの update を呼びます。

packages/Domain/Application/Product/ProductUpdateInteractor.php
<?php

namespace packages\Domain\Application\Product;

use packages\Domain\Domain\Product\Product;
use packages\Domain\Domain\Product\ProductRepositoryInterface;
use packages\UseCase\Product\Update\ProductUpdateInputData;
use packages\UseCase\Product\Update\ProductUpdateUseCaseInterface;

class ProductUpdateInteractor implements ProductUpdateUseCaseInterface
{
    private $repository;

    public function __construct(ProductRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function handle(ProductUpdateInputData $inputData)
    {
        $product = $this->repository->findById($inputData->getId());

        $product->setName($inputData->getName());
        $product->setType($inputData->getType());
        $product->setMadeIn($inputData->getMadeIn());
        $product->setStock($inputData->getStock());

        $this->repository->update($product);

        return;
    }
}

次は ProductStoreInteractor です。
これは新規登録の際に呼ばれます。

登録するデータは InputData を介して受け取り、それをもとに Product を生成してリポジトリに生成を依頼します。

packages/Domain/Application/Product/ProductStoreInteractor.php
<?php

namespace packages\Domain\Application\Product;

use packages\Domain\Domain\Product\Product;
use packages\Domain\Domain\Product\ProductRepositoryInterface;
use packages\UseCase\Product\Store\ProductStoreInputData;
use packages\UseCase\Product\Store\ProductStoreUseCaseInterface;

class ProductStoreInteractor implements ProductStoreUseCaseInterface
{
    private $repository;

    public function __construct(ProductRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function handle(ProductStoreInputData $inputData)
    {
        $id = $this->repository->createId();

        $product = new Product(
            $id,
            $inputData->getName(),
            $inputData->getType(),
            $inputData->getMadeIn(),
            $inputData->getStock(),
        );

        $this->repository->store($product);

        return;
    }
}

最後は ProductRemoveInteractor です。

これは削除実行時に呼ばれます。
InputData を介して削除対象の id をうけとり、その削除をリポジトリに依頼します。

packages/Domain/Application/Product/ProductRemoveInteractor.php
<?php

namespace packages\Domain\Application\Product;

use packages\Domain\Domain\Product\Product;
use packages\Domain\Domain\Product\ProductRepositoryInterface;
use packages\UseCase\Product\Remove\ProductRemoveInputData;
use packages\UseCase\Product\Remove\ProductRemoveUseCaseInterface;

class ProductRemoveInteractor implements ProductRemoveUseCaseInterface
{
    private $repository;

    public function __construct(ProductRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function handle(ProductRemoveInputData $inputData)
    {
        $this->repository->remove($inputData->getId());

        return;
    }
}

InputBoundary

InputBoundary.png

InputBoundary は2か所から線がつながっています。
ひとつは順番が前後しますが、さきほどの UseCaseInteractor を抽象化した線です。
InputBoundary は UseCaseInteractor のインタフェースです。

もうひとつはコントローラーからの線で、コントローラーは InputBoundary に依存します。
コントローラーは UseCaseInteractor に処理を依頼するわけですが、それは UseCaseInteractor を直接呼び出して実現するのではなく、それらのインタフェースである InputBoundary を呼び出すことで抽象的なものに依存するようにしているというわけですね。

  • packages/UseCase/Product/Index/ProductIndexUseCaseInterface.php
  • packages/UseCase/Product/Get/ProductGetUseCaseInterface.php
  • packages/UseCase/Product/Update/ProductUpdateUseCaseInterface.php
  • packages/UseCase/Product/Store/ProductStoreUseCaseInterface.php
  • packages/UseCase/Product/Remove/ProductRemoveUseCaseInterface.php

まずは ProductIndexUseCaseInterface です。

packages/UseCase/Product/Index/ProductIndexUseCaseInterface.php
<?php

namespace packages\UseCase\Product\Index;

interface ProductIndexUseCaseInterface
{
    public function handle();
}

次は ProductGetUseCaseInterface です。

packages/UseCase/Product/Get/ProductGetUseCaseInterface.php
<?php

namespace packages\UseCase\Product\Get;

interface ProductGetUseCaseInterface
{
    public function handle(int $id);
}

次は ProductUpdateUseCaseInterface です。

packages/UseCase/Product/Update/ProductUpdateUseCaseInterface.php
<?php

namespace packages\UseCase\Product\Update;

interface ProductUpdateUseCaseInterface
{
    public function handle(ProductUpdateInputData $inputData);
}

次は ProductStoreUseCaseInterface です。

packages/UseCase/Product/Store/ProductStoreUseCaseInterface.php
<?php

namespace packages\UseCase\Product\Store;

interface ProductStoreUseCaseInterface
{
    public function handle(ProductStoreInputData $inputData);
}

次は ProductRemoveUseCaseInterface です。

packages/UseCase/Product/Remove/ProductRemoveUseCaseInterface.php
<?php

namespace packages\UseCase\Product\Remove;

interface ProductRemoveUseCaseInterface
{
    public function handle(ProductRemoveInputData $inputData);
}

InputData

InputData.png

InputData は2か所から依存されています。
ひとつはコントローラーから、もうひとつは UseCaseInteractor からです。
箱の右上の <DS> はこの箱がデータオブジェクトであることを示し、InputData はデータのやりとりのために利用されます。

コントローラーは InputBoundary というインタフェースを呼び出して、その実装である UseCaseInteractor に処理を依頼しますが、コントローラーは Request などでデータを扱うこともあるとおもいます。
こういったものを InputData として用意することで、 UseCaseInteractor に渡します。

今回のアプリでいえば、Product を作成したり編集したりするときのデータは View からコントローラーに送られるわけですが、それらをコントローラーの中で InputData としてパックして、その処理を UseCaseInteractor に依頼するかたちになっています。

InputData は以下です。

  • packages/UseCase/Product/Update/ProductUpdateInputData.php
  • packages/UseCase/Product/Store/ProductStoreInputData.php
  • packages/UseCase/Product/Remove/ProductRemoveInputData.php

まず ProductUpdateInputData です。
これは、Product の編集処理をコントローラーが ProductUpdateInteractor に依頼するときに使われます。

packages/UseCase/Product/Update/ProductUpdateInputData.php
<?php

namespace packages\UseCase\Product\Update;

class ProductUpdateInputData
{
    private $id;
    private $name;
    private $type;
    private $made_in;
    private $stock;

    public function __construct(
        int $id,
        string $name,
        string $type,
        string $made_in,
        int $stock
    )
    {
        $this->id = $id;
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMadeIn(): string
    {
        return $this->made_in;
    }

    public function getStock(): int
    {
        return $this->stock;
    }
}

次に ProductStoreInputData です。
これは Product の新規登録処理をコントローラが ProductStoreInteractor に依頼するときに使われます。

packages/UseCase/Product/Store/ProductStoreInputData.php
<?php

namespace packages\UseCase\Product\Store;

class ProductStoreInputData
{
    private $name;
    private $type;
    private $made_in;
    private $stock;

    public function __construct(
        string $name,
        string $type,
        string $made_in,
        int $stock
    )
    {
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMadeIn(): string
    {
        return $this->made_in;
    }

    public function getStock(): int
    {
        return $this->stock;
    }
}

次に、ProductRemoveInputData です。
これは Product の削除処理をコントローラーが ProductRemoveInteractor に依頼するときに使われます。

packages/UseCase/Product/Remove/ProductRemoveInputData.php
<?php

namespace packages\UseCase\Product\Remove;

class ProductRemoveInputData
{
    private $id;

    public function __construct(int $id)
    {
        $this->id = $id;
    }

    public function getId(): int
    {
        return $this->id;
    }
}

OutputBoundary

OutputBoundary.png

OutputBoundary は Presenter を抽象化したものです。
UseCaseInteractor が処理した結果を OutputData とともに Presenter に依頼する際には、UseCaseInteractor はインタフェースである OutputBoundary を呼び出すかたちにするということですね。

今回は、UseCaseInteractor が結果をコントローラーに戻すようにし、コントローラーが Presenter を兼ねているため OutputBoundary もつくっておりません。

OutputData

OutputData.png

次は OutputData です。
InputData と同じくこちらもデータをあらわします。

UseCaseInteractor が DataAccessInterface をつかって処理した結果を View に渡すため、この中に必要なデータを詰め込みます。
詰め込んだら処理を Presenter に依頼するわけですが、今回は Presenter の役割は コントローラが兼ねているので、コントローラが受け取ることになります。

OutputData は以下が該当します。

  • packages/UseCase/Product/Index/ProductIndexOutputData.php
  • packages/UseCase/Product/Get/ProductGetOutputData.php
packages/UseCase/Product/Index/ProductIndexOutputData.php
<?php

namespace packages\UseCase\Product\Index;

use packages\UseCase\Product\Commons\ProductModel;

class ProductIndexOutputData
{
    public $products;

    public function __construct(array $products)
    {
        $this->products = $products;
    }
}
packages/UseCase/Product/Get/ProductGetOutputData.php
<?php

namespace packages\UseCase\Product\Get;

class ProductGetOutputData
{
    private $id;
    private $name;
    private $type;
    private $made_in;
    private $stock;

    public function __construct(
        int $id,
        string $name,
        string $type,
        string $made_in,
        int $stock
    )
    {
        $this->id = $id;
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMadeIn(): string
    {
        return $this->made_in;
    }

    public function getStock(): int
    {
        return $this->stock;
    }
}

Controller

Controller.png

次にコントローラです。
今回、編集前と編集後それぞれのコードを残したかったため、コントローラをわけており、ルートのコメントの付け外しで切り替えられるようにしています。

編集前

再掲ですが編集前のコントローラは以下です。
ひとつのコントローラの中にすべてのメソッドを定義しています。

  • app/Http/Controllers/ProductController.php
app/Http/Controllers/ProductController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;
use DB;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::all()->toArray();

        return view('product', compact('products'));
    }

    public function edit(Request $request)
    {
        $product = Product::find($request->id);

        return view('edit', compact('product'));
    }

    public function update(Request $request, int $id)
    {
        $product = Product::find($id);

        $product->name = $request->name;
        $product->type = $request->type;
        $product->made_in = $request->made_in;
        $product->stock = $request->stock;
        $product->save();

        return redirect(url('/product'));
    }

    public function create()
    {
        return view('create');
    }

    public function store(Request $request)
    {
        Product::create([
            'name' => $request->name,
            'type' => $request->type,
            'made_in' => $request->made_in,
            'stock' => $request->stock,
        ]);

        return redirect(url('/product'));
    }

    public function delete(Request $request)
    {
        $product = Product::find($request->id);

        return view('delete', compact('product'));
    }

    public function remove(Request $request)
    {
        $product = Product::find($request->id);

        $product->delete();

        return redirect(url('/product'));
    }
}

編集後

編集後は1コントローラ1メソッドにわけてみました。

  • app/Http/Controllers/CleanArchitecture/ProductIndexController.php
  • app/Http/Controllers/CleanArchitecture/ProductEditController.php
  • app/Http/Controllers/CleanArchitecture/ProductUpdateController.php
  • app/Http/Controllers/CleanArchitecture/ProductCreateController.php
  • app/Http/Controllers/CleanArchitecture/ProductStoreController.php
  • app/Http/Controllers/CleanArchitecture/ProductDeleteController.php
  • app/Http/Controllers/CleanArchitecture/ProductRemoveController.php

まずは ProductIndexController です。
Product の全量を取得し、それらを ViewModel にして View に渡します。

app/Http/Controllers/CleanArchitecture/ProductIndexController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

use Illuminate\Http\Request;
use App\Http\Models\Product\Commons\ProductViewModel;
use App\Http\Models\Product\Index\ProductIndexViewModel;
use packages\UseCase\Product\Index\ProductIndexUseCaseInterface;
use packages\UseCase\Product\Index\ProductIndexOutputData;

class ProductIndexController extends \App\Http\Controllers\Controller
{
    public function __invoke(ProductIndexUseCaseInterface $interactor)
    {
        $response = $interactor->handle();

        $products = array_map(
            function ($product) {
                return new ProductViewModel(
                    $product->id,
                    $product->name,
                    $product->type,
                    $product->made_in,
                    $product->stock);
            },
            $response->products
        );
        $viewModel = new ProductIndexViewModel($products);

        return view('clean_architecture/product', compact('viewModel'));
    }
}

次は ProductEditController です。
編集画面を取得するときに呼ばれます。
指定された id の Product を取得するために ProductGetUseCaseInterface のインタフェースを実装する ProductGetInteractor を呼び出し、その結果を ViewModel として編集画面に渡します。

app/Http/Controllers/CleanArchitecture/ProductEditController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

use Illuminate\Http\Request;
use App\Http\Models\Product\Edit\ProductEditViewModel;
use packages\UseCase\Product\Get\ProductGetUseCaseInterface;
use packages\UseCase\Product\Get\ProductGetOutputData;

class ProductEditController extends \App\Http\Controllers\Controller
{
    public function __invoke(ProductGetUseCaseInterface $interactor, Request $request)
    {
        $outputData = $interactor->handle($request->id);

        $viewModel = new ProductEditViewModel(
            $outputData->getId(),
            $outputData->getName(),
            $outputData->getType(),
            $outputData->getMadeIn(),
            $outputData->getStock()
        );

        return view('clean_architecture/edit', compact('viewModel'));
    }
}

次は ProductUpdateController です。
Product の編集を実行するときに呼ばれます。
Request から編集内容をうけとり、それを InputData として ProcutUpdateInteractor に処理を依頼します。

app/Http/Controllers/CleanArchitecture/ProductUpdateController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

use Illuminate\Http\Request;
use packages\UseCase\Product\Update\ProductUpdateInputData;
use packages\UseCase\Product\Update\ProductUpdateUseCaseInterface;

class ProductUpdateController extends \App\Http\Controllers\Controller
{
    public function __invoke(ProductUpdateUseCaseInterface $interactor, Request $request)
    {
        $inputData = new ProductUpdateInputData(
            $request->id,
            $request->name,
            $request->type,
            $request->made_in,
            $request->stock
        );

        $interactor->handle($inputData);

        return redirect('/product');
    }
}

次は ProductCreateController です。
これは単に Product の新規作成画面を表示するだけであるため、View を呼び出すだけです。

app/Http/Controllers/CleanArchitecture/ProductCreateController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

class ProductCreateController extends \App\Http\Controllers\Controller
{
    public function __invoke()
    {
        return view('clean_architecture/create');
    }
}

次は ProductStoreController です。
これは Product の新規登録を実行するときに呼ばれます。
Request から登録内容をうけとりまして、それを UseCaseInteractor へ連携するため、InputData につめてから UseCaseInteractor に処理を依頼します。

app/Http/Controllers/CleanArchitecture/ProductStoreController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

use Illuminate\Http\Request;
use packages\UseCase\Product\Store\ProductStoreInputData;
use packages\UseCase\Product\Store\ProductStoreUseCaseInterface;

class ProductStoreController extends \App\Http\Controllers\Controller
{
    public function __invoke(ProductStoreUseCaseInterface $interactor, Request $request)
    {
        $inputData = new ProductStoreInputData(
            $request->name,
            $request->type,
            $request->made_in,
            $request->stock
        );

        $interactor->handle($inputData);

        return redirect('/product');
    }
}

次は ProductDeleteController です。
これは削除画面を取得するときに呼ばれます。
指定された id の Product を取得するために ProductGetUseCaseInterface を実装した ProductGetInteractor を呼びます。
取得した結果を View に渡すため、ViewModel を生成してからそれを渡します。

app/Http/Controllers/CleanArchitecture/ProductDeleteController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

use Illuminate\Http\Request;
use App\Http\Models\Product\Delete\ProductDeleteViewModel;
use packages\UseCase\Product\Get\ProductGetUseCaseInterface;
use packages\UseCase\Product\Get\ProductGetOutputData;

class ProductDeleteController extends \App\Http\Controllers\Controller
{
    public function __invoke(ProductGetUseCaseInterface $interactor, Request $request)
    {
        $outputData = $interactor->handle($request->id);

        $viewModel = new ProductDeleteViewModel(
            $outputData->getId(),
            $outputData->getName(),
            $outputData->getType(),
            $outputData->getMadeIn(),
            $outputData->getStock()
        );

        return view('clean_architecture/delete', compact('viewModel'));
    }
}

最後は ProductRemoveController です。
これは Product の削除を実行するときに呼ばれます。
指定された id を ProductRemoveInteractor に連携し、削除を実施します。

app/Http/Controllers/CleanArchitecture/ProductRemoveController.php
<?php

namespace App\Http\Controllers\CleanArchitecture;

use Illuminate\Http\Request;
use packages\UseCase\Product\Remove\ProductRemoveInputData;
use packages\UseCase\Product\Remove\ProductRemoveUseCaseInterface;

class ProductRemoveController extends \App\Http\Controllers\Controller
{
    public function __invoke(ProductRemoveUseCaseInterface $interactor, Request $request)
    {
        $inputData = new ProductRemoveInputData($request->id);

        $interactor->handle($inputData);

        return redirect('/product');
    }
}

ViewModel

ViewModel.png

次は ViewModel です。

これも箱の右上に <DS> とあるので、InputData、OutputData と同じくデータをあらわします。
ViewModel は Presenter(今回の例ではコントローラ)と View の間にあり、この間のデータをやりとりを担います。

今回作成した ViewModel は以下で、それぞれ blade からデータを参照するために利用しています。

  • app/Http/Models/Product/Index/ProductIndexViewModel.php
  • app/Http/Models/Product/Commons/ProductViewModel.php
  • app/Http/Models/Product/Edit/ProductEditViewModel.php
  • app/Http/Models/Product/Delete/ProductDeleteViewModel.php

まずは ProductIndexViewModel です。
これは Product の一覧を表示する画面に渡す Product の配列なのですが、次の ProductViewModel を要素として持ちます。

ProductIndexController がこの ViewModel を生成し、それを View に渡しています。

app/Http/Models/Product/Index/ProductIndexViewModel.php
<?php

namespace App\Http\Models\Product\Index;

use App\Http\Models\Product\Commons\ProductViewModel;

class ProductIndexViewModel
{
    public $products;

    public function __construct(array $products)
    {
        $this->products = $products;
    }
}
app/Http/Models/Product/Commons/ProductViewModel.php
<?php

namespace App\Http\Models\Product\Commons;

class ProductViewModel
{
    public $id;
    public $name;
    public $type;
    public $made_in;
    public $stock;

    public function __construct(
        int $id,
        string $name,
        string $type,
        string $made_in,
        int $stock)
    {
        $this->id = $id;
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }
}

次は ProductEditViewModel です。
これは ProductEditController が、編集画面を表示するために ProductGetInteractor から受けとった OutputData をもとにこの ViewModel を生成し、それを View に渡します。

app/Http/Models/Product/Edit/ProductEditViewModel.php
<?php

namespace App\Http\Models\Product\Edit;

class ProductEditViewModel
{
    private $id;
    private $name;
    private $type;
    private $made_in;
    private $stock;

    public function __construct(
        int  $id,
        string $name,
        string $type,
        string $made_in,
        int $stock)
    {
        $this->id = $id;
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMadeIn(): string
    {
        return $this->made_in;
    }

    public function getStock(): int
    {
        return $this->stock;
    }
}

次に ProductDeleteViewModel です。
これは ProductDeleteController が、削除画面を表示するために ProductGetInteractor から受け取った OutputData をもとにこの ViewModel を生成し、それを View に渡します。

app/Http/Models/Product/Delete/ProductDeleteViewModel.php
<?php

namespace App\Http\Models\Product\Delete;

class ProductDeleteViewModel
{
    private $id;
    private $name;
    private $type;
    private $made_in;
    private $stock;

    public function __construct(
        int  $id,
        string $name,
        string $type,
        string $made_in,
        int $stock)
    {
        $this->id = $id;
        $this->name = $name;
        $this->type = $type;
        $this->made_in = $made_in;
        $this->stock = $stock;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMadeIn(): string
    {
        return $this->made_in;
    }

    public function getStock(): int
    {
        return $this->stock;
    }
}

View

次に View です。
ここも修正前とは blade ファイルをわけており、以下が修正後の View です。

  • resources/views/clean_architecture/product.blade.php
  • resources/views/clean_architecture/edit.blade.php
  • resources/views/clean_architecture/create.blade.php
  • resources/views/clean_architecture/delete.blade.php

まずは product.blade.php から抜粋です。
viewModel を参照するように変更しています。

resources/views/clean_architecture/product.blade.php
<h2>Index</h2>
<table border="1">
    <tr>
        <th>id</th>
        <th>name</th>
        <th>type</th>
        <th>made_in</th>
        <th>stock</th>
        <th>edit</th>
        <th>delete</th>
    </tr>
    @foreach ($viewModel->products as $product)
        <tr>
            <td>{{ $product->id }} </td>
            <td>{{ $product->name }}</td>
            <td>{{ $product->type }}</td>
            <td>{{ $product->made_in }}</td>
            <td>{{ $product->stock }}</td>
            <td>
                <a href="/edit/{{ $product->id }}">
                    <button type="button" class="btn btn-primary">Edit</button>
                </a>
            </td>
            <td>
                <a href="/delete/{{ $product->id }}">
                    <button type="button" class="btn btn-primary">Delete</button>
                </a>
            </td>
        </tr>
    @endforeach
</table>
<div class="btn_create">
    <a href="/create">
        <button type="button" class="btn btn-primary">Create</button>
    </a>
</div>

次に edit.blade.php から抜粋です。
こちらも ViewModel を参照するように変更しています。

resources/views/clean_architecture/edit.blade.php
<h2>Edit</h2>
<form action="{{ route('update', ['id' => $viewModel->getId()]) }}" method="POST"/>
    @csrf
    <div class="form-group row">
        <label class="col-sm-2">id</label>
        <div class="col-sm-10">{{ $viewModel->getId() }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">name</label>
        <input type="text" name="name" class="form-control col-sm-10" value="{{ $viewModel->getName() }}">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">type</label>
        <input type="text" name="type" class="form-control col-sm-10" value="{{ $viewModel->getType() }}">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">made_in</label>
        <input type="text" name="made_in" class="form-control col-sm-10" value="{{ $viewModel->getMadeIn() }}">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">stock</label>
        <input type="number" name="stock" class="form-control col-sm-10" value="{{ $viewModel->getStock() }}">
    </div>
    <button type="submit" class="btn btn-primary">Enter</button>
</form>

次は create.blade.php です。
これは変更ありません。

resources/views/clean_architecture/create.blade.php
<h2>Create</h2>
<form action="{{ route('store') }}" method="POST"/>
    @csrf
    <div class="form-group row">
        <label class="col-sm-2">name</label>
        <input type="text" name="name" class="form-control col-sm-10">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">type</label>
        <input type="text" name="type" class="form-control col-sm-10">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">made_in</label>
        <input type="text" name="made_in" class="form-control col-sm-10">
    </div>
    <div class="form-group row">
        <label class="col-sm-2">stock</label>
        <input type="number" name="stock" class="form-control col-sm-10">
    </div>
    <button type="submit" class="btn btn-primary">Enter</button>
</form>

次は delete.blade.php です。
これも ViewModel を参照するように変更しています。

resources/views/clean_architecture/delete.blade.php
<h2>Delete</h2>
<form action="{{ route('remove', ['id' => $viewModel->getId()]) }}" method="POST"/>
    @csrf
    <div class="form-group row">
        <label class="col-sm-2">id</label>
        <div class="col-sm-10">{{ $viewModel->getId() }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">name</label>
        <div class="col-sm-10">{{ $viewModel->getName() }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">type</label>
        <div class="col-sm-10">{{ $viewModel->getType() }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">made_in</label>
        <div class="col-sm-10">{{ $viewModel->getMadeIn() }}</div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2">stock</label>
        <div class="col-sm-10">{{ $viewModel->getStock() }}</div>
    </div>
    <button type="submit" class="btn btn-primary">Enter</button>
</form>

#そのほか
上記でひととおりの説明はおわりです。

その他の変更点として、この変更でいくつかインタフェースをつくっており、それらをメソッドインジェクションしていますが、そのための設定を AppServiceProvider に登録しています。

app/Providers/AppServiceProvider.php
public function register()
{
    $this->app->bind(
        \packages\UseCase\Product\Index\ProductIndexUseCaseInterface::class,
        \packages\Domain\Application\Product\ProductIndexInteractor::class
    );

    $this->app->bind(
        \packages\UseCase\Product\Get\ProductGetUseCaseInterface::class,
        \packages\Domain\Application\Product\ProductGetInteractor::class
    );

    $this->app->bind(
        \packages\UseCase\Product\Update\ProductUpdateUseCaseInterface::class,
        \packages\Domain\Application\Product\ProductUpdateInteractor::class
    );

    $this->app->bind(
        \packages\UseCase\Product\Store\ProductStoreUseCaseInterface::class,
        \packages\Domain\Application\Product\ProductStoreInteractor::class
    );

    $this->app->bind(
        \packages\UseCase\Product\Remove\ProductRemoveUseCaseInterface::class,
        \packages\Domain\Application\Product\ProductRemoveInteractor::class
    );

    $this->app->bind(
        \packages\Domain\Domain\Product\ProductRepositoryInterface::class,
        \packages\Infrastructure\Product\ProductRepository::class
    );
}

もうひとつ変更点として、ルーティングも変更しています。
web.php のコメントのつけはずしで変更する前と後を切り替えられるようにしました。

routes/web.php
/*
Route::get('/product', 'App\Http\Controllers\ProductController@index');
Route::get('/edit/{id}', 'App\Http\Controllers\ProductController@edit');
Route::post('/update/{id}', 'App\Http\Controllers\ProductController@update')->name('update');
Route::get('/create', 'App\Http\Controllers\ProductController@create');
Route::post('/store', 'App\Http\Controllers\ProductController@store')->name('store');
Route::get('/delete/{id}', 'App\Http\Controllers\ProductController@delete');
Route::post('/remove/{id}', 'App\Http\Controllers\ProductController@remove')->name('remove');
 */

Route::get('/product', 'App\Http\Controllers\CleanArchitecture\ProductIndexController');
Route::get('/edit/{id}', 'App\Http\Controllers\CleanArchitecture\ProductEditController');
Route::post('/update/{id}', 'App\Http\Controllers\CleanArchitecture\ProductUpdateController')->name('update');
Route::get('/create', 'App\Http\Controllers\CleanArchitecture\ProductCreateController');
Route::post('/store', 'App\Http\Controllers\CleanArchitecture\ProductStoreController')->name('store');
Route::get('/delete/{id}', 'App\Http\Controllers\CleanArchitecture\ProductDeleteController');
Route::post('/remove/{id}', 'App\Http\Controllers\CleanArchitecture\ProductRemoveController')->name('remove');

感じたこと

記事の本論は以上です、ここまで読んでいただきまして本当にありがとうございました。
最後に感想などをかこうとおもいます。

クリーンアーキテクチャという概念を実際に現場で採用するかどうかは別として、ある程度の規模のアプリを作ろうとすると方向性としては似たようなものを目指すことにはなろうかとおもいます。

そうすると、クリーンアーキテクチャに登場するエンティティとかユースケースとかデータアクセスなど、言葉はなんでもいいのですがそれに類する概念をなんとなくイメージしておくと、設計をするときに役に立ちそうだな、とおもいました。

抽象的なイメージを共有すると、レイヤーごとに担当者をわけたり、スケジュールをひいたりといったことがやりやすく、開発者同士の意思疎通が円滑になることが、コードの修正しやすさとかより大きなメリットかもしれないなとおもいました。

#参考記事

※1

1
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
1
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?