Hatena::ブログ(Diary)

noopな日々 このページをアンテナに追加 RSSフィード

2013-05-10

zf2.2 translator周りの変更点

※2.2の変更履歴にありました。

デフォルトのトランスレータを設定するのに、

<?php
        $translator = $serviceManager->get('translator');
        \Zend\Validator\AbstractValidator::setDefaultTranslator($translator);

としているケースがありますが、古い設定のままだと、

Argument 1 passed to Zend\Validator\AbstractValidator::setTranslator() must be an instance of Zend\Validator\Translator\TranslatorInterface, instance of Zend\I18n\Translator\Translator given.

といったエラーになりました。

変更があって、下記のコメントの通り、translatorのfactoryをZend\Mvc\Service\TranslatorServiceFactoryに変更したほうがいいようです。

https://github.com/zendframework/zf2/issues/4413#issuecomment-17405785

Skeletonではどうなっているかを見ると、

https://github.com/zendframework/ZendSkeletonApplication/blob/master/module/Application/config/module.config.php#L60-L62

<?php
array(
    'service_manager' => array(
        'abstract_factories' => array(
            'Zend\Cache\Service\StorageCacheAbstractServiceFactory',
            'Zend\Log\LoggerAbstractServiceFactory',
        ),
        'aliases' => array(
            'translator' => 'MvcTranslator',
        ),
    ),
)

なるほど、この通りでOKのようです。translatorを使っているのが一箇所なら、MvcTranslatorを指定した方がてっとり早いかもしれませんね。

translatorの設定が不要になりそう。Applicationモジュールに依存するのはつらいですよね。 translatorキーで依存を解決しているものが残っているので、aliasesかfactoryかどちらかで指定する必要はまだ、ありそうです。

<?php
        $translator = $serviceManager->get('MvcTranslator');
        \Zend\Validator\AbstractValidator::setDefaultTranslator($translator);

2013-04-12 Configのキャッシュについて

無名関数とvar_export

有名な話なのかもしれませんが、メモ

設定配列などを読み込んでマージした後にキャッシュ目的でvar_exportする、といったことはあると思いますが、

<?php
//設定ファイル
$foo = array(
    'test' => function(){return new StdClass;},
);

//エクスポートされた内容を確認
echo var_export($foo, true) . ';';

//---result---
array (
  'test' => 
  Closure::__set_state(array(
  )),
);

ところが、Closureには__set_stateは定義されていませんので、このままキャッシュしても正しく戻せません。

やってみます。

<?php
//設定ファイル
$foo = array(
    'test' => function(){return new StdClass;},
);

$fp = fopen("php://memory", "r+");
fputs($fp, var_export($foo, true) . ';');

rewind($fp);

eval(stream_get_contents($fp));

fclose($fp);

結果、

Fatal error: Call to undefined method Closure::__set_state()

となります。(php5.4.12で確認)

ファイルに書き出して、includeしても同じです。

そこで回避方法ですが、こういうのがありますね。serializableでクロージャーをラップする方法ですね。なるほど。

しかし、無名関数を宣言しているファイルを読み込んでコード行から判定しているようですが、書き方によってはハマリそうです。namespaceも反映されていない気がしますが、大丈夫なんでしょうか。

http://www.htmlist.com/development/extending-php-5-3-closures-with-serialization-and-reflection/

<?php
$closure = new SuperClosure(
	function($num1, $num2) {return $num1 + $num2;}
);

zf2のモジュールコンフィグのキャッシュ

zf2のアプリケーション、サービス、Diやモジュール用設定は毎回、ほぼ同じ設定を読み込みます。複数のファイルへ毎回アクセスしてマージするのももったいないので、簡単にキャッシュする方法が用意されています。(※ベンチを取るならせめてこのくらいのことはやって欲しいです。ページキャッシュのようにベンチを無意味にするキャッシュではないので。)

https://github.com/zendframework/ZendSkeletonApplication/blob/master/config/application.config.php

スケルトンアプリの設定にもあるように、コンフィグのキャッシュを有効にします。

'config_cache_enabled' => true,

'config_cache_key' => '123',

'cache_dir' => 'data/cache',

'module_map_cache_enabled' => true,

'module_map_cache_key' => '456',

たとえば、上記のkey値を別ファイルにしてディプロイ時に更新されるようにするとか、キャッシュファイルを消すなどで更新されるようにしておけばOKですね。

key値を更新していく方法なら、古いキャッシュファイルが残ってしまいますが。

さて、稀にglobalコンフィグで、サービス定義を無名関数で与えている場合に、上記した、var_exportの問題にはまることになります。

この点、すでにissueに上がっているようなんですが、closeされていますね。

https://github.com/zendframework/zf2/issues/2697

対策1:素直にFactoryクラスを使う

サービス設定では、Factoryを用意するというのが王道だろうと思います。

対策2:無名関数をserializableでラップする

上記した、serializableにする工夫を施せばグローバルに使えますが・・・

対策3:設定ファイル中のClosureは、モジュール側へ移すと解決されるという説

https://github.com/SpoonX/SxBootstrap/pull/9

モジュール内で、getConfigで読まれた設定はキャッシュされますが、Module::getServiceConfigで読まれた設定は、ロード後に読まれるようです。そのため、getServiceConfig側でClosureを使う分にはキャッシュしないので問題は生じません。

この仕様が意図的なものかどうかはわかりません(testされていない)ので、この方法を取る場合は自前でテストを追加してフレームワークの更新時にチェックする必要がありそうです。

2013-03-04 このエントリーを含むブックマーク このエントリーのブックマークコメント

ZF2のDi関連で

Zend\Db\Adapter\AdapterとProfilerAwareInterfaceについて

ZF2.1でZend\Db\AdapterにProfilerAwareInterfaceが追加されました。

使い方にもよりますが、Zend\Di\Diで自動化している場合、2.0系の設定でそのまま使うと、

'Invalid instantiator of type "NULL" for "Zend\\Db\\Adapter\\Profiler\\ProfilerInterface".

という例外が発生することがあります。

簡単な解決としては、Profilerを加えてしまうというのがあります。

2.0系での設定例としては、下記のような感じでできますが、

<?php
return array(
'di' => array (
    'instance' => array(
        'Zend\Db\Adapter\Adapter' => array(
            'parameters' => array(
                'driver' => array(
                    'dbname' => 'test',
                    //その他パラメーター
                ),
            ),
        ),
    ),
),
);

2.1からはProfilerAwareInterfaceが追加されたので、パラメーターとしてProfilerを渡します。

<?php
return array(
'di' => array (
    'instance' => array(
        'Zend\Db\Adapter\Adapter' => array(
            'parameters' => array(
                'driver' => array(
                    'dbname' => 'test',
                    //その他パラメーター
                ),
                'profiler' => 'Zend\Db\Adapter\Profiler\Profiler',
            ),
        ),
    ),
),
);

'profiler' => 'Zend\Db\Adapter\Profiler\Profiler',

の行ですが、

ProfilerAwareInterfaceがsetProfilerで$profilerをパラメーターとして定義しているため、'profiler'という名前で、ProfilerInterfaceを実装しているクラス名を指定します。これにより、自動的にProfilerインスタンスを抽入してくれます。

「Profilerがいらないとき、困らない?」というスタンスと「AwareInterfaceは依存の表明だから、必須だ」というスタンスがありえますね。以前にMLで議論されていた気がしますが、とりあえず両方に対応できるようにしておくのがいいかもしれません。

(追記) AwareInterfaceは依存の表明でprofilerがsetterインジェクションで自動的に抽入されます。一方、〜〜CapableInterfaceは特定の機能をgetterでgetできることを表すようです。他にもパターンがあるのでどこかで一覧ができているといいですよね。

Zend\Diをデバッグする

デバッグするといっても基本的にはDi定義を参照してそれに対する動作を確認することに尽きます。

たとえば、リフレクションで指定されたZend\Db\Adapter\Adapterの定義を確認してみます。

<?php
Zend\Di\Display\Console::export($di);
Zend\Di\Display\Console::export($di, array('Zend\Db\Adapter\Adapter'));

結果

    Parameters For Class: Zend\Db\Adapter\Adapter
      driver [type: scalar, required, injection-method: __construct fq-name: Zend\Db\Adapter\Adapter::__construct:0]
      platform [type: Zend\Db\Adapter\Platform\PlatformInterface, not required, injection-method: __construct fq-name: Zend\Db\Adapter\Adapter::__construct:1]
      queryResultPrototype [type: Zend\Db\ResultSet\ResultSetInterface, not required, injection-method: __construct fq-name: Zend\Db\Adapter\Adapter::__construct:2]
      profiler [type: Zend\Db\Adapter\Profiler\ProfilerInterface, not required, injection-method: __construct fq-name: Zend\Db\Adapter\Adapter::__construct:3]
      profiler [type: Zend\Db\Adapter\Profiler\ProfilerInterface, required, injection-method: setProfiler fq-name: Zend\Db\Adapter\Adapter::setProfiler:0]

Diの設定として、driver platform queryResultPrototype profilerが使えることを意味しています。

この中で、profilerが2行あり、not requiredとrequiredがあります。

この違いは、最初のprofilerはコンストラクタのオプションで、次行のprofilerはserProfilerで使うprofilerです。

さて、この出力結果から、最初に書いたProfilerAwareInterfaceに関する挙動が出てくると判断できればよいのですが、もう少し詳しく見てみる必要がありそうです。

これらパラメーターとinjectionMethodの組み合わせのうち必須とマークされたものかどうかをチェックしてみます。

上記の出力では不十分で、CompilerDefinitionを使って設定をファイルに書き出してチェックするか、自前で出力することになります。

大雑把ですが、こんな感じ。

<?php
class RuntimeDefinitionExporter extends RuntimeDefinition {
    public function export()
    {
        return var_export($this->classes);
    }
}

Zend\Db\Adapter\Adapterの出力結果は下記になります。

<?php
return array (
  'Zend\\Db\\Adapter\\Adapter' => 
  array (
    'supertypes' => 
    array (
      0 => 'Zend\\Db\\Adapter\\AdapterInterface',
      1 => 'Zend\\Db\\Adapter\\Profiler\\ProfilerAwareInterface',
    ),
    'instantiator' => '__construct',
    'methods' => 
    array (
      '__construct' => true,
      'setProfiler' => true,
    ),
    'parameters' => 
    array (
      '__construct' => 
      array (
        'Zend\\Db\\Adapter\\Adapter::__construct:0' => 
        array (
          0 => 'driver',
          1 => NULL,
          2 => true,
          3 => NULL,
        ),
        'Zend\\Db\\Adapter\\Adapter::__construct:1' => 
        array (
          0 => 'platform',
          1 => 'Zend\\Db\\Adapter\\Platform\\PlatformInterface',
          2 => false,
          3 => NULL,
        ),
        'Zend\\Db\\Adapter\\Adapter::__construct:2' => 
        array (
          0 => 'queryResultPrototype',
          1 => 'Zend\\Db\\ResultSet\\ResultSetInterface',
          2 => false,
          3 => NULL,
        ),
        'Zend\\Db\\Adapter\\Adapter::__construct:3' => 
        array (
          0 => 'profiler',
          1 => 'Zend\\Db\\Adapter\\Profiler\\ProfilerInterface',
          2 => false,
          3 => NULL,
        ),
      ),
      'setProfiler' => 
      array (
        'Zend\\Db\\Adapter\\Adapter::setProfiler:0' => 
        array (
          0 => 'profiler',
          1 => 'Zend\\Db\\Adapter\\Profiler\\ProfilerInterface',
          2 => true,
          3 => NULL,
        ),
      ),
    ),
  ),
);

これを見れば、はっきりします。

まず、Zend\Di\Display\Console::exportの出力結果で、profilerが2度出現した理由は、定義配列中のparameters内で、メソッド毎の解析結果からprofilerという名前でコンストラクタとセッターの2箇所の定義が生じていることがわかります。

コンストラクタのprofilerはfalse (= not required) setProfilerのprofilerはtrue ( = required)と表示された理由がここにあります。

次に、Zend\Diが依存の自動解決を試みるかどうかですが、これはmethodsにあるメソッドがtrueなら必須であり、falseなら任意です。

Instantiatorや〜〜AwareInterface中のsetterは必須としてRuntimeDefinitionが解析します。単純なsetterは任意となります。

したがって、最初の問いに戻りますが、

Zend\Db\Adapter\AdapterがProfilerAwareInterfaceを実装しているとき、profilerはAwareInterfaceによる強制で解決される必要があるとすれば、設定中に書かれていれば、そのProfilerがセットされ、Profilerの指定が何もなければ例外が発生することになります。

では、AwareInterfaceではあるが、Profilerは不要と判断している場合はどうでしょうか。

この場合は少し遠回りになりますが、Definitionを調整してDefinitionListに加える方法がよいのではないかと思っています。

調整箇所は

    'methods' => 
    array (
      '__construct' => true,
      'setProfiler' => false, // true,
    ),

という具合に、trueをfalseに変更します。

そこで、ClassDefinitionの定義をdiに加えます。

<?php
return array(
    'di' => array(
        'definition' => array(
            'class' => array(
                'Zend\Db\Adapter\Adapter' =>  array(
                    'methods' => array(
                        'setProfiler' => array(
                            'profiler' => array(
                                'name' => 'profiler',
                                'type' => 'Zend\Db\Adapter\Profiler\ProfilerInterface',
                                'required' => false,
                            ),
                        ), 
                    ),
                ),
            ),
        ),
    ),
),

ClassDefinitionを定義してもRuntimeDefinitionは走ります。もし、RuntimeDefinitionを走らせたくないときは、モジュールのbootstrap時にでも、ArrayDefinitionでそれを読み込んでDefinitionListに加えるか、DiFactoryの修正版を作り、service_listener_optionsでDependencyInjectorキーに割り当てればOKでしょう。

まとめると、FooAwareInterfaceでは、Interface上で定義されたsetterは依存の表明となり、Diコンテナが強制的に抽入を試みます。Zend\Db\Adapter\Adapterの場合は、parametersでprofilerを指定します。

抽入を任意にしたい場合は、ClassDefinitionを追加し、methods定義でrequiredをfalseに設定します。

※追記

http://d.hatena.ne.jp/n314/20130417/1366226869

こちらで、addTypePreferenceによるインターフェースへのデフォルト実装と、全体でインターフェースインジェクションを停止する方法が紹介されています。

Profilerのように横断的に共通するものについてはaddTypePreferenceがいいですね。

2012-10-25 zf2 - EventManagerの詳細

EventManagerの詳細

ZendFramework2を使うメリットの一つに新しいEventManagerがあります。ZF2の特徴的部分としては、インターフェース単位でイベントを管理したり、対象メソッドをリスナーとして並列化したり、リスナーの型変換ルールを規定したりと、なかなか公式ドキュメントだけでは伝わらない内容もありますので、まとめてみました。

前半はSymfony2との共通点、後半はzf2の特徴になります。

  1. Symfony2のドキュメントを見ながら比較しつつ共通項を見る
    • イベントオブジェクト
    • イベントのユニーク性(命名規則・インターフェース)
    • ディスパッチャのスコープと抽入
    • リスナーとサブスクライバ
    • イベントの流れを止める
  2. ZF2のEventManagerの特徴
    • EventManagerとSharedEventManager
      • EventManagerについて
      • SharedEventManagerとインターフェース単位のイベント
    • zf2推奨のトリガーとプライオリティ
    • イベントフロー、もうひとつの終了方法

Symfony2のドキュメントを見ながら比較しつつ共通項を見る

Symfony2はすばらしいフレームワークで、ドキュメントも充実していていいですね。ここでは、Symfony2のドキュメントを参照しながら共通項を探ってみたいと思います。基本的にはこちら。文中、このドキュメントからの引用とzf2を比較していきます。

http://docs.symfony.gr.jp/symfony2/book/internals.html#event-dispatcher

イベントオブジェクト

通常は、特定のイベントに対するリスナーで必要とされるデータは、``Event`` オブジェクトとともにリスナーへ渡します。 kernel.response イベントの場合、作成され各リスナーへ渡される Event オブジェクトは、基底の Event オブジェクトのサブクラスである Symfony\Component\HttpKernel\Event\FilterResponseEvent のインスタンスです。 このクラスには getResponse や setResponse といったメソッドがあり、Response オブジェクトを取得したり置き換えたりできます。

通常のストーリーは次のようになります。 イベントに対するリスナーを作成する時、リスナーへ渡される Event オブジェクトは専用のサブクラスで、イベントから情報を取得したり、イベントに情報を返すための追加のメソッドを持っています。

http://docs.symfony.gr.jp/symfony2/book/internals.html#index-14

ZF2でも、基底のEventはシンプルで、Applicationで利用するイベントオブジェクトはMvcEventになります。MvcEventオブジェクトにはMvcフローで使われるリクエストやレスポンス、ビューモデルなどさまざまなオブジェクトのコンテナになります。

イベントのユニーク性(命名規則・インターフェース)

ユニークなイベント名には任意の文字列を使えますが、次のような簡単な命名規約に従うことが推奨されています:

  • 小文字、数字、ドット (.) およびアンダースコア (_) のみを使うようにしてください。
  • プレフィックス名前空間とドットを使います (例 kernel.)。
  • 実行されるアクションの内容を表す動詞で終わるようにします (例 request)。

次に、良いイベント名の例を示します:

  • kernel.response
  • form.pre_set_data
http://docs.symfony.gr.jp/symfony2/book/internals.html#index-13

イベント名を識別するのに名前空間とアクションの組み合わせになるパターンはよく見られます。

zf2では文字列としては結合せず、独立したパラメータとして名前空間の部分はID、アクションに相当するのがイベント名になります。zf2でのIDには少し特徴があり、拡張性の肝になっているので、SharedEventManagerのところで後述します。

イベントディスパッチャのスコープと抽入

EventDispatcher クラスのソースコードを読むと、シングルトンではないことに気づくでしょう (getInstance() スタティックメソッドは定義されていません)。 これは設計者の意図で、単一の PHP リクエストの処理中に複数のイベントディスパッチャーを同時に実行したい場合に役立ちます。 その代わりに、イベントへの接続や通知を行う必要があるオブジェクトへ、ディスパッチャーオブジェクトを渡す必要があります。

この問題に対するベストプラクティスは、対象となるオブジェクトへディスパッチャーオブジェクトを注入することで、これを依存性の注入 (dependency injection) と呼びます。

http://docs.symfony.gr.jp/symfony2/book/internals.html#id29

zf2で、Symfony2のEventDispatcherに相当するのはEventManagerです。Symfony2でEventDispatcherがアプリケーション全体をラップしていると思いますが、zf2の場合、個々のオブジェクトに対してEventManagerインスタンスを割り当て、EventManagerオブジェクトのスコープはイベントを発生させるオブジェクト(=target)に対応することになります。通常、イベントオブジェクトにsetTargetで登録します。また、EventManagerのIdentifierとしてTargetのクラス名やインターフェース名などを登録し、これがSymfony2での名前空間と同じ役割を果たします。ここがかなり重要になりますので冗長ですが後述します。

EventManagerの抽入については、EventManagerAwareInterfaceを実装してサービスマネージャーで抽入してもらうこともできますし、もともとイベントのスコープはそのオブジェクト内でのものなので、innerクラス的に内部でnewしたとしても原則に反しないと思います。PHP5.4で、trait ProvidesEventsを使うか、5.4以前ならProvidesEvents相当のメソッドを実装することでイベントフルなオブジェクトになります。

リスナーとサブスクライバ

イベントを監視する最も一般的な方法は、イベントリスナーをディスパッチャーに登録することです。 リスナーは 1 つ以上のイベントを監視でき、イベントがディスパッチされるたびに通知されます。

http://docs.symfony.gr.jp/symfony2/book/internals.html#index-17

Symfony2と同様、zf2でもリスナーを同時に複数のイベントをリッスンするように登録できます。

また、

イベントを監視するもう 1 つの方法が、イベントサブスクライバ です。 イベントサブスクライバは PHP のクラスで、ディスパッチャーに対してどのイベントが監視されるのかを、サブスクライバクラス自身が通知します。

これはリスナークラスとよく似ていますが、監視するイベントの種類をクラス自身がディスパッチャーに対して通知できる点が異なります。

http://docs.symfony.gr.jp/symfony2/book/internals.html#index-17

これは、リスナーを登録する詳細をどのオブジェクトの責務にするかという問題になります。

たとえばbootstrapで、あるリスナーを特定のイベントと結びつけて登録する処理を書いたとします。この関連付けを決定するのはbootstrapクラスとなり、bootsrapにイベントの関連付けの知識が配置されます。これは目先、問題がないように見えますが、リッスン対象を変えるときに、bootstrapにも変更が必要になります。これは、リスナーとリスナーを登録するオブジェクトの間の結合を意味します。Diによって関連付けは分離されますが、もし直接書いたとしたらという仮定です。設定ファイルに分離すれば疎結合かというとそうでもないのです。

サブスクライバはこの、"どのイベントをリッスンするかという知識"についてサブスクライバオブジェクトが責任を持つことを意味します。bootstrapは、そのサブスクライバを使用する使用しないだけを決定すればよく、どのイベントをリッスンしているのかということについての知識が不要になります。イベント変更時もサブスクライバ内部のみになります。疎結合、いいですね。

zf2での、これと同じ仕組みはListnerAggregateです。詳細はドキュメントで!

イベントの流れを止める

イベントループ中、そのフローを中断したいときがあります。

リスナーの処理後に、他のリスナーが呼び出されないようにしたい場合もあります。 つまり、リスナーからディスパッチャーに対して、以降のリスナーへのイベントの伝播を停止する (これ以上リスナーへ通知を行わない) よう要求できるようにしたいのです。 このような場合は、リスナーから :method:`Symfony\\Component\\EventDispatcher\\Event::stopPropagation` method: メソッドを呼び出します。

http://docs.symfony.gr.jp/symfony2/book/internals.html#event-dispatcher-event-propagation

zf2でも同様で、$event->stopPropagation();でそのイベントにループの停止を指定できます。

zf2での終了条件はもう一つありますが、それについては後述します。

ZF2のEventManagerの特徴

EventManagerとSharedEventManager

zf2でのEventManagerはイベント供給元オブジェクトにつき一つのスコープとして使います。(もちろん、自由なので複数のオブジェクトを跨ぐことも、オブジェクト以外のスコープで作成することもできますが)

EventManager

Mvc上ではじめに現れるのは、ApplicationスコープでApplicationオブジェクトに付随したEventManagerとMvcEvent。また、個々のコントローラーでも、それぞれEventManagerとMvcEventが割り当てられます。そのため、イベントの名前空間という考え方はなく、対象オブジェクトがスコープを代表します。

では、アプリケーションの任意の場所でそれらにリスナーを登録するにはどうするかですが、

  • 直接オブジェクトを取得して登録する。
    • アプリケーションオブジェクトを取得して、そのEventManagerにattachする
    • コントローラーを取得して、そのEventManagerにattachする

ApplicationやControllerなどを既知なオブジェクトであれば、直接attachすることができます。たとえば、Module.phpはデフォルトでonBootstrapメソッドが呼ばれますので、

<?php
    public function onBootstrap(MvcEvent $e)
    {
        $EventManager = $e->getApplication()->getEventManager();
        $EventManager->attach('route', array($this, 'onRoute'));
        $EventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'onError'), -1);
    }

こういう書き方で、自身をリスナーとして登録することが可能です。

SharedEventManagerとインターフェース単位のイベント

Mvcフローでレスポンスにフィルターを加えたいとか、ルーティングに介入して別の値を付与したいとか、サービスを実行する前に権限チェックを加えたい、など、アプリケーションには共通の処理があります。zf1ではこれらを実装するとき、プラグインを利用していました。

EventManagerが各オブジェクトにスコープが絞られるため個々のオブジェクトを知らないと横断的リスナー(zf1でのプラグイン)を登録することができません。そこで、zf2ではSharedEventManagerを使います。

SharedEventManagerは、イベントをグローバルにシェアします。オフにもできますし、一定のスコープを与えることも可能ですが、通常はグローバルに使います。

個々のEventManagerをイベント対応オブジェクトに登録するとき、Identifierを登録します。その際、イベントのキーになるインターフェース名やクラス名をIDとして複数登録します。複数登録することで、リスナーに自由度を与えます。このIDをグローバルにシェアするのが、SharedEventManagerという風に理解していいと思います。

下記はAbstractControllerが自EventManagerを初期化するソース

<?php
        $events->setIdentifiers(array(
            'Zend\Stdlib\DispatchableInterface',
            __CLASS__,
            get_called_class(),
            $this->eventIdentifier,
            substr(get_called_class(), 0, strpos(get_called_class(), '\\'))
        ));
        $this->events = $events;
        $this->attachDefaultListeners();

遅延静的束縛で、[インターフェース名, Abstract名, クラス名, 名前空間]をIdentifierとして登録しています。これにより、SharedEventManagerを経由して目的に応じたリスナー登録が可能になっています。

Zend\Mvc\View\Http\ViewManagerでコントローラーのdispatchイベントを-100のプライオリティでリッスンするというソースが下記です。直接コントローラーオブジェクトやコントローラーに付属するEventManagerを知る必要はなく、コントローラーの振る舞いを定義したインターフェースのイベントをリッスンしているわけです。

<?php
$sharedEvents->attach('Zend\Stdlib\DispatchableInterface'
, MvcEvent::EVENT_DISPATCH
, array($injectViewModelListener, 'injectViewModel')
, -100);

インターフェース名、クラス名をIDとして使うメリットは二つあります。

  • クラス名、インターフェース名はグローバルに一意であり、名前の衝突が起きない。
  • 横断的関心事はそもそもオブジェクトに対してではなく、インターフェースメソッド(もしくはクラスメソッド)に対する関心事であるため、意味が明白。

アプリケーション上の都合でも、設計上の意味的にもSharedEventManagerは自然です。

たとえば「ログリスナーがentityInterfaceを実装したオブジェクトのsaveイベントをリッスンしてロギングする」という、意味的に直結したイベント管理ができることになります。

  • entityInterfaceのsaveイベント
  • adminEntity のsaveイベント

adminEntity自身はsaveイベントをトリガーするだけで、グローバルなリスナー、個別リスナーのそれぞれにイベントを送ることができるわけです。リスナーにはグローバルにentityをリッスンするのか、adminEntityだけをリッスンするのかを選択できる柔軟性が生まれます。

EventManagerはオブジェクト単位で直接的、SharedEventManagerはグローバルで横断的なイベント管理を行うことができます。

trait ProvidesEventsが示す模範例

インターフェースベースのイベント管理と言っても、実際にイベントを発生させる側がそれに従ってくれなければ問題を生じます。trait ProvidesEventsでは、イベント発生側の模範例を示してくれています。

https://github.com/zendframework/zf2/blob/master/library/Zend/EventManager/ProvidesEvents.php

このソース中でIdentifierにはインターフェース名、クラス名相当のものを使うという模範が示されています。メンバ変数でIdentifierを追加することもできるので、PHP5.4↑な環境では、ProvidesEventsを使い、そうでない場合は、ProvidesEvents相当のソースを組み込むのがよいでしょう。

zf2推奨のトリガーとプライオリティ

zf1でもそうですが、特定のメソッドの前後でイベントをトリガーし、前後処理を行うパターンがあります。

zf2での推奨パターンとしては、前後をイベントで挟むのではなく、対象メソッドをイベントリスナーとしてプライオリティ1として登録し、trigger経由で実行します。他のリスナーは1以上ならpre、それ以下ならpostという具合に優先度をつけてイベントループ中に配置します。

zf2でよくあるイベント呼び出しのパターンとしては、主に2つあり、

  1. イベント経由で他のオブジェクトに処理を依頼する。
  2. 自オブジェクトのメソッド実行をイベント対応にする。

1の方は、呼び出し元はイベントをトリガーするだけで、主に処理をするオブジェクトをリスナーとして登録、リスナーが処理の主体になります。たとえば、ApplicationオブジェクトはgetRouter()->route()で直接依頼するのではなく、EVENT_ROUTEをトリガーしてRouteListenerに任せます。

2の方は、自オブジェクトをイベント対応にし、振る舞いの変更を可能にします。たとえば、fooメソッドをイベント対応にするとき、fooメソッド内ではイベントをトリガーし、onFooをプライオリティ1としてとしてリッスンします。

メソッドの振る舞いを根本的に変更したいとします。前後処理パターンの場合、メソッド側で変更可能性を考慮して注意深く実装する必要がありますが、1のパターンならリスナーを選択することで完全に処理を変更できます。

2のパターンなら、デフォルトのリスナーとして登録されたメソッドをdetachして代わりのリスナーを登録してしまうことができます。(tips:プライオリティ1のうち、最初に取得できるのがデフォルトのリスナーです。)文字通りメソッドの振る舞いを交換可能です。実際にonFooをdetachすることはないかもしれませんが、そこまで可能な仕組みになっています。また、detachせず、上位プライオリティを与えて条件次第で、fooメソッドを呼ぶ前に正常終了する方法もあります。

これらにより、メソッドの振る舞いを変更するために細かなクラス継承を行ったり、イベントフックを意識したメソッド実装を行う必要がなくなります。Zend\Mvcの各所でこのパターンで実装されているので、zf2でモジュールを作成する際にもこのパターンに従っておくとシェアしやすいと思います。

イベント対応にしたメソッドはリスナーと並列的扱えることで従来のフィルタ的な動作も、動作モデルを変更することも可能となっています。

イベントフロー、もうひとつの終了方法

zf2でのイベントをトリガーする方法のひとつとして、下記のようなものがあります。

<?php
$result = $this->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH, $e, function($test) {
    return ($test instanceof Response);
});

コントローラーのアクションを呼び出すが、もし、リスナーが返す結果がレスポンスオブジェクトならイベントループを終了します。

これはイベントループの型の推移をわかりやすくしています。

リスナーの実装はイベントオブジェクトを与えられ、型Aまたは型Bを返す。型Aならループを継続し、型Bならループをただちに正常終了して型Bを結果とする。最後のリスナーまで到達したとき、最後に渡された型Aをイベントループの結果とする。

異常終了の際は、stopPropergation()をセットし、中断フラグをイベントオブジェクトにセットした上でレスポンス型Eで返す。通常Eはエラー内容を示す。

これにこだわる必要こそありませんが、このパターンは各種実装でも見られるクリーンなイベントループパターンなので、単にイベントを呼ぶ、呼ばれる形で介入するのではなくリスナーの振る舞いを決めるとよいですね。

これらは、zf1でのテンプレートメソッドパターンに乗ったイベントフローではショートカットして正常終了させる場合に事前に呼ぶ側で機構を準備する必要があったという反省に基づいているのかもしれません。stopPropergationによるイベントループの中断とは本質的に異なります。

zf2のEventManagerが冗長に見えるか、スマートに見えるか、それは見方次第ですが、ユースケースを想像しながらインスピレーションが沸いて楽しいと私は最近思います。

2012-10-23

Zend\Mailで日本語(ISO-2022-JP)メールを送る

ZendFramework2.0でメール送信を行いました。

公式ドキュメントに加えて、下記ブログは参考になります。私はこの通りにはしませんが、ひとつの方法として参考にしました。

http://www.michaelgallego.fr/blog/?p=208

日本語メールは?

テストした環境はmb_stringが使えて、

mbstring.internal_encoding = UTF-8

mbstring.language = Japanese

な環境です。

UTF-8でメールを送る場合、ほとんど問題は起きず、そのまま何もしなくても日本語のsubjectやアドレスの注釈が使えます。ほとんど、と書いたのは、その日本語の中に、たとえば、半角カッコを含んだ場合に、メールサーバーによってはヘッダを解釈してアドレス区切りと理解し、ドメイン名を追加されたりで壊れてしまうケースがあるためです。(どちらに問題があるかについてはよく調べていません!)

JISで送る

さて、スクリプトUTF-8で書いている場合、データのやり取りのほとんどをUTF-8で統一しているという前提とします。

内輪で使う場合は、ほとんどのメールソフトはUTF-8に対応しているので問題ないですが、不特定の顧客に対して送りつけるにはJISで送りたいケースもあると思います。

この場合、Zend Framework2.0では、

$message->setEncoding('ISO-2022-JP');

としてあげれば、ドキュメント上ではできそうですが、勝手に文字コード変換は行わない(行うべきではない)ので個別にデータをJISに文字コード変換してから与える必要があり、それで十分にも見えます。

ところが、よくある文字列でも、JIS変換するとUTF-8上で処理するとかなりの数のカッコ相当の文字が入り、Zend\Mimeでは文字コードを意識しないのでエンコードしてもカッコが残ってしまいます。(内部エンコーディングを変えれば問題が解消するかということについては、どのみち筋が悪いので試していません。)カッコが残ったままsmtpトランスポートに乗せると、UTF-8では稀だったトラブルがかなりの確率で発生することになります。

そこで、対処としては、アドレスを設定する際に、mb_encode_mimeheaderでエンコードしておくこと。(本文については、mb_convert_encodingでよいでしょう。)

$message->setFrom('from@example.com', mb_encode_mimeheader("日本語も(大丈夫)あいうえお", 'JIS', 'Q'));

次に、setEncodingを省略するとUTF-8エンコードしてしまいますので、

$message->setEncoding('ASCII');

として、Zend\Mimeによるエンコードをキャンセルします。

mb_encode_mimeheaderにはmb_encode_mimeheader固有の問題がありますし、UTF-8からJISへの変換では脱落する文字もあり、変換用のライブラリを持っているケースでは、それらを通すような中間クラスを用意することになると思いますが。

たとえばHTMLメールとか

$html = $this->render('mail/receipt', $data);

のように、viewスクリプトからHTMLを育成したとします。

$txtPart = new \Zend\Mime\Part('');
$txtPart->type = "text/plain";
$htPart = new \Zend\Mime\Part(mb_convert_encoding($html, 'JIS'));
$htPart->type = "text/html";
$htPart->charset = "ISO-2022-JP";
$htPart->encoding = "7bit";
$body = new \Zend\Mime\Message();
$body->setParts(array($txtPart, $htPart));

これで、

$message->setEncoding('ASCII');

としている場合でも、JISコードで、charset指定のあるメッセージbodyを作れます。

必要に応じて、language指定なども。

2012-10-14

ZendFramework 2.0でバリデータのバリデーションメッセージをカスタマイズするメモ

  1. translatorで翻訳する
  2. translatorのテキストドメインを使う
  3. 個別のメッセージを設定する
  4. Zend\Formで使う


translatorで翻訳する。

デフォルトのバリデーションメッセージを翻訳してみます。

http://framework.zend.com/manual/2.0/en/modules/zend.validator.messages.html#using-pre-translated-validation-messagesv

ドキュメントにあるとおりですが、SkeltonApplicationと同様にtranslatorをセットアップすることで、translatorをServiceManager(ServiceLocator)から取得し、

Zend\Validator\AbstractValidator::setDefaultTranslator($translator);

とセットするコードをどこかに書くことになりそうです。

ZF2になって、Validationサービスでも立っているのかと思いましたが、このあたりは従来通りのようです。

translatorでテキストドメインを使う。

翻訳ファイルが巨大になるとメンテナンスが面倒になりますので、translatorファイルをバリデーションメッセージ用に分割してもよさそう。

translator側でテキストドメイン用の設定をするのはZend\I18nのドキュメントにあるとおりで、バリデーター側にその情報を伝えます。

validatorのオプションで'translatorTextDomain'をキーにテキストドメインを設定します。

個別のメッセージを使う。

基本的には組み込みのバリデーションメッセージを翻訳を利用しますが、お客様向けのメッセージとしてはサービス用の内容を含んでいないので不親切です。そこで、個別のメッセージを設定します。

APIドキュメントを読むと、

$validator->setMessage($message, $key);

という具合に、キーを指定することで発生したエラーの種類毎にメッセージテンプレートを設定できます。翻訳ファイルはこのキー名でもメッセージでも翻訳可能です。

InputFilterを設定から作る際、InputFilterのfactoryはこの点、完全対応しているわけではなく、オプションの'message'キーで設定するとそのバリデーターチェーンのエラーメッセージを完全に置き換えられます。これはケースバイケースで、エラーのバリエーションが1つでよいときはこれで十分でしょう。

種類が多様になる際は、setMessageメソッド経由で設定することになります。

サンプルはベタですが、こういった処理は、DIのインスタンスマネージャーを使うと管理しやすくなると思います。

Zend\Formで使う

Zend\Formの$form->isValid()で使うInputFilterも同様ですが、いくつかの方法があるため利用目的に合わせて検討する必要があります。

  • InputFilterAwareInterfaceなエンティティ(もしくはオブジェクト)をバインドする
  • Elementのデフォルトのバリデーションを有効にする
  • 個別に定義する

下記では、$form->getInputFilter()でinputFilterを取得してinputFilterにaddしています。

Zend\Formとの連携で注意すること

Zend\Formはデフォルトで、各エレメントが持っているInputSpecificationからInputFilterを構築するようになっています。HTML5の要素など利便性が高いデフォルトのフィルターとバリデーションがセットされているので、これは通常は使いたい機能です。このデフォルトのInputFilter設定は、

$form->getInputFilter()がコールされたときに、その要素のためのInputFilterにマージされます。

フォームのバリデーションではgetInputFilterがコールされるので、デフォルトにバリデーションが先頭に入ってくることになります。

この動作を停止するには、フォームクラスを書く際に、$useInputFilterDefaults = false;とすればよいのですが、これは習慣的にやらないほうがいいかなと。

さて、Zend\InputFilter\Inputが個別のエレメント用のInputFilterになりますが、この動作として、required=>true時は、isValid()コール時にバリデーションチェーンの先頭がNotEmptyバリデーターでなければ、設定なしNotEmptyを'break_chain_on_failure' => trueで追加し、required=>falseの際には値が空の場合は、validとしてバリデーションを終了します。結果的にオプション設定したNotEmptyバリデータを設定時に追加しても意味をなしません。(何を言ってるかわからないと思いますが・・・!)NotEmptyバリデータのメッセージを変更したいときは、下記のパッチを充ててInputFilterにNotEmptyバリデーターを設定するとできます。上のサンプルはこれが前提になっています。

https://github.com/noopable/zf2/commit/4e0bec36a95a334c71a564b2496eedbebc8b0c2a

※追記 あるバグが解決されて

https://github.com/zendframework/zf2/commit/4fd2d42e58245f03d1909306a8ef8858974611b1

手順として、

  1. フォームにエレメントを追加する
  2. getInputFilter()で初期化する
  3. 変更したいエレメントの先頭に設定変更したNotEmptyバリデータを入れる

これで大丈夫そうです。

素直にElementを継承して書く

上記した問題は、プリセットなエレメントをそのまま使い、設定だけで処理をしようとしたときにNotEmptyバリデータのカスタムメッセージを設定したいという限られたときにだけ発生するわけですが、ライブラリに手を入れなくても、個別のエレメントクラスを継承して書き、getInputSpecification()で、requiredにしたいエレメントについては、NotEmptyバリデータを先頭に書いておくという方法が素直かもしれません。

http://framework.zend.com/manual/2.0/en/modules/zend.form.elements.html

2012-01-13

Apache CouchDBとCouchbase Server2.0は別のプロダクトへ

先日CouchDBPHPについて下記のようなメモを残しました。

http://d.hatena.ne.jp/noopable/20111013/1318490214

Couchbaseは、CouchDBの商用ディストリというイメージを持っていたのですが、Apache CouchDBからDamien Katz氏が離脱し、今後、Couchbase ServerとApache CouchDBの互換性は担保されないという流れのようです。

CouchOneとMembaseが合併したときも驚きましたが、今回は、実質的にCouchbaseが独立ということで、もう少しインパクトが大きいです。

http://www.infoq.com/jp/news/2012/01/Katz-CouchDB-Couchbase-Server

原文は翻訳記事冒頭にあります。

Couchbaseにはdocomoも出資しており、mobile用Couchbaseもあり、モバイル、PCを問わず可搬性のいいポータブルでオフラインに強いドキュメント/アプリ環境としてブレイクする可能性もあると思いますが、うーん、どうでしょう・・・

http://www.couchbase.com/press-releases/couchbase-and-docomo-investment