CodeIgniter 4 最速マスター

(2023-01-20 追記) この記事は古くなっています。 「 CodeIgniter 4.3 最速マスター」を参照してください。

(最終更新:2022/10/24)

インストール&設定

インストール

📕インストール

Composerでインストールすると簡単です。

$ composer create-project codeigniter4/appstarter {フォルダ名}

フォルダ構成

CodeIgniter4のフォルダ構成は以下のようになっています。

ci4app/
├── app/ ... アプリケーション
│   ├── Common.php
│   ├── Config/      ... 設定
│   ├── Controllers/ ... コントローラ
│   ├── Database/    ... データベース
│   ├── Filters/     ... コントローラフィルタ
│   ├── Helpers/
│   ├── Language/
│   ├── Libraries/
│   ├── Models/      ... モデル
│   ├── ThirdParty/
│   └── Views/       ... ビュー
├── builds*       ... buildsコマンド
├── composer.json
├── composer.lock
├── env           ... 環境変数設定ファイルのサンプル
├── phpunit.xml.dist
├── public/ ... Web公開領域(ドキュメントルート)
│   ├── favicon.ico
│   ├── index.php
│   └── robots.txt
├── spark* ... sparkコマンド
├── tests/ ... テストファイル
│   ├── _support/
│   ├── database/
│   ├── session/
│   └── unit/
├── vendor/   ... Composer管理
└── writable/ ... 書き込み用フォルダ
    ├── cache/
    ├── debugbar/
    ├── logs/
    ├── session/
    └── uploads/

システムメッセージの翻訳のインストール

Composerで最新の開発版の翻訳をインストールします。

$ composer require codeigniter4/translations:dev-develop

設定

📕設定

設定ファイル

app/Config/ フォルダに設定ファイルがあります。設定ファイルはクラスです。 本番環境と共通の設定項目は、設定ファイルを変更します。

特に設定を変更しなくてもCodeIgniterは動作しますが、app/Config/App.php の以下の項目は多くの場合、変更することになるでしょう。

  • $indexPage ... URLにindex.phpを付けない場合は、この設定を空文字にする
  • $defaultLocale ... デフォルトのロケール('ja' に変更すると日本語のシステムメッセージが使用されます)
  • $supportedLocales ... サポートするロケール(優先順位が高い順に記載する)
  • $appTimezone ... タイムゾーン

開発環境固有の設定項目(データベースのパスワードなど)は、env ファイルを .env にコピーして、そこに設定します。

設定クラスのプロパティに対応する環境変数があると、設定ファイルのインスタンス化時に環境変数の値が自動的に設定されます。

複数の環境

📕複数の環境の処理

デフォルトでは、CodeIgniterは production 環境で動作します。

開発環境では development を指定します。これでデバッグモードになり、デバッグツールバーが表示されます。 .env で設定できます。

CI_ENVIRONMENT = development

testing 環境はPHPUnitでのテストのための環境です。通常の開発やステージング環境には使えません。

データベース設定

📕データベース設定

app/Config/Database.php に設定クラスがあります。

設定クラスの値を変更したい開発環境固有の設定項目を、以下のように .env に設定します。

.env

database.default.hostname = localhost
database.default.database = ci4app
database.default.username = dbuser
database.default.password = dbpassword

サーバの起動

spark コマンドでPHPビルトインサーバーが起動します。

$ php spark serve

http://localhost:8080/ にアクセスしてください。

ルーティング

📕URIルーティング

自動ルーティング

CodeIgniter v4.2.0から、セキュリティ上の理由で自動ルーティングはデフォルトではオフになります。

手動ルーティング

自動ルーティングをオフに

app/Config/Routes.php

$routes->setAutoRoute(false);

セキュリティ上の理由から、自動ルーティング(レガシー)は推奨しません。 できる限り、自動ルーティング(レガシー)は使用せず、「手動ルーティングのみ」または「自動ルーティング(改善)」を使いましょう。

HTTPメソッドでのルーティング

app/Config/Routes.php にルートを設定します。

GETメソッドの場合は、$routes->get() を使用します。

$routes->get('news', 'News::index');

上記は、http://example.com/news にアクセスすると、News コントローラの index() メソッドが呼び出されます。

IDEでコントローラにジャンプしたい場合は、CodeIgniter v4.2.0から、以下の構文が使えます。

use App\Controllers\News;

$routes->get('news', [News::class, 'index']);

POSTメソッドの場合は、$routes->post() を使用します。

$routes->post('news/create', 'News::create');

上記は、http://example.com/news/create にアクセスすると、News コントローラの create() メソッドが呼び出されます。

URLの一部をキャプチャ

(:segment)(:any) などのプレースホルダーを使います。 (:segment) は1つのURIセグメント、(:any) は任意の文字列にマッチします。

$routes->get('news/(:segment)', 'News::view/$1');

上記は、/news/foo の場合に、News コントローラの view() メソッドに foo を渡します。

グループ化

ルートをグループ化することもできます。

$routes->group('admin', function ($routes) {
    $routes->get('users', 'Admin\Users::index');
    $routes->get('blog', 'Admin\Blog::index');
});

上記は、admin/usersadmin/blog のルートを設定しています。

ルートの確認

spark routes コマンドでルートを確認できます。上に表示されたルートが優先されます。

$ php spark routes

コントローラ

📕コントローラー

コントローラの作成

sparkコマンドで作成できます。

$ php spark make:controller {コントローラクラス名}

以下のようなファイルが作成されます。

app/Controllers/Blog.php

<?php

namespace App\Controllers;

use App\Controllers\BaseController;

class Blog extends BaseController
{
    public function index()
    {
        //
    }
}

コントローラは、BaseController を継承します。 $this->request(Requestオブジェクト) と $this->response(Responseオブジェクト)が使えます。

バリデーション

📕$this->validate()

$this->validate() を使います。

if ($this->request->getMethod() === 'post' && $this->validate([
    'title' => ['label' => 'タイトル', 'rules' => ['required', 'max_length[100]']],
    'body'  => ['label' => '本文', 'rules' => 'required'],
])) {
    // 検証パス
} else {
    // 検証エラー
}
検証するデータの指定

📕$this->validateData()

検証するデータを指定したい場合は、$this->validateData() を使います(v4.2.0以降)。

if ($this->request->getMethod() === 'post' && $this->validateData($data, $rule)) {
    // 検証パス
} else {
    // 検証エラー
}
エラーメッセージの上書き

$this->validate() の第2引数にエラーメッセージを指定します。

if ($this->request->getMethod() === 'post' && $this->validate(
    [
        'title' => ['label' => 'タイトル', 'rules' => ['required', 'min_length[3]', 'max_length[255]']],
        'body'  => ['label' => '本文', 'rules' => 'required'],
    ],
    [
        'title' => [
            'required' => 'タイトルは必須です。',
        ],
    ]
)) {
独自の検証ルールの作成

📕カスタムルールの作成

app/Config/Validation.php$ruleSets に検証ルールを実装したクラス名を追加します。

public $ruleSets = [
    Rules::class,
    FormatRules::class,
    FileRules::class,
    CreditCardRules::class,
    \App\Libraries\Validation\MyRules::class,
];

検証ルールを実装したクラスは以下のようになり、バリデーションルール even が使えるようになります。

namespace App\Libraries\Validation;

class MyRules
{
    public function even($str, string &$error = null): bool
    {
        if ((int) $str % 2 !== 0) {
            $error = 'エラーメッセージ。';

            return false;
        }

        return true;
    }
}

リクエストパラメータの取得

📕入力の取得

$this->request->getPost()$this->request->getGet()$this->request->getJSON() を使います。

$this->request->getPost('title')

上記は、POSTされた title の値を取得します。

モデルの呼び出し

📕model() 関数を使います。

$model = model(UserModel::class);

ビューのレンダリング

📕view() 関数を使います。

return view('pages/view_filename', $data);

第1引数はビューファイル名、第2引数はビューに渡すデータの配列です。 ビューファイル名、app/Views/ 以下のファイルパスを .php を除いて指定します。

ログの出力

📕log_message() 関数を使います。

log_message('debug', 'デバッグ用のログメッセージ。');

ログファイルは、writable/logs/ に日付別に作成されます。

リダイレクト

📕redirect() 関数を使います。

return redirect()->to('home/index');

API

📕RESTfulリソース処理

ルーティング

app/Config/Routes.php

$routes->resource('photos');

上記を設定すると、以下のルートが設定されたことになります。

$routes->get('photos/new',             'Photos::new');
$routes->post('photos',                'Photos::create');
$routes->get('photos',                 'Photos::index');
$routes->get('photos/(:segment)',      'Photos::show/$1');
$routes->get('photos/(:segment)/edit', 'Photos::edit/$1');
$routes->put('photos/(:segment)',      'Photos::update/$1');
$routes->patch('photos/(:segment)',    'Photos::update/$1');
$routes->delete('photos/(:segment)',   'Photos::delete/$1');

不要なメソッドがあれば、除外指定します。

$routes->resource('photos', ['except' => 'new,edit']);

コントローラ

ResourceController を継承します。

<?php

namespace App\Controllers;

use App\Models\PhotoModel;
use CodeIgniter\RESTful\ResourceController;

class Photos extends ResourceController
{
    protected $modelName = PhotoModel::class;
    protected $format    = 'json';

    public function index()
    {
        return $this->respond($this->model->findAll());
    }

    // ...
}

ResponseTrait に実装されている $this->respond() などのメソッドが使えるようになります。 上記のコードでは、モデルを検索した結果がJSONで返されます。

ビュー

📕ビュー

デフォルトでは、ビューファイルは普通のPHPファイルです。

個人的には、出力時に自動でエスケープ処理してくれるTwigなどのテンプレートエンジンかView Parserの使用を推奨します。

デフォルトのView

コントローラから渡された配列が、 キーを変数名とした変数に設定されます。

変数を出力する場合は、忘れずに esc() 関数でエスケープします。

View Parser

📕ビューパーサー

テンプレートエンジンです。変数の値の自動HTMLエスケープフィルター機能も提供します。

コントローラから渡された配列の値が {blog_title} のような疑似変数に表示されます。 値は自動的にHTMLエスケープされます。

モデル

CodeIgniter\Model が標準で提供されていますが、使わなくても構いません。 データベース操作が必要な場合、 最小限のモデルクラスは以下のようになります。

app/Models/UserModel.php

<?php

namespace App\Models;

use CodeIgniter\Database\ConnectionInterface;

class UserModel
{
    protected $db;

    public function __construct(ConnectionInterface $db)
    {
        $this->db = $db;
    }
}

$db はインスタンス化する時に自分で注入してください。

モデルの作成

以下のようなファイルを作成します。

app/Models/UserModel.php

<?php

namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $table = 'users';
}

CodeIgniter\Model を継承すると、いろいろ便利な機能が使えます。

クエリビルダー

📕クエリビルダークラス

ビルダーの生成

db_connect() 関数で「データベース接続」オブジェクトを取得できます。

$db = db_connect();

データベース接続オブジェクトの table() メソッドにテーブル名を指定すると、新しいクエリビルダーを取得できます。

$builder = $db->table('users');
SELECT
$query = $builder->where('name !=', $name)
    ->where('id <', $id)
    ->orderBy('name', 'ASC')
    ->get();

foreach ($query->getResult() as $row) {
    echo $row->title;
}

CodeIgniter3との違いは、テーブル名をビルダー生成時に指定することとと、メソッド名がcamelCaseに変わった程度です。

INSERT

insert() メソッドを使います。

$data = [
    'title' => 'My title',
    'name'  => 'My Name',
    'date'  => 'My date',
];
$builder->insert($data);
UPDATE

update() メソッドを使います。

$data = [
    'title' => $title,
    'name'  => $name,
    'date'  => $date,
];
$builder->where('id', $id);
$builder->update($data);
DELETE

delete() メソッドを使います。

$builder->delete(['id' => $id]);

where() メソッドでレコードを指定することもできます。

$builder->where('id', $id);
$builder->delete();

CodeIgniter\Model

📕CodeIgniterのモデルの使用

以下のような機能があります。

  • 自動データベース接続
  • 基本的なCRUDメソッド
  • モデル内でのバリデーション
  • 自動ページネーション
  • 論理削除
データの検索

📕データの検索

find()

プライマリーキーでレコードを検索します。

$user = $userModel->find($id);
$users = $userModel->find([1, 2, 3]);
findAll()

レコードを検索します。

$users = $userModel->findAll();
$users = $userModel->where('active', 1)->findAll();
$users = $userModel->findAll($limit, $offset);
データの保存

📕データの保存

insert()

レコードを挿入します。

$data = [
    'username' => 'darth',
    'email'    => 'd.vader@example.com',
];
$userModel->insert($data);
update()

プライマリーキーを指定してレコードを更新します。

$data = [
    'username' => 'darth',
    'email'    => 'd.vader@example.com',
];
$userModel->update($id, $data);
$data = [
    'active' => 1,
];
$userModel->update([1, 2, 3], $data);

WHERE句を指定してレコードを更新します。

$userModel->whereIn('id', [1, 2, 3])->set(['active' => 1])->update();
save()

プライマリーキーがない場合は挿入します。

$data = [
    'username' => 'darth',
    'email'    => 'd.vader@example.com',
];
$userModel->save($data);

プライマリーキーがある場合は更新します。

$data = [
    'id'       => 3,
    'username' => 'darth',
    'email'    => 'd.vader@example.com',
];
$userModel->save($data);
データの削除

📕データの削除

delete()

プライマリーキーを指定してレコードを削除します。

$userModel->delete(12);
$userModel->delete([1, 2, 3]);

WHERE句を指定してレコードを削除します。

$userModel->where('id', 12)->delete();

Entityクラス

📕エンティティクラスの使用

テーブルのレコードを表すクラスです。POPOではありません。使わなくても構いません。

app/Entities/User.php

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    // ...
}

CodeIgniter\Model では、$returnType プロパティに指定すると、検索結果がこのクラスのインスタンスになります。

    protected $returnType = \App\Entities\User::class;

クエリビルダーでも getResult() メソッドで指定すれば使えます。

$query->getResult(\App\Entities\User::class);

認証

ユーザガイドの 📕認証 に推奨事項が記載されています。

執筆時点で上記を満たす推奨できるパッケージは codeigniter4/shieldmyth/auth 以外ありません。基本的に公式パッケージである codeigniter4/shield を使うことを推奨します。

セッション

📕セッションライブラリ

以下でセッションオブジェクトが取得できます。

$session = session();

セッションの読み書き

書き込み

1アイテムを書き込む場合。

$session->set('some_name', 'some_value');

まとめて書き込む場合。

$newdata = [
    'username'  => 'johndoe',
    'email'     => 'johndoe@example.com',
    'logged_in' => true,
];
$session->set($newdata);
読み込み

どちらでも取得できます。

$session->get('some_name');
$session->some_name
削除

1アイテムを削除する場合。

$session->remove('some_name');

まとめて削除する場合。

$items = ['username', 'email'];
$session->remove($items);

参考

Tags: codeigniter, codeigniter4

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

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

FLUDパターンのおさらい

FLUDパターンは、以下の3階層のパターンです。

黒い矢印は依存(使うということ)を表します。白い矢印は汎化、つまりインターフェイスにして使うということを表します。

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

  • フレームワークやライブラリを直接使うコード
  • フレームワークを使ったMVCコードは通常全てこの層になる

ユースケース層

  • ユーザーがソフトウェアで行えるアクション
  • ソフトウェアがなくなったら存在しなくなる
  • ドメイン層のオブジェクトを使いユースケースを組み立てる

ドメイン層

  • ドメイン知識
  • ソフトウェアを適用して問題解決しようとする領域(ドメイン)に存在するルールや制約
  • ソフトウェアがなくても存在する
  • ユースケースによって変わることがない

FLUDパターンの基本ルール

  1. MVCコードから、ドメイン層とユースケース層のコードをできる限り分離する
  2. 下の層のコードが上の層のコードを取り扱う場合は、抽象型(インターフェース)に依存する

ガイドライン

ユースケースクラス
  • 動作を表すため動詞で始める
  • UseCase を最後に付ける(例、CreateNewsUseCase
  • 1クラス1パブリックメソッドとする(例、run() メソッド)

ディレクトリ構成

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

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
│               ├── AbstractNewsUserCase.php
│               ├── CreateNewsUseCase.php
│               ├── GetNewsItemUseCase.php
│               ├── GetNewsListUseCase.php
│               └── NewsDto.php
└── shared ... Sharedパッケージ
    └── src
        └── GetterTrait.php

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

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

サンプルコード

ユースケース層

ユースケース

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

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

use Acme\News\Domain\News\News;

class GetNewsListUseCase extends AbstractNewsUserCase
{
    /**
     * @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;
    }
}
DTO

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

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

use Acme\Shared\GetterTrait;

class NewsDto
{
    use GetterTrait;

    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;
    }
}

ドメイン層

リポジトリ

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;

    public function getNewsBySlug(string $slug): ?News;

    public function addNews(News $news): void;
}
エンティティ

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

<?php

declare(strict_types=1);

namespace Acme\News\Domain\News;

use Acme\Shared\GetterTrait;

class News
{
    use GetterTrait;

    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;
    }
}

何故こんなことをするのか?

もともとのコードは app/ 以下のMVCのコードだけで動作していました。

FLUDパターンにしたことで、packages/ 以下のドメイン層とユースケース層のコードが増えました。

これは、コードを書くことが好きすぎて、少しでも書くコードを増やそうとして増やしているわけではありません。

目的は、ドメイン層とユースケース層のコードを長期間運用可能にすることです。フレームワークやライブラリと密結合しないようにし、たとえフレームワークやライブラリが変更されても動作し続けるようにするためです。

また、本来ドメイン層にあるコードがフレームワークを利用することで、余計な制約を受けないようにするためです。OOPによるモデルの表現力を損なわないようにするためです。

例えば、ドメイン層のクラスがフレームワークのModelクラスを継承しているとしたら、本来必要ない継承関係がコードに含まれることになります。モデルとコードが解離しています。複雑なドメインモデルをOOPでコードにより表現することが妨げられます。

何故このサンプルは無駄に感じるのか?

CodeIgniter4のチュートリアルのコードをFLUDパターンに変更しました。このコードを見れば、何故、こんな無駄なことをするのだろうと疑問に思う人がいるかも知れません。

また、チュートリアルがこうなっていたら、なんだか同じようなコードをいくつも書かないといけなくて面倒だなと思うかも知れません。

これは、このサンプルのドメイン層のコードが薄いからです。要するに、複雑ではなくロジックもほぼありません。

本来、FLUDパターンは複雑なドメイン層をきれいにモデリングして、長期間運用可能にするためのものです。仮に、このチュートリアルのNewsサイトがこれでほぼ完成するとしたら(実際にNewsサイトが本当に単純なままかはわかりませんが)、FLUDパターンはオーバーヘッドが大きすぎます。

本来は、ドメイン層のクラスがもっともっと増えて、複雑になるようなケースで使うべきものです。

まとめ

  • フレームワークへの依存度を下げる方法として「FLUDパターン」があります。
  • FLUDパターンは3階層のシンプルなパターンです。
  • FLUDパターンは長期間運用される複雑なドメインで利用することが適したパターンです。
  • CodeIgniter4でもFLUDパターンを適用して、フレームワークへの依存度を下げることが可能です。

参考

Tags: codeigniter, codeigniter4, ddd, mvc, php

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

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

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

重複したコードが見られますので、FLUDパターンとは直接関係ないですが、リファクタリングしておきます。

リファクタリング

NewsとNewsDto

NewsNewsDto に重複したコードがありますので、トレイト化します。

--- a/packages/news/src/Domain/News/News.php
+++ b/packages/news/src/Domain/News/News.php
@@ -4,10 +4,12 @@ declare(strict_types=1);

 namespace Acme\News\Domain\News;

-use LogicException;
+use Acme\Shared\GetterTrait;

 class News
 {
+    use GetterTrait;
+
     private $id;
     private $title;
     private $slug;
@@ -24,22 +26,4 @@ class News
         $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;
-    }
 }
--- a/packages/news/src/UseCase/News/NewsDto.php
+++ b/packages/news/src/UseCase/News/NewsDto.php
@@ -4,10 +4,12 @@ declare(strict_types=1);

 namespace Acme\News\UseCase\News;

-use LogicException;
+use Acme\Shared\GetterTrait;

 class NewsDto
 {
+    use GetterTrait;
+
     private $id;
     private $title;
     private $slug;
@@ -24,22 +26,4 @@ class NewsDto
         $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;
-    }
 }

GetterTrait は複数のパッケージで使いそうですので、Sharedパッケージに配置します。

packages/shared/src/GetterTrait.php

<?php

declare(strict_types=1);

namespace Acme\Shared;

use LogicException;

trait GetterTrait
{
    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;
    }
}

UseCase

ユースケースクラスにも重複がありますので、抽象クラスを抽出します。

--- a/packages/news/src/UseCase/News/CreateNewsUseCase.php
+++ b/packages/news/src/UseCase/News/CreateNewsUseCase.php
@@ -5,20 +5,9 @@ declare(strict_types=1);
 namespace Acme\News\UseCase\News;

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

-class CreateNewsUseCase
+class CreateNewsUseCase extends AbstractNewsUserCase
 {
-    /**
-     * @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(
--- a/packages/news/src/UseCase/News/GetNewsItemUseCase.php
+++ b/packages/news/src/UseCase/News/GetNewsItemUseCase.php
@@ -5,20 +5,9 @@ declare(strict_types=1);
 namespace Acme\News\UseCase\News;

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

-class GetNewsItemUseCase
+class GetNewsItemUseCase extends AbstractNewsUserCase
 {
-    /**
-     * @var NewsRepositoryInterface
-     */
-    private $newsRepository;
-
-    public function __construct(NewsRepositoryInterface $newsRepositry)
-    {
-        $this->newsRepository = $newsRepositry;
-    }
-
     public function run(string $slug): ?NewsDto
     {
         /** @var News|null $news */
--- a/packages/news/src/UseCase/News/GetNewsListUseCase.php
+++ b/packages/news/src/UseCase/News/GetNewsListUseCase.php
@@ -5,20 +5,9 @@ declare(strict_types=1);
 namespace Acme\News\UseCase\News;

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

-class GetNewsListUseCase
+class GetNewsListUseCase extends AbstractNewsUserCase
 {
-    /**
-     * @var NewsRepositoryInterface
-     */
-    private $newsRepository;
-
-    public function __construct(NewsRepositoryInterface $newsRepositry)
-    {
-        $this->newsRepository = $newsRepositry;
-    }
-
     /**
      * @return NewsDto[]
      */

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

<?php

declare(strict_types=1);

namespace Acme\News\UseCase\News;

use Acme\News\Domain\News\NewsRepositoryInterface;

abstract class AbstractNewsUserCase
{
    /**
     * @var NewsRepositoryInterface
     */
    protected $newsRepository;

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

Newsコントローラ

重複したコードをコンストラクタに抽出します。

--- a/app/Controllers/News.php
+++ b/app/Controllers/News.php
@@ -10,10 +10,19 @@ use Acme\News\UseCase\News\CreateNewsUseCase;

 class News extends BaseController
 {
+    /**
+     * @var NewsRepository
+     */
+    private $repository;
+
+    public function __construct()
+    {
+        $this->repository = model(NewsRepository::class);
+    }
+
     public function index()
     {
-        $repository = model(NewsRepository::class);
-        $useCase = new GetNewsListUseCase($repository);
+        $useCase = new GetNewsListUseCase($this->repository);

         /** @var NewsDto[] $newsList */
         $newsList = $useCase->run();
@@ -30,8 +39,7 @@ class News extends BaseController

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

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

@@ -48,8 +56,7 @@ class News extends BaseController

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

         if ($this->request->getMethod() === 'post' && $this->validate([
             'title' => 'required|min_length[3]|max_length[255]',

ディレクトリ構成

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

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
│               ├── AbstractNewsUserCase.php ← 追加
│               ├── CreateNewsUseCase.php
│               ├── GetNewsItemUseCase.php
│               ├── GetNewsListUseCase.php
│               └── NewsDto.php
└── shared ... Sharedパッケージ
    └── src
        └── GetterTrait.php ← 追加

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

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

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

参考

Tags: codeigniter, codeigniter4, ddd, mvc, php