フレームワークへの依存度を下げる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\Model
の findAll()
メソッドでデータベースから全てのレコードを取得し、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_item
を NewsDto
インスタンスに変えたため、プロパティを表示するようにコードを変更しています。
--- 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) へ続く。
参考
Date: 2021/12/21