Symfony2のテンプレートレンダリング時の名前の指定
Symfony2でテンプレートをレンダリングする場合、以下のようにやや長い名前でテンプレートを指定します。
$this->render('HelloBundle:Hello:index.twig');
この指定はコロンで区切られた3つのパーツで構成されていて、
- テンプレートのあるバンドルの名前
- テンプレートのあるコントローラーの名前
- テンプレートファイル名(とフォーマット、レンダラー)
のようになっています。
現段階ではこの様に3つのパーツで書かないとエラーになりますが、それはFrameworkBundle\Templating\EngineクラスのsplitTemplateName()メソッドで行われています。
<?php public function splitTemplateName($name, array $defaults = array()) { $parts = explode(':', $name); if (3 !== count($parts)) { throw new \InvalidArgumentException(sprintf('Template name "%s" is not valid.', $name)); } $options = array_replace( array( 'format' => '', ), $defaults, array( 'bundle' => str_replace('\\', '/', $parts[0]), 'controller' => $parts[1], ) ); $elements = explode('.', $parts[2]); if (3 === count($elements)) { $parts[2] = $elements[0]; $options['format'] = $elements[1]; $options['renderer'] = $elements[2]; } elseif (2 === count($elements)) { $parts[2] = $elements[0]; $options['renderer'] = $elements[1]; $format = $this->container->get('request')->getRequestFormat(); if (null !== $format && 'html' !== $format) { $options['format'] = '.'.$format; } } else { throw new \InvalidArgumentException(sprintf('Template name "%s" is not valid.', $name)); } return array($parts[2], $options); }
バンドル名やコントローラー名など、呼び出されているバンドルやコントローラーのものが自動的に入らないのかなと思ったりしていますが・・・。
コアのDIエクステンションでの設定を、自前のDIエクステンションで変更する
まず、DIエクステンションについては以下の記事を参照
このDIエクステンションですが、Symfony2コアの各バンドルごとのDIエクステンション自体を拡張するうまいやり方は今のところ分かっていないのですが、コアのDIエクステンションで行っているDIコンテナへの設定を、別のバンドルのDIエクステンションで変更する方法は分かりました。
単純なことなのですが、自分で作ったバンドルのDIエクステンションで、別のバンドルで行われている設定を変更するというだけです。
例:ビューテンプレートファイルの格納場所を変更する
ビューテンプレートのファイルは、デフォルトでは以下のようなディレクトリ・ファイルになっています。
\src\Application\HelloBundle\Resources\views\Hello\index.php
日本語で書くと、「バンドル内の、Resources\viewsディレクトリ以下のコントローラー名のディレクトリにある、アクション名.phpというファイル」が使われますが、このうち「ハンドル内の」や「Resources\views」ディレクトリは、FrameworkBundleの初期設定としてTemplatingのFilesystemLoaderに渡されています。
- このような設定はDIコンテナを通して行われる
- 設定値はDIコンテナに格納されている
- 実際にTemplatingがインスタンス化されるのは、このサービスがDI経由でアクセスされた時点なので、だいぶ後
- つまり、DIコンテナに格納された初期値群を何らかの方法で変更してやれば、Templatingの挙動などを思い通りにコントロールできます
- 通常はFrameworkExtensionが用意している設定インターフェイスで十分なんでしょうけど。
1. TestBundleに「DependencyInjection」ディレクトリを用意し、「TestExtension.php」ファイルを作る
<?php namespace Application\TestBundle\DependencyInjection; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\ContainerBuilder; class TestExtension extends Extension { public function configLoad($config, ContainerBuilder $container) { if (is_array($config['test']['templatepath'])) { $dirs = $container->getParameter('templating.loader.filesystem.path'); $dirs = array_merge($config['test']['templatepath'], $dirs); $container->setParameter('templating.loader.filesystem.path', $dirs); } } public function getXsdValidationBasePath() { return ""; } public function getNamespace() { return 'http://www.symfony-project.org/schema/dic/symfony'; } public function getAlias() { return 'test'; } }
DIコンテナの設定のうち「templating.loader.filesystem.path」に、テンプレートを探すパターン文字列のリストが設定されるので、その配列の先頭に追加しています。
また、getXsdValidationBasePath()、getNamespace()、getAlias()はExtensionインターフェイスでabstractになっているので実装しておく必要があるメソッドです。
2. このバンドルをAppKernelのregisterBundles()に追加する
<?php public function registerBundles() { $bundles = array( new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), // enable third-party bundles : // register your bundles : new Application\TestBundle\TestBundle(), );
3. config.ymlで、TestExtensionへの設定を記述する
test.config: test: templatepath: - '%kernel.root_dir%/views/%%controller%%_%%name%%%%format%%.%%renderer%%'
この設定だと、「app/views/Hello_index.php」というようなディレクトリ/ファイルが読み込めるようになります。
- %1つで囲まれているものは、DIの設定へ落とし込まれる段階で実際の値に置き換えられます。
- %2つで囲まれているものは、DIの設定へ落とし込まれる段階では%1つになり、実際に使われる段階の値が入ります。
4. アクションでのビューの指定
<?php return $this->render('HelloBundle:Hello:index.php', array('name' => $name));
アクションでビューを描画する場合、必ず「:」で区切られた3つのパートを指定する必要があります。(パートが3つじゃないとエラーになります)
- バンドル名として解釈される = %%bundle%%
- コントローラー名として解釈される = %%controller%%
- テンプレート名(+フォーマット、レンダラー)として解釈される %%name%%、%%format%%、%%renderer%%
Symfony2のKernelでregisterBundles()で登録していなくても、ルーティングに記述しているコントローラーまでは呼び出せる
routing.ymlでresource指定にて各バンドルのrouting.ymlをインポートしている場合、そこは独自に処理されてUrlMatcherとして展開されるので、コントローラーの呼び出しまでは実行されるようです。
しかし、コントローラーから通常の方法でビューをレンダリングしようとしても、例えば「Helloバンドルはないよ」というようなエラーになります。
Symfony2のDIエクステンション
(まだきちんと全容をつかめていないのですが)Symfony2のDI+バンドルのシステムには「エクステンション」という機構があって、DIコンテナから得られる設定を使ってバンドルの初期化などを行えます。
これは、各バンドル内の「DependencyInjection」というディレクトリ内に、「〜Extension.php」というファイル名で、Symfony\Bundle\FrameworkBundle\DependencyInjection\Extensionを継承したクラスを作って実装します。
FrameworkExtensionのエイリアスは「app」
エクステンションにはエイリアスをつけることができるようで、エクステンションの「getAlias()」でエイリアス名を返せば、それがエイリアス名になります。
ちなみにFrameworkExtensionはエイリアスが「app」になっていました。
そして、configファイルの「app.config」というエントリの設定で、FrameworkExtensionが初期化されます。
このように、configファイルの各エントリは、エクステンションが対応しています。
- app.config → appというエイリアスのエクステンション(= FrameworkExtension)のconfigLoad()メソッドが呼ばれる。
- twig.config → twigというエイリアスのエクステンション(= TwigExtension)のconfigLoad()メソッドが呼ばれる。
- doctrine.dbal → doctrineというエイリアスのエクステンション(= DoctrineExtension)のdbalLoad()メソッドが呼ばれる。
- doctrine.orm → doctrineというエイリアスのエクステンション(= DoctrineExtension)のormLoad()メソッドが呼ばれる。
- swiftmailer.config → swiftmailerというエイリアスのエクステンション(= SwiftmailerExtension)のconfigLoad()メソッドが呼ばれる。
Symfony2で、キャッシュディレクトリへ書き出している処理
DIコンテナを使っているのに、キャッシュディレクトリへの書き出しはそれぞれのコンポーネントが別々に直接ファイルを書き出しているのは何とかしたいなと思いつつ・・・。(Symfony Componentsのコンポーネントで、DIコンテナがなくても動作するように設計されているから、仕方ないといえば仕方ないのですが)
- Kernel.php(Symfony\Component\HttpKernel\Kernel.php)
- writeCacheFile
- Router.php(Symfony\Component\Routing\Router.php)
- writeCacheFile
- Environment.php(twig\lib\Twig\Enavironment.php)
- writeCacheFile
- Store.php(Symfony\Component\HttpKernel\Cache\Store.php)
- save
- ClassCollectionLoader.php(Symfony\Component\HttpKernel\ClassCollectionLoader.php)
- writeCacheFile
Cache\Storeのsaveだけちょっと別物なんですが、他はwriteCacheFileメソッドで、中身もほとんど一緒なんですよね。
こういう部分だけを手軽に差し替えたいorちょっと処理を追加したい(パーミッションだけ変えたいetc)という場合に、Lithiumのfilterのような仕組みが使えるといいんでしょうけど・・・。
Doctrine2とDoctrine1のUnit Of Work
Doctrine2では、エンティティの値をデータベースへ保存する際にUnit Of Workパターンというデザインパターンの実装を使っています。
(twitterで@vectorxenonさんからツッコミがありましたが)Doctrine1でもUnit Of Workという名前のクラスがあります!(私は今までDoctrine1のこのクラスを意識して読んだことがなかったようで、記憶にほとんど残っていませんでしたがw)
しかし、Doctrine1のUnit Of Workクラスには、ソースコードのコメントで以下のように記述されているとおり、本来のUnit Of Workパターンの仕組みは実装されておらず、モデルのsaveメソッドを呼ぶ度にその場でデータベースへアクセスします。
Note: This class does not have the semantics of a real "Unit of Work" in 0.10/1.0.
Database operations are not queued. All changes to objects are immediately written
to the database. You can think of it as a unit of work in auto-flush mode.
Doctrine2では、最初のリンクで示したようなUnit Of Workパターンが実装されていて、以下のような2段階の手順でDBへデータを保存します。
# EntityManagerのpersist()を呼び出すと、その時点でエンティティインスタンスのハッシュがUnit Of Workに追加される
# EntityManagerのflush()を呼び出した時点で、実際にUnit Of Workに追加されているエンティティの中でDBに反映する必要があるものを選び出し、その分だけ処理が行われます。
このような形になっているので、特に更新系の処理が効率よく行われるようです。
また、Unit Of Workが「DBに変更が必要」と判定する部分の処理もカスタマイズが可能で、デフォルトではDeferred Implicitです。
(→Doctrine2のエンティティの変更トラッキング - しんふぉにゃん)