🔥

フレームワークへの依存度を下げるFLUDパターン (2)

2021/12/21に公開

CodeIgniter Advent Calendar 2021

目次

フレームワークへの依存度を下げるFLUDパターン (1)
の続きです。

コードがないと理解しづらいですから、CodeIgniter4のチュートリアルのコードをFLUDパターンに変更してみましょう。

まずは、http://localhost/news/ のNewsの表示を変更します。

パッケージの名前空間の定義

オートローダーの設定にパッケージ用の名前空間を追加します。
Composerのオートローダーを使うことも可能です。

--- a/app/Config/Autoload.php
+++ b/app/Config/Autoload.php
@@ -43,6 +43,8 @@ class Autoload extends AutoloadConfig
     public $psr4 = [
         APP_NAMESPACE => APPPATH, // For custom app namespace
         'Config'      => APPPATH . 'Config',
+        'Acme\News'        => ROOTPATH . 'packages/news/src',
+        'Acme\Shared'      => ROOTPATH . 'packages/shared/src',
     ];
 
     /**

ここでは、ベンダー名前空間は Acme としました。
NewsパッケージとSharedパッケージを追加しました。

ドメイン層

News

まず、Newsを表す News クラスを追加します。

packages/news/src/Domain/News/News.php

<?php

declare(strict_types=1);

namespace Acme\News\Domain\News;

use LogicException;

class News
{
    private $id;
    private $title;
    private $slug;
    private $body;

    public function __construct(
        ?int $id,
        string $title,
        string $slug,
        string $body
    ) {
        $this->id = $id;
        $this->title = $title;
        $this->slug = $slug;
        $this->body = $body;
    }

    public function __isset(string $name)
    {
        if (! property_exists($this, $name)) {
            throw new LogicException('No such property: ' . $name);
        }

        return isset($this->$name);
    }

    public function __get(string $name)
    {
        if (! property_exists($this, $name)) {
            throw new LogicException('No such property: ' . $name);
        }

        return $this->$name;
    }
}

Newsの id はデータベースでのオートインクリメントなので、新規にオブジェクトを作成する際にはまだわかりません。仕方がないので、nullを許容しています。

NewsRepositoryInterface

続いて、News を永続化するためのリポジトリインターフェースを定義します。

packages/news/src/Domain/News/NewsRepositoryInterface.php

<?php

declare(strict_types=1);

namespace Acme\News\Domain\News;

interface NewsRepositoryInterface
{
    /**
     * @return News[]
     */
    public function getNewsList(): array;
}

getNewsList() メソッドは News の配列を返します。

ユースケース層

GetNewsListUseCase

News のリストを取得するユースケースを追加します。

ユースケースクラスの名前は、動作を表すため動詞で始め、UseCaseを最後に付けます。
1クラス1パブリックメソッドを推奨します。 ここでは run() メソッドとしています。

packages/news/src/UseCase/News/GetNewsListUseCase.php

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

use Acme\News\Domain\News\News;
use Acme\News\Domain\News\NewsRepositoryInterface;

class GetNewsListUseCase
{
    /**
     * @var NewsRepositoryInterface
     */
    private $newsRepository;

    public function __construct(NewsRepositoryInterface $newsRepositry)
    {
        $this->newsRepository = $newsRepositry;
    }

    /**
     * @return NewsDto[]
     */
    public function run(): array
    {
        $newsList = $this->newsRepository->getNewsList();

        $newsDtoList = array_map(function ($news) {
            return new News(
                $news->id,
                $news->title,
                $news->slug,
                $news->body
            );
        }, $newsList);

        return $newsDtoList;
    }
}

リポジトリから News のリストを取得して、NewsDto に詰め替えて返しています。

これは、以下の理由からそうしています。

  • ドメイン層の知識がフレームワーク/ライブラリ層に漏れないように
  • ドメイン層のクラスがフレームワーク/ライブラリ層の影響を受けないように
  • ドメイン層のクラスの変更が直接フレームワーク/ライブラリ層に影響を及ぼさないように

NewsDto

ユースケースからの返り値はドメイン層の News ではなく、ユースケース層の NewsDto を使っています。

packages/News/UseCase/News/NewsDto.php

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

use LogicException;

class NewsDto
{
    private $id;
    private $title;
    private $slug;
    private $body;

    public function __construct(
        int $id,
        string $title,
        string $slug,
        string $body
    ) {
        $this->id = $id;
        $this->title = $title;
        $this->slug = $slug;
        $this->body = $body;
    }

    public function __isset(string $name)
    {
        if (! property_exists($this, $name)) {
            throw new LogicException('No such property: ' . $name);
        }

        return isset($this->$name);
    }

    public function __get(string $name)
    {
        if (! property_exists($this, $name)) {
            throw new LogicException('No such property: ' . $name);
        }

        return $this->$name;
    }
}

フレームワーク/ライブラリ層

モデル

フレームワークのモデルです。

NewsRepository

CodeIgniter\Model を使い、リポジトリを実装します。

app/Models/NewsRepository.php

<?php

namespace App\Models;

use CodeIgniter\Model;
use Acme\News\Domain\News\News;
use Acme\News\Domain\News\NewsRepositoryInterface;
use RuntimeException;

class NewsRepository extends Model implements NewsRepositoryInterface
{
    protected $table = 'news';

    protected $allowedFields = ['title', 'slug', 'body'];

    /**
     * @return News[]
     */
    public function getNewsList(): array
    {
        $stdClassArray = $this->asObject()->findAll();

        $newsList = array_map(function ($news) {
            return new News(
                $news->id,
                $news->title,
                $news->slug,
                $news->body
            );
        }, $stdClassArray);

        return $newsList;
    }
}

CodeIgniter\ModelfindAll() メソッドでデータベースから全てのレコードを取得し、News インスタンスに変換して返します。

コントローラ

フレームワークのコントローラです。

Newsコントローラ

--- a/app/Controllers/News.php
+++ b/app/Controllers/News.php
@@ -2,16 +2,22 @@
 
 namespace App\Controllers;
 
-use App\Models\NewsModel;
+use App\Models\NewsRepository;
+use Acme\News\UseCase\News\NewsDto;
+use Acme\News\UseCase\News\GetNewsListUseCase;
 
 class News extends BaseController
 {
     public function index()
     {
-        $model = model(NewsModel::class);
+        $repository = model(NewsRepository::class);
+        $useCase = new GetNewsListUseCase($repository);
+
+        /** @var NewsDto[] $newsList */
+        $newsList = $useCase->run();
 
         $data = [
-            'news'  => $model->getNews(),
+            'news'  => $newsList,
             'title' => 'News archive',
         ];
 

GetNewsListUseCase を生成し、実行しています。

index() メソッド全体は以下のようになります。

    public function index()
    {
        $repository = model(NewsRepository::class);
        $useCase = new GetNewsListUseCase($repository);

        /** @var NewsDto[] $newsList */
        $newsList = $useCase->run();

        $data = [
            'news'  => $newsList,
            'title' => 'News archive',
        ];

        echo view('templates/header', $data);
        echo view('news/overview', $data);
        echo view('templates/footer', $data);
    }

ビュー

フレームワークのビューです。

配列だった $news_itemNewsDto インスタンスに変えたため、プロパティを表示するようにコードを変更しています。

--- a/app/Views/news/overview.php
+++ b/app/Views/news/overview.php
@@ -4,12 +4,12 @@
 
     <?php foreach ($news as $news_item): ?>
 
-        <h3><?= esc($news_item['title']) ?></h3>
+        <h3><?= esc($news_item->title) ?></h3>
 
         <div class="main">
-            <?= esc($news_item['body']) ?>
+            <?= esc($news_item->body) ?>
         </div>
-        <p><a href="/news/<?= esc($news_item['slug'], 'url') ?>">View article</a></p>
+        <p><a href="/news/<?= esc($news_item->slug, 'url') ?>">View article</a></p>
 
     <?php endforeach ?>
 

これで、http://localhost/news/ にアクセスするとNewsが表示されるようになりました。

ディレクトリ構成

現状のディレクトリ構成は以下になります。

app/
├── Config
│   ├── Autoload.php
├── Controllers
│   ├── News.php
├── Models
│   └── NewsRepository.php
└── Views
     └── news
          └── overview.php

packages/
└── news ... Newsパッケージ
     └── src
         ├── Domain ... ドメイン層
         │   └── News
         │       ├── News.php
         │       └── NewsRepositoryInterface.php
         └── UseCase ... ユースケース層
             └── News
                 ├── GetNewsListUseCase.php
                 └── NewsDto.php

app/ 以下のファイルはモデルのファイル名を変更しただけで配置はそのままにしています。

packages/ 以下にドメイン層とユースケース層のコードが追加されています。

フレームワークへの依存度を下げるFLUDパターン (3)
へ続く。

参考

Discussion