bearsunday / BEAR.Package

A BEAR.Sunday framework component package
https://packagist.org/packages/bear/package
MIT License
30 stars 26 forks source link

Example of dependency modification. #37

Closed koriym closed 11 years ago

koriym commented 11 years ago

SmartyProvider create Smarty instance.

class SmartyProvider implements Provide
{
    use TmpDirInject;
    use AppDirInject;

    /**
     * Return instance
     *
     * @return Smarty
     */
    public function get()
    {
        $smarty = new Smarty;
        $appPlugin = $this->appDir . '/vendor/libs/smarty/plugin/';
        $frameworkPlugin = __DIR__ . '/plugin';
        $smarty
            ->setCompileDir($this->tmpDir . '/smarty/template_c')
            ->setCacheDir($this->tmpDir . '/smarty/cache')
            ->setTemplateDir($this->appDir . '/Resource/View')
            ->setPluginsDir(array_merge($smarty->getPluginsDir(), [$appPlugin, $frameworkPlugin]) );

        return $smarty;
    }
}

SmartyConfigureProvider modify smarty object as we need.. This provider class can be changed by override binding.

class MySmartyConfigProvider implements ProviderInterface
{
    /**
     * @param Smarty $smarty
     *
     * @Inject
     */
    public function __construct(Smarty $smarty)
    {
        $this->smarty = $smarty;
    }

    /**
     * Return instance
     *
     * @return Smarty
     */
    public function get()
    {
        // config here

        return $this->smarty;
    }
}

SmartyModule connects all together.

class SmartyModule extends AbstractModule
{
    /**
     * Configure dependency binding
     *
     * @return void
     */
    protected function configure()
    {
        $this
            ->bind('BEAR\Sunday\Extension\TemplateEngine\TemplateEngineAdapterInterface')
            ->to(__NAMESPACE__ . '\SmartyAdapter')
            ->in(Scope::SINGLETON);
        $this
            ->bind('Smarty')
            ->annotatedWith('configured')
            ->toProvider(__NAMESPACE__ . '\SmartyConfigProvider');
        $this
            ->bind('Smarty')
            ->toProvider(__NAMESPACE__ . '\SmartyProvider')
            ->in(Scope::SINGLETON);
    }
}

SmartyAdapter needs @Named annotation to have "configured" dependency.

class SmartyAdapter implements TemplateEngineAdapterInterface
{
    ...

    /**
     * Constructor
     *
     * Smarty $smarty
     *
     * @Inject
     * @Named("configured")
     */
    public function __construct(Smarty $smarty)
    {
        $this->smarty = $smarty;
    }

素案でこういうのを考えてみました。 ping @tanakahisateru

koriym commented 11 years ago

SmartyProviderもそれを最終的に利用するコンシュマーも拡張がある事に無関心です。

しかし、こうやってデフォルトの設定とユーザーの設定の合成をつくるのもいいですが、SmartyProviderをコピーしてMySmartyProviderをつくった方が単純でいいのではないか?とも思います。(DRYを比較的軽視してます)

tanakahisateru commented 11 years ago

なるほど、やはりAOP的にはそういう考え方になるのですね。

これは僕の勉強不足かもしれないのですが、結局このSmartyModuleには、ユーザのMySmartyConfigProviderクラスの知識が必要になって、MySmartyModule になってしまわないのでしょうか?

class MySmartyModule {
    protected function configure()
    {
        // ...
        $this
            ->bind('Smarty')
            ->annotatedWith('configured')
            ->toProvider(__NAMESPACE__ . '\MySmartyConfigProvider'); // ここ

こうなると、現実的には、BEAR\Package\Provideの下からMyApp\Moduleに、ModuleとConfigProviderをひな形としてコピーしてくることになり、あまり知識を隠蔽できていない(Providerによるちょっとした初期設定だけ隠蔽している)のに、コンテナ側の複雑さが増えてしまいますよね... ちがいます?

※ もし、自分のAppModuleの中にMySmartyConfigProviderと書けば、デフォルトのConfigProviderをすり替えることができるというなら、アプリケーションでは、自分で書くクラスが1つだけになり、SmartyModuleのDIがどうなっているかをほとんど知らなくていい、となるので、素敵だと思います。

いっぽうで、もしBEAR.Packageが、ユーザにとって「デフォルトの構成サンプル」という立ち位置なんだとすると、よりリッチに拡張されたSmartyを提供するProviderだったり、もっと器用に振る舞うTemplateEngineAdapterだったりは、「デフォルトを参考にして各自それぞれ実装してください」とするのもいいかもしれませんね。 BEAR.Sundayのコアはあくまで疎結合構成のフレームワークなので(DIとAOPのコンテナであり、具体的な機能はリソース提供)、Webアプリケーションフレームワークを作るのはアーキテクトの仕事です、そこにフレームワークとしての縛りはありません、という意味で。

koriym commented 11 years ago

DIの束縛は上書き/あるいは先取りすることができます。 AppModuleで

$this->bind('Smarty')->annotatedWith('configured')->toProvider('MyApp\MySmartyConfigProvider');

あるいは@Named("configured")使わないで

$this->bind('Smarty')->toProvider('MyApp\MySmartyProvider');

これで優先的に束縛されます。

あるいはこの1つだけの束縛をMyphptalModuleモジュールにしてPhptalModule渡すようなつくりでもいいと思います。

$this->install(new PhptalModule(new MyphptalModule));

しかしこのような設定はアプリケーションを横断してチームや個人で共有されるものです。その意味では言われるようなMyModuleをつくってしまって

$this->install(new MyPhptalModule);

でもいいと思います。

更にこれを進めると

$this->install(new MyPhptalModule);
$this->install(new MyOrmModule);
$this->install(new MyCacheModule);

とFWの各コンポーネントの束縛の集合となり

$this->install(new MyAppPackageModule);

アプリケーションのconfigurationとなります。 実装を持たないBEAR\Sunday\Extensionに対する、実装と束縛の集合のBEAR\Package\Provideになります

koriym commented 11 years ago

余談ですが、これらの設定はAOPとは関係ありません。

koriym commented 11 years ago

結論として

A) ユーザーのよる可変点を明確にしてその変更の規模が大きい時(ロジックがはいるようなもの)はこの例のように別モジュール B) 少数の定数のインジェクトなら@Namedで受ける(現在TmpDirを受けてるように) C) それ以外ならProviderをAppModuleで変更 (SmartyProvider -> MySmartyProvider)

でどうでしょうか。

tanakahisateru commented 11 years ago

DIの上書き、理解しました! 上書きできる発想がなかったです。ConfigProvider のアイデアで、ユーザは自分の欲しい拡張をそれ以外の事情を知らずに足せます。これいい。

結論は、自分でコントロールしたければ独自Module/独自Providerを作ることもできる、としつつ、標準では拡張ポイントとして ConfigProvider を挟んである、ということですね。自身がないので、Smartyがその形になったとき、PHPTALも追従します。

koriym commented 11 years ago

SmartyもPHPTalもこのサイズだったらConfigureModuleなしで元のProviderを入れ替えるだけで良いと思いますがどうでしょうか。

tanakahisateru commented 11 years ago

たしかに (^^

koriym commented 11 years ago

DIを使った、違う設定の仕方も紹介しましょう。

この例では 生成 => 設定 => 「利用への注入」とごく一般的なフローで捉えていますが、これに縛られる必要はありません。利用への注入を済ませてから設定をすることもできます。

まずこのようにデフォルトの設定で注入をした後に、

$this->install(new PhptalModule);

そのインスタンスをAppModuleで取り出し、設定を行う事ができるはずです。

class ProdModule
{
    public function configure()
    {
        $phptal = $this->injector->getInstance('PHPTal');
        $phtal->setConfigForProdA();
        $phtal->setConfigForProdB();
    } 
}

先に注入を済ませたオブジェクトを後から変更できるのは直感に反する感じがあるのですが、これはインジェクターがPHPTalをシングルトンでデリバリーするので、セッターで注入されるオブジェクトも、後からインジェクターで取得するオブジェクトも同じものを指してるためです。

コンテキストに応じて設定を変更するのもこのようにコンテキスト別モジュールで自然にできます。

class DevModule
{
    public function configure()
    {
        $phptal = $this->injector->getInstance('PHPTal');
        $phtal->setConfigForDevA();
        $phtal->setConfigForDevB();
    }
}
koriym commented 11 years ago

最初のサンプルはインジェクションを行ってないのでをのままでは動きません。requestInjectionする必要があります。訂正します。

koriym commented 11 years ago

色々検討してみたのですがtoProviderでファクトリーを指定するようにtoModifierとか新設するのが良いのではないかと考えました。既存の設定に変更がなく、拡張が後から外付けできます。