PHPのフレームワークは数多くありますが、その中でも特に人気を誇っているのはLaravelでしょうか。
そんなLaravelの魅力の一つといえば、DIの仕組みが内蔵されていて引数をタイプヒントするだけで気軽に注入できるところにあるでしょう。
Cake PHPは長らくDIの仕組みを持っていませんでしたが、近日(12/20)リリースされた4.2.0からDIの機能が追加されました。
注意:Cake PHPのDIは現在実験的な機能です。
最も簡単なDIの例
実際にどのように記述していくのか見ていきましょう。まずはコントローラーは以下の通りです。
<?php
declare(strict_types=1);
namespace App\Controller;
use App\UseCases\Users\CreateAction;
class UsersController extends AppController
{
public function add(CreateAction $createAction)
{
if ($this->request->is('post')) {
try {
$createAction->invoke($this->request->getData());
$this->Flash->success(__('ユーザーを作成しました。'));
return $this->redirect(['action' => 'index']);
} catch (\Exception $e) {
$this->Flash->error(__('予期せぬエラーが発生しました。'));
}
}
}
}
いつものCake PHPと明らかに違うところは、メソッドの引数でクラスが注入されているところです。後述するサービスコンテナで追加したクラスが引数の型指定をすることによってインスタンスを作成して渡してくれます。ユーザーの保存処理の殆どを、App\UseCases\Users\CreateAction
にまかせています。
UseCaseの実装も見てみましょう。
<?php
declare(strict_types=1);
namespace App\UseCases\Users;
use App\Services\UsersService;
use Cake\ORM\TableRegistry;
class CreateAction
{
protected $service;
public function __construct(UsersService $service)
{
$this->service = $service;
}
public function invoke(array $params): void
{
$users = TableRegistry::getTableLocator()->get('Users');
$user = $users->newEntity($params);
$user->plan = $this->service->selectPlan($user);
$users->saveOrFail($user);
}
}
このUseCaseは更にUsersService
をコンストラクタで注入しています。
UsersService
の実装です。
<?php
declare(strict_types=1);
namespace App\Services;
use App\Model\Entity\User;
class UsersService
{
public function selectPlan(User $user): string
{
if ($user->age > 19) {
return 'Adult Plan';
} else {
return 'Child Plan';
}
}
}
これらのサービスの作成は、src/Application.php
のservice()
メソッド内で行います。
/**
* Register application container services.
*
* @param \Cake\Core\ContainerInterface $container The Container to update.
* @return void
* @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
*/
public function services(ContainerInterface $container): void
{
$container
->add(CreateAction::class)
->addArgument(new UsersService());
}
$container->add()
メソッドで、注入したいクラスのクラス名を渡します。更に、注入したいクラスにさらに依存関係を注入したい場合、(CreateAction
のコンストラクタでUsersService
を注入したい場合)container->addArgument()
メソッドでさらに依存関係を追加できます。
インターフェースを登録する
もちろん、インターフェースをサービスに登録することも可能です。
$container->add(LogInterface::class, LogService($args));
コールバックを渡すことも可能です。
// $argsには、addArgumentで渡したものが入っています
$container
->add(LogInterface::class, function(...$args) {
return new LogService($args);
})
->addArgument(new SomeService());
シングルトン
いわゆるシングルトンを作成したいならば、share()
メソッドを利用します
$container->share(LogService::class);
サービスの拡張
一度登録したサービスに対して、extend
で追加の引数を渡すことができます。
$container->extend(LogService::class)
->addArgument('logLevel');
読み取り専用のConfigureの注入
サービスコンテナの仕組みを使えば、読み取り専用のConfigureとして注入することが可能です。
Cake\Core\ServiceConfig
クラスをサービスコンテナに登録します。
use Cake\Core\ServiceConfig;
$container->share(ServiceConfig::class);
ServiceConfig
が提供しているメソッドは、Configure
と異なりget
とhas
のみです。誤って設定を上書きすることがなく安全に扱えます。
// コントローラーなどで
public function index(ServiceConfig $config)
{
$key = $config->get('API_KEY'));
}
モックサービスの注入
テストコードにおいてConsoleIntegrationTestTrait
またはIntegrationTestTrait
を使っている場合には、サービスをモックに置き換えることが可能です。
$this->mockService(StripeService::class, function () {
return new FakeStripe();
});