Hatena::ブログ(Diary)

モバイル系ニートエンジニア`chobi_e`の日記 Twitter

2010-11-06 Symfony2を理解するためにBundleを書く

Symfony2を理解するためにBundleを書く


第11回 Symfonyユーザー会IRC集会を開催しましたで本エントリをご紹介いただきました。


日本Symfonyユーザー会では隔週でircでSymfony関連で談話したりしているのでSymfony2にトライしている方は是非参加してみてくださいネ(私も時間つくれれば参加しています)



さて、前回シンプルすぎるアプリケーションを書いたので、PDOと連携させてうごかすサンプルを書いてみましょう。


Symfony2にはDoctrine2というORM/DBALのBundleが既にあるので車輪の再発明となってしまいますが、LAMP構成で身近なPDOを題材として選ぶことで今までのライブラリも簡単にBundle化できる理解力が得られるのではないかな、と思います。


それでは早速トライしてみましょう。


動作の確認の為に次のようなMysqlのデータベースを作成します。


create database uhi default character set utf8;
use uhi;
create table moe(id int unsigned auto_increment, name varchar(20), primary key(id));
insert into moe set name = "chobi_e";

まずは出来上がりのイメージを大切にするために、シンプルなPHPコードを書いてみます。出力結果は前回と同じHello chobi_eで行きましょうか。


<?php
$pdo = new \PDO("mysql:dbname=moe;host=localhost;charset=utf8","root","");
$pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
$result = $pdo->query("select id,name from uhi limit 1");

list($id,$name) = $result->fetch(\PDO::FETCH_NUM);

echo "Hello $name";

PDOをBundle化した場合は取りあえず2行目、3行目まではDIコンテナに作成を任せたいと思います。では、PDOは何に依存しているのかをきちんと書き出してみましょう。


パラメーター
  ・ドライバ名(driver)
  ・データベース名(database)
  ・ホスト名(host)
  ・キャラセット(charset)
  ・ユーザーアカウント(user)
  ・パスワード(password)
クラス
  ・\PDO
作成後の処理
  ・->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION)

こんな感じでしょうか。実際に先程の1ー2行目のコードを言葉で言い直しただけですね。それでは設定ファイルの叩き台を書いてみましょう。

設定の叩き台

pdo.config
  dirver: mysql
  database: moe
  host: localhost
  charset: utf8
  user: root
  password: ~

では、これを実際にPHPの設定ファイルとして落とし込みましょう(app/config.phpに追記します)

$pdo_config = array();
$pdo_config['driver'] = "mysql";
$pdo_config['database'] = "uhi";
$pdo_config['host'] = "localhost";
$pdo_config['charset'] = "utf8";
$pdo_config['user'] = "root";
$pdo_config['password'] = "";

$container->loadFromExtension('pdo','config',$pdo_config);

前回学んだ通り設定ファイルは[ExtensionのAlias].[Loaderメソッド]という形でしたね。今回はよく使われるconfigLoaderメソッドを上書きして作ろうと思うのでこんな感じにしてみました。


ではPDOBundleの実ディレクトリなどを掘って作成の続きをしましょう。

cd ~/servers/symfony2.chobie.air
mkdir -p src/Application/PDOBundle/{DependencyInjection,Resources/config/schema}
cat > src/Application/PDOBundle/PDOBundle.php <<EOF
<?php
namespace Application\PDOBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class PDOBundle extends Bundle
{
}
EOF

これでPDOBundleに最低限必要なディレクトリとBundleを書きました。次にPDOBundle\DependencyInjection\PDOExtension.phpを作成しましょう。

PDOBundle\DependencyInjection\PDOExtension.php

<?php
namespace Application\PDOBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;


class PDOExtension extends Extension
{
  public function configLoad($config, ContainerBuilder $container)
  {
    var_dump($config);
  }

  public function getNameSpace()
  {
    return 'http://mandarine.co/schema/dic/pdo';
  }
  
  public function getXsdValidationBasePath()
  {
    return __DIR__.'/../Resources/config/schema';
  }

  public function getAlias()
  {
    return 'pdo';
  }
}

ExtensionはSymfony\Component\DependencyInjection\Extension\Extensionを継承したクラスで最低限getNameSpace,getXsdValidationBasePath,getAliasの3メソッドを定義する必要があります。

getAliasはExtensionの識別に必要な別名なので設定ファイルに書いたExtension名と一致させる必要があります。今回はpdoとしました。


getXsdValidationBasePathは特に説明しませんがこういうもんだ、と思っておいてください。


getNameSpaceはXMLそんな詳しくないんでよく理解していませんがアプリケーション作成者が判別しやすいドメイン/schemea/dic/Extension名と書いておけばいいんじゃないでしょうか。


今後ExtensionのAliasは重要になってくるので、必ず一貫した書き方にしましょうね。

例えば

・NG例
Namespace: http://mandarine.co/schema/dic/oreore-pdo
Alias: pdo

・OK例
Namespace: http://mandarine.co/schema/dic/pdo
Alias: pdo

ということですね。


とりあえずこれで最低限PDOBundleを実行する仕組みが出来たと思うのでKernelに登録して実行してみます。



f:id:chobi_e:20101107000946p:image


ちゃんと設定ファイルが読まれてApplication\PDOBundle\DependencyInjection\PDOExtension::loadConfigメソッドが実行されて設定の配列が渡ってきましたね!


Symfony2のDIコンテナは一度必要な設定やクラスをまとめたモノをコンパイルして実際に利用する仕組みとなっています。ここにPDOの初期化と後処理を追加し、DIコンテナから参照できる名前をつければオッケーということですね。


仕組みとしてはXMLで定義させてしまうこともできるのですが、最初に応用編をやってしまうとイミフになってしまうのでまずはPHPで実際に書いてみましょう。DIコンテナに新しいサービスを追加するにはSymfony\Component\DependencyInjection\Definitionを使い、その他のDIコンテナの設定の参照の為に

Symfony\Component\DependencyInjection\Referenceを使います。


PDOBundle\DependencyInjection\PDOExtension.php

<?php
  public function configLoad($config, ContainerBuilder $container)
  {
    $def = new Definition("PDO");
    $def->setArguments(array(
      sprintf("%s:dbname=%s;host=%s;charset=%s",
        $config['driver'],
        $config['database'],
        $config['host'],
        $config['charset']),
      $config['user'],
      $config['password']
      ));
    $container->setDefinition('pdo.connection', $def);
  }
//...

今回はReferenceを使いませんでしたが、最終的に定義されたDIコンテナのパラメーターを使いたいという場合はReferenceを使う必要があります。


それでは前回作成したHelloControllerとViewも修正しましょう。


SampleBundle\Controller\HelloController.php

<?php
  public function indexAction()
  {
    $result = $this->container->get("pdo.connection")
      ->query("select id, name from moe limit 1");

    return $this->render("Sample:Hello:index.php",$result->fetch(\PDO::FETCH_ASSOC));
  }
//...

SampleBundle\Resources\views\Hello\index.php

<?php
echo "Hello {$name}";

ブラウザでhttp://symfony2.chobie.air/(ローカルの開発環境)を開いてみましょう。


f:id:chobi_e:20101106031210p:image


決して、画像つくるのが面倒くさくて同じ出力にしてるわけじゃないですからね!w


因みに$this->container->get("pdo.connection")は$this->container->getPdo_Connectionもしくは$this['pdo.connection']で参照することもできます。詳細はContainerBuilderによって作成されたDIコンテナクラスを見てもらえば分かります。


自動的にDefinitionを登録する際に指定した名前でメソッドが定義される(今回はgetPdo_ConnectionService)んですがちょっとイケてなかったりします・・・。因みにFabienさん曰くArrayAccessは利便性の為に使いたいと言っていたと思うので今のところは鉄板な$this->container->get("pdo.connection")もしくは$this['pdo.connection']を使うのが吉でしょう。


でこっから先はより深く知りたい人向け。


探求者の為のBundle&Extension


Symfony\Component\DependencyInjection\DefinitionでDIにクラスを追加できるのは分かりましたが、同梱されている他のBundleのようにExtension作成に関する設定をXMLで定義しておきたいですね!

cat > src/Application/PDOBundle/Resources/config/pdo.xml <<EOF
<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="pdo.class">PDO</parameter>
        <parameter key="pdo.driver">mysql</parameter>
        <parameter key="pdo.database">null</parameter>
        <parameter key="pdo.host">localhost</parameter>
        <parameter key="pdo.user">root</parameter>
        <parameter key="pdo.password">null</parameter>
        <parameter key="pdo.charset">UTF-8</parameter>
    </parameters>

    <services>
      <service id="pdo.connection" class="%pdo.class%">
        <argument>%pdo.driver%:dbname=%pdo.database%;host=%pdo.host%;charset=%pdo.charset%</argument>
        <argument>%pdo.user%</argument>
        <argument>%pdo.password%</argument>
        <call method="setAttribute">
          <argument type="constant">\PDO::ATTR_ERRMODE</argument>
          <argument type="constant">\PDO::ERRMODE_EXCEPTION</argument>
        </call>
      </service>
    </services>
</container>
EOF

PDOBundle\DependencyInjection\PDOExtension.php

<?php
  public function configLoad($config, ContainerBuilder $container)
  {
    $loader = new XmlFileLoader($container, __DIR__.'/../Resources/config');
    $loader->load("pdo.xml");

    foreach($config as $key => $value){
      $container->setParameter('pdo.'.$key , $value);
    }
  }
//...

DIコンテナ登録に使う為のXMLの書き方についてはhttp://www.symfony-project.org/schema/dic/services/services-1.0.xsdに詳しく乗っているので眺めていればかけるようになるかと思います。


実際に扱うExtensionだとXMLによる定義では柔軟に書けないので設定に必要なパラメーターのデフォルトのみを定義しておいてDIコンテナへの登録などは自前でDefinitionを使ってやるほうが利便性が高いようです。(例えばPDOだと複数コネクションを登録する場合はDefinitionを使わないとちょっと厳しいとか)


で、せっかくここまで作ったんでKernelのXMLファイルでの設定に使うXSDも書いてしまいましょう。現時点(2010/11/6)ではnamespaceでのExtension名の解決が行えないバグ?があるので実際には動作しません(どうやら自分のExtensionの書き方が悪かっただけのようです....)がユーザーが好きな設定を選べるSymfony2なのでBundle作成者としてはきちんと用意しておきたいところです。


cat > src/Application/PDOBundle/Resources/config/schema/pdo-1.0.xsd
<?xml version="1.0" encoding="UTF-8" ?>

<xsd:schema xmlns="http://mandarine.co/schema/dic/pdo"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://mandarine.co/schema/dic/pdo"
    elementFormDefault="qualified">

    <xsd:element name="config" type="config" />
    <xsd:complexType name="config">
        <xsd:attribute name="driver" type="xsd:string" />
        <xsd:attribute name="host" type="xsd:string" />
        <xsd:attribute name="database" type="xsd:string" />
        <xsd:attribute name="user" type="xsd:string" />
        <xsd:attribute name="password" type="xsd:string" />
        <xsd:attribute name="charset" type="xsd:string" />
    </xsd:complexType>
</xsd:schema>

これでKernelの設定にXMLを使った場合でもきちんとValidationが行なえますね(今後)


あとがき


Symfony2はだいぶ広く、理解が浅い部分もかなりあって説明や認識を間違えている部分も多々あるかと思いますがSymfony2独学者の為の何かしらの足しになればいいかなーというエントリーでした。


複数設定が扱えるデメリット?


XSDとXMLの問題でもうひとつ。(そんなにXML詳しくないのでアレなのかもしれませんが)


XMLでは複数シーケンスがあるパターンをXMLParserで処理した場合

<some>
  <sequence>hello</sequence>
  <sequence>hello</sequence>
  <sequence>hello</sequence>
</some>
some:
  sequence:
    - hello
    - hello
    - hello

となりますが

<some>
  <sequence>hello</sequence>
</some>

だと

some:
  sequence: hello

になるみたいです。YAMLやPHPだとそもそも複数ある場所は必ず配列の書き方で定義されるんですがXMLだとコンパクトになっちゃうんでハマらないExtensionの書き方がどうやら必要っぽいと。


ふー、複数設定があつかえるっつーのも大変ですねー。


  • ちゃんと内容確認して動くようにコード等を修正しました(11/7)