目次

Apricot DIコンテナー

y2sunlight 2020-05-25

Apricot に戻る

関連記事

ApricotではDIコンテナーにLeague/Containerを採用します。主な用途はコントローラに対する Auto Wiring です。Auto Wiring とはコンストラクター引数の型ヒントを調べることにより、オブジェクトとそのすべての依存関係を再帰的に自動的に解決する機能の事です。地味な機能ですが、保守性向上の為、コンストラクター・インジェクションは必須と考えて実装することにしました。

また、DIコンテナーを使って、サービスプロバイターの仕組みも実装します。この機能については現版のApricotでは使用する場面が無かったので、スタブコントローラの中に例題をつくりました。

尚、DIコンテナーに関する設定ファイル( config/setting/container.setting.php )や初期設定ファイル( config/setup/container.setup.php )は今のところありません。カスタマイズの際は必要に応じて追加して下さい。


コントローラの Auto Wiring

以下は、現状のユーザコントローラのコンストラクタです。

    public function __construct()
    {
        // モデル
        $this->user = new User();
        ...
    }

このコードを Auto Wiring によって、次のようにすることがここでの目的です。

    public function __construct(User $user)
    {
        // モデル
        $this->user = $user;
        ...
    }

Auto Wiring は、コンストラクタの型ヒントによって生成するオブジェクトを判断します。(オブジェクト型でない引数には対応していません)


ActionInvokerクラス

コントローラを生成( new )しているのは、ActionInvoker クラスの invoke() メソッドです。このメソッドを以下のように変更します。

/apricot/core/Foundation

ActionInvoker.php
<?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]);
    }
}

Containerの使い方については、League/Containerを参照して下さい。


ユーザコントローラ

ユーザコントローラ (UserController )のコンストラクタをで述べたように以下のように変更して下さい。

/apricot/app/Controllers

UserController.php
<?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 でサービスプロバイダーを構築するには以下のステップに従います。

  1. League/Container の 基本サービスプロバイダークラス( AbstractServiceProvider )を拡張して 独自のサービスプロバイダーを作ります。
  2. League/Container の Container クラスに 独自のサービスプロバイダーを登録します。

Apricotでは、独自のサービスプロバイダーとして App\Provider クラスを定義し、それを登録する App\Foundation\Container クラスをシングルトンとして実装します。サービスの使用者は、App\Foundation\Containerが持っている PSR-11 に準じた get()has() を使ってサービスを利用することができます。


Providerクラス

以下に、League/Container の 基本サービスプロバイダークラス( AbstractServiceProvider )を拡張したApricot独自のサービスプロバイダークラス( Provider )を以下に示します。

/apricot/app

Provider.php
<?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クラス

App\Foundation\Containerクラスは、\League\Container\Container クラスを生成し、Apricotのサービスプロバイダー(Provider)を登録したクラスで、シングルトンとして動作します。

使用法: Container::{メソッド}

メソッド機能
mixed get(string $id)識別子idでコンテナのエントリを検索して返します。
bool has(string $id)コンテナが指定された識別子idのエントリを返すことができる場合はtrueを返します。

/apricot/app/Foundation

Container.php
<?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

StubController.php
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]);
    }
}


テスト実行

Apricotのホーム画面を表示して、[Menu2]をクリックして下さい。

■ 画面にユーザ数が表示されます

Number of registered users : 2