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

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

CodeIgniter4のチュートリアルのコードをFLUDパターンに変更しています。

最後は、記事の作成です。

ドメイン層

NewsRepositoryInterface

NewsRepositoryInterface にNewsを追加するメソッドを追加します。

--- a/packages/news/src/Domain/News/NewsRepositoryInterface.php
+++ b/packages/news/src/Domain/News/NewsRepositoryInterface.php
@@ -12,4 +12,6 @@ interface NewsRepositoryInterface
     public function getNewsList(): array;

     public function getNewsBySlug(string $slug): ?News;
+
+    public function addNews(News $news): void;
 }

addNews() メソッドは渡された News をリポジトリに追加(保存)します。

ユースケース層

CreateNewsUseCase

News を作成するユースケースを追加します。

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

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

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

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

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

    public function run(string $title, string $slug, string $body): void
    {
        $news = new News(
            null,
            $title,
            $slug,
            $body,
        );

        $this->newsRepository->addNews($news);
    }
}

渡されたデータを元に News インスタンスを作成し、リポジトリに保存します。

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

モデル

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

NewsRepository

NewsRepositoryaddNews() メソッドを追加します。

--- a/app/Models/NewsRepository.php
+++ b/app/Models/NewsRepository.php
@@ -49,4 +49,17 @@ class NewsRepository extends Model implements NewsRepositoryInterface

         return $news;
     }
+
+    public function addNews(News $news): void
+    {
+        $id = $this->insert([
+            'title' => $news->title,
+            'slug' => $news->slug,
+            'body' => $news->body,
+        ]);
+
+        if ($id === false) {
+            throw new RuntimeException('Failed to insert news.');
+        }
+    }
 }

CodeIgniter\Modelinsert() メソッドでテーブルに挿入しています。

コントローラ

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

Newsコントローラ

create() メソッドを CreateNewsUseCase を使うように変更します。

--- a/app/Controllers/News.php
+++ b/app/Controllers/News.php
@@ -6,6 +6,7 @@ use App\Models\NewsRepository;
 use Acme\News\UseCase\News\NewsDto;
 use Acme\News\UseCase\News\GetNewsListUseCase;
 use Acme\News\UseCase\News\GetNewsItemUseCase;
+use Acme\News\UseCase\News\CreateNewsUseCase;

 class News extends BaseController
 {
@@ -47,17 +48,18 @@ class News extends BaseController

     public function create()
     {
-        $model = model(NewsModel::class);
+        $repository = model(NewsRepository::class);
+        $useCase = new CreateNewsUseCase($repository);

         if ($this->request->getMethod() === 'post' && $this->validate([
             'title' => 'required|min_length[3]|max_length[255]',
             'body'  => 'required',
         ])) {
-            $model->save([
-                'title' => $this->request->getPost('title'),
-                'slug'  => url_title($this->request->getPost('title'), '-', true),
-                'body'  => $this->request->getPost('body'),
-            ]);
+            $title = $this->request->getPost('title');
+            $slug  = url_title($this->request->getPost('title'), '-', true);
+            $body  = $this->request->getPost('body');
+
+            $useCase->run($title, $slug, $body);

             echo view('news/success');
         } else {

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

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

    public function create()
    {
        $repository = model(NewsRepository::class);
        $useCase = new CreateNewsUseCase($repository);

        if ($this->request->getMethod() === 'post' && $this->validate([
            'title' => 'required|min_length[3]|max_length[255]',
            'body'  => 'required',
        ])) {
            $title = $this->request->getPost('title');
            $slug  = url_title($this->request->getPost('title'), '-', true);
            $body  = $this->request->getPost('body');

            $useCase->run($title, $slug, $body);

            echo view('news/success');
        } else {
            echo view('templates/header', ['title' => 'Create a news item']);
            echo view('news/create');
            echo view('templates/footer');
        }
    }

これで、記事の作成ができるようになりました。

ディレクトリ構成

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

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

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

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

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

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

参考

Tags: codeigniter, codeigniter4, ddd, mvc, php

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

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

CodeIgniter4のチュートリアルのコードをFLUDパターンに変更しています。

次は、個別の記事の表示を変更します。

ドメイン層

NewsRepositoryInterface

NewsRepositoryInterface に個別のNewsを取得するメソッドを追加します。

--- a/packages/news/src/Domain/News/NewsRepositoryInterface.php
+++ b/packages/news/src/Domain/News/NewsRepositoryInterface.php
@@ -10,4 +10,6 @@ interface NewsRepositoryInterface
      * @return News[]
      */
     public function getNewsList(): array;
+
+    public function getNewsBySlug(string $slug): ?News;
 }

getNewsBySlug() メソッドは指定されたslugに対して News または null を返します。

ユースケース層

GetNewsItemUseCase

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

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

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

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

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

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

    public function run(string $slug): ?NewsDto
    {
        /** @var News|null $news */
        $news = $this->newsRepository->getNewsBySlug($slug);

        if ($news !== null) {
            return new NewsDto(
                $news->id,
                $news->title,
                $news->slug,
                $news->body
            );
        }

        return null;
    }
}

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

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

モデル

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

NewsRepository

NewsRepositorygetNewsBySlug() メソッドを追加します。

--- a/app/Models/NewsRepository.php
+++ b/app/Models/NewsRepository.php
@@ -31,4 +31,22 @@ class NewsRepository extends Model implements NewsRepositoryInterface

         return $newsList;
     }
+
+    public function getNewsBySlug(string $slug): ?News
+    {
+        $stdClass = $this->where(['slug' => $slug])->asObject()->first();
+
+        if ($stdClass === null) {
+            return null;
+        }
+
+        $news = new News(
+            $stdClass->id,
+            $stdClass->title,
+            $stdClass->slug,
+            $stdClass->body
+        );
+
+        return $news;
+    }
 }

CodeIgniter\Modelfirst() メソッドでデータベースから条件にマッチする最初のレコードを取得し、News インスタンスに変換して返します。

コントローラ

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

Newsコントローラ

view() メソッドを GetNewsItemUseCase を使うように変更します。

--- a/app/Controllers/News.php
+++ b/app/Controllers/News.php
@@ -5,6 +5,7 @@ namespace App\Controllers;
 use App\Models\NewsRepository;
 use Acme\News\UseCase\News\NewsDto;
 use Acme\News\UseCase\News\GetNewsListUseCase;
+use Acme\News\UseCase\News\GetNewsItemUseCase;

 class News extends BaseController
 {
@@ -28,15 +29,16 @@ class News extends BaseController

     public function view($slug = null)
     {
-        $model = model(NewsModel::class);
+        $repository = model(NewsRepository::class);
+        $useCase = new GetNewsItemUseCase($repository);

-        $data['news'] = $model->getNews($slug);
+        $data['news'] = $useCase->run($slug);

         if (empty($data['news'])) {
             throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find the news item: ' . $slug);
         }

-        $data['title'] = $data['news']['title'];
+        $data['title'] = $data['news']->title;

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

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

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

    public function view($slug = null)
    {
        $repository = model(NewsRepository::class);
        $useCase = new GetNewsItemUseCase($repository);

        $data['news'] = $useCase->run($slug);

        if (empty($data['news'])) {
            throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find the news item: ' . $slug);
        }

        $data['title'] = $data['news']->title;

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

ビュー

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

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

--- a/app/Views/news/view.php
+++ b/app/Views/news/view.php
@@ -1,2 +1,2 @@
-<h2><?= esc($news['title']) ?></h2>
-<p><?= esc($news['body']) ?></p>
+<h2><?= esc($news->title) ?></h2>
+<p><?= esc($news->body) ?></p>

これで、個別の記事も表示されるようになりました。

ディレクトリ構成

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

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

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

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

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

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

参考

Tags: codeigniter, codeigniter4, ddd, mvc, php

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

フレームワークへの依存度を下げる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) へ続く。

参考

Tags: codeigniter, codeigniter4, ddd, mvc, php