— y2sunlight 2020-05-25
関連記事
ApricotではDIコンテナーにLeague/Containerを採用します。主な用途はコントローラに対する Auto Wiring
です。Auto Wiring
とはコンストラクター引数の型ヒントを調べることにより、オブジェクトとそのすべての依存関係を再帰的に自動的に解決する機能の事です。地味な機能ですが、保守性向上の為、コンストラクター・インジェクションは必須と考えて実装することにしました。
また、DIコンテナーを使って、サービスプロバイターの仕組みも実装します。この機能については現版のApricotでは使用する場面が無かったので、スタブコントローラの中に例題をつくりました。
尚、DIコンテナーに関する設定ファイル( config/setting/container.setting.php
)や初期設定ファイル( config/setup/container.setup.php
)は今のところありません。カスタマイズの際は必要に応じて追加して下さい。
以下は、現状のユーザコントローラのコンストラクタです。
public function __construct() { // モデル $this->user = new User(); ... }
このコードを Auto Wiring
によって、次のようにすることがここでの目的です。
public function __construct(User $user) { // モデル $this->user = $user; ... }
Auto Wiring
は、コンストラクタの型ヒントによって生成するオブジェクトを判断します。(オブジェクト型でない引数には対応していません)
コントローラを生成( new )しているのは、ActionInvoker クラスの invoke() メソッドです。このメソッドを以下のように変更します。
/apricot/core/Foundation
<?php namespace Core\Foundation; /** * Request ActionInvoker Class */ class ActionInvoker implements Invoker { .... /** * Invoke action * {@inheritDoc} * @see \Core\Foundation\Invoker::invoke() */ public function invoke() : Response { // Enable auto wiring $container = new \League\Container\Container; $container->delegate(new \League\Container\ReflectionContainer); // Get controller instance $instance = $container->get("\\App\\Controllers\\{$this->controller}"); return call_user_func_array(array($instance, 'invokeAction'), [$this->action, $this->params]); } }
Auto Wiring
を有効にします。Containerの使い方については、League/Containerを参照して下さい。
ユーザコントローラ (UserController )のコンストラクタを上で述べたように以下のように変更して下さい。
/apricot/app/Controllers
<?php namespace App\Controllers; use App\Exceptions\ApplicationException; use App\Foundation\Controller; use App\Models\User; use Core\Input; /** * ユーザコントローラ */ class UserController extends Controller { /** * User * @var \App\Models\User */ private $user; /** * ユーザコントローラの生成 */ public function __construct(User $user) { // モデル $this->user = $user; // インターセプター登録 $this->intercept('insert', 'UserInterceptor@insert'); $this->intercept('update', 'UserInterceptor@update'); // トランザクションアクション登録 $this->transactional('insert','update','delete'); } ... }
修正が終わったら、Apricotのユーザ一覧画面や編集画面を操作して動作確認をして見て下さい。
サービスコンテナを使用することで、サービスとサービス間の依存関係を登録しておいて後で取得することができます。例えば、サービスAがモデルBとモデルCを使用しているような場合、サービスコンテナにサービスAを要求すると、自動的にモデルBとCを生成し、それらをサービスAのコンストラクタに与えてサービスAを生成してくれます。
サービスプロバイターは、アプリケーション内の全てのサービスコンテナを登録し整理する方法を提供してくれます。また、サービスプロバイダーではサービスが取得された時点で遅延登録されるため、アプリケーションのパフォーマンス向上にも寄与します。
League/Container でサービスプロバイダーを構築するには以下のステップに従います。
AbstractServiceProvider
)を拡張して 独自のサービスプロバイダーを作ります。Container
クラスに 独自のサービスプロバイダーを登録します。
Apricotでは、独自のサービスプロバイダーとして App\Provider
クラスを定義し、それを登録する App\Foundation\Container
クラスをシングルトンとして実装します。サービスの使用者は、App\Foundation\Container
が持っている PSR-11 に準じた get()
と has()
を使ってサービスを利用することができます。
以下に、League/Container の 基本サービスプロバイダークラス( AbstractServiceProvider )を拡張したApricot独自のサービスプロバイダークラス( Provider )を以下に示します。
/apricot/app
<?php namespace App; use League\Container\ServiceProvider\AbstractServiceProvider; /** * Provider class for service */ class Provider extends AbstractServiceProvider { /** * The provided array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ // Example 'user', ]; /** * This is where the magic happens, within the method you can * access the container and register or retrieve anything * that you need to, but remember, every alias registered * within this method must be declared in the `$provides` array. */ public function register() { // Example $this->getContainer()->add('user', \App\Models\User::class ); } }
このクラスは、名前空間Appの直下に存在し、アプリケーションのモデル及びサービスのマップを提供します。現版のApricotでは、モデルはユーザモデル( User )だけで、サービスについては存在しません。モデルやサービスを追加する場合は、上例に習って適宜追加して下さい。
現版のApricotでは、サービスは存在しませんが、サービス用として以下のフォルダが予約されています。
/apricot/app/Services
尚、League/Container のサービスプロバイダーについての詳細はこちらをご覧ください。
App\Foundation\Containerクラスは、\League\Container\Container クラスを生成し、Apricotのサービスプロバイダー(Provider)を登録したクラスで、シングルトンとして動作します。
使用法: Container::{メソッド}
メソッド | 機能 |
---|---|
mixed get(string $id) | 識別子idでコンテナのエントリを検索して返します。 |
bool has(string $id) | コンテナが指定された識別子idのエントリを返すことができる場合はtrueを返します。 |
/apricot/app/Foundation
<?php namespace App\Foundation; use Core\Foundation\Singleton; use App\Provider; /** * Container class for service * * @method static Container getInstance(); * @method static mixed get(string $id) Finds an entry of the container by its identifier and returns it. * @method static bool has(string $id) Returns true if the container can return an entry for the given identifier. */ class Container extends Singleton { /** * Create Container instance. * @return \League\Container\Container */ protected static function createInstance() { $container = new \League\Container\Container; $container->addServiceProvider(new Provider()); return $container; } }
サービスコンテナをテストするために、スタブコントローラを以下のように修正します。
/apricot/app/Controllers
namespace App\Controllers; use App\Foundation\Container; use App\Foundation\Controller; /** * Stubコントローラ */ class StubController extends Controller { /** * Stub Page * @return \Core\Foundation\Response */ public function index(int $no=null) { $title = "Stub {$no}"; /* * Example for Container * @var \App\Models\User $user */ $user = Container::get('user'); $userCount = count($user->findAll()); $messages[] = "Number of registered users : {$userCount}"; return render('stub',['title'=>$title,'messages'=>$messages]); } }
Container::get('user')
でユーザモデルを生成します。findAll()
を実行して全ユーザのリストを取得します。$messages
をセットします。
Apricotのホーム画面を表示して、[Menu2]をクリックして下さい。
■ 画面にユーザ数が表示されます
Number of registered users : 2