[Symfony2]PEAR::Net_UserAgent_MobileをDIコンテナから呼び出す

Written by uechoco 12月 09
この記事を読む時間:1336くらい

この記事は、Symfony アドベントカレンダー 2010 に参加しています。

Symfony2も先日PR4バージョンがリリースされて、ベータ版のリリースも近くなってきているようです。Symfony2はphp 5.3.2以上を必要とする新しいフレームワークに生まれ変わりますが、まだまだphp4時代の遺産にはお世話になるんではないかと思っています。特にお世話になるのはPEARですよね。今回は、Symfony2でのPEARライブラリを使用してみる例として、日本人phperの誰もがお世話になってる(言い過ぎか?)PEAR::Net_UserAgent_MobileをSymfony2で扱う方法を模索してみます。「模索」と言っているのは、私自身がSymfony2歴が非常に浅いという意味と、エラーをどのように回避していったかの軌跡をしっかりとお見せしようと思っているためです。対象バージョンはgithubの最新ではなく、Symfony2 PR4のsandboxです。

それでは進めていきましょう。

PEARライブラリの配置

まずはPEAR::Net_UserAgent_MobileとPEAR.phpをダウンロードし、以下のように/src/vendorにpearディレクトリを作成して中に配置します。

pearディレクトリの配置場所

pearディレクトリの中身の様子

autoload.phpの編集

PEARライブラリに対してパスを通すため、/src/autoload.phpを編集します。

// /src/autoload.php の下部
[phpcode]
$loader->registerPrefixes(array(
‘Swift_’ => $vendorDir.’/swiftmailer/lib/classes’,
‘Twig_’ => $vendorDir.’/twig/lib’,
‘Net_’ => $vendorDir.’/pear’, // added prefix for PEAR::Net_XXX libraries
));
$loader->register();

// hack for some PEAR libraries
set_include_path(get_include_path().PATH_SEPARATOR.$vendorDir.’/pear’);
[/phpcode]

上のコードでは、「Net_」という文字から始まるクラス名の場合の起点ディレクトリをregisterPrefixes()メソッドで指定しています。PEARライブラリには「PEARである」という接頭辞がないため、ちょっと気持ちが悪い指定の仕方になっています(本当は接頭辞をNet_と指定したらNetディレクトリを起点に登録するか、PEARまるごと起点ディレクトリを登録できると嬉しい)。

また、多くのPEARライブラリは、PEARの起点ディレクトリからのパスを用いてrequire_onceすることが多いため、起点ディレクトリをinclude pathに登録する必要があります(「require_once ‘Net/UserAgent/Mobile/Error.php’;」といったインクルードに対応するためです)。

実はこれだけでもうクラスパスが通ったので使えるようになりました。おめでとう!これにて一件落着!
…というところで終わってしまったらこの記事の価値は全くありません(笑 Symfony2らしく、DIコンテナからNet_UserAgent_Mobileクラスのインスタンスを取り出せるようにHelloBundleを拡張してみます。(ここからちょっとハードルが上がります)

HelloExtensionの作成

Symfony2のDI機構には、Extensionと呼ばれる仕組みがあり、バンドル毎にDIコンテナに対する初期設定などを行うことができます。HelloBundleであれば、以下のようにHelloBundleディレクトリ直下にDependencyInjectionディレクトリを作成しHelloExtension.phpを配置すると、設定ファイルに記載があれば自動的に読み込まれるようになっています。

// /src/Application/HelloBundle/DependencyInjection/HelloExtension.php
[phpcode]
setFactoryMethod(‘factory’);
$container->setDefinition(‘net.useragent.mobile’, $def);
}

/**
* Returns the base path for the XSD files.
*
* @return string The XSD base path
*/
public function getXsdValidationBasePath()
{
return __DIR__.’/../Resources/config/schema’;
}

public function getNamespace()
{
return ‘http://labs.uechoco.com/schema/dic/hello’;
}

public function getAlias()
{
return ‘hello’;
}
}

[/phpcode]

このコードが何をしているのかを一言で言えば、DIコンテナがNet_UserAgent_Mobileを勝手に管理してくれるように登録しています。Net_UserAgent_Mobileクラスのサービスを定義し、ファクトリメソッドとして、factoryを設定し、net.useragent.mobileという識別子で登録しています。

また、getAlias()メソッドでHelloExtensionのエイリアスとしてhelloを指定していますが、これは次のconfig設定で使用されます。

config.ymlの修正

さきほど作成したHelloExtensionが自動的に読み込まれるには、/app/config/config.ymlにHelloExtensionの設定を記述する必要があります。

// /app/config.config.yml に追記

  1. # HelloBundle Configuration
  2. hello.config: ~

最後のチルダも忘れずに。

設定ファイルの読み込みの仕組みをここで詳しく説明はしませんが、例えばhello.configという記述があると、「hello」エイリアスのExtensionの、「config」Load()メソッドが自動的に呼び出される仕組みにあっています。これで先程のサービス定義のconfigLoad()メソッドが呼ばれるように成ります。

HelloContorllerから使ってみる

HelloControllerのindexActionを以下のように修正して、Net_UserAgent_Mobileクラスでモバイルかどうかを判定した真偽値をテンプレートに渡してあげます。

// /src/Application/HelloBundle/Controller/HelloController.php
[phpcode]
public function indexAction($name)
{
$agent = $this->container->get(‘net.useragent.mobile’);

return $this->render(‘HelloBundle:Hello:index.twig’, array(‘name’ => $name, ‘non_mobile’ => $agent->isNonMobile()));

// render a PHP template instead
// return $this->render(‘HelloBundle:Hello:index.php’, array(‘name’ => $name));
}
[/phpcode]

そして、twigテンプレートにも少し修正を加えます。

// /src/Application/HelloBundle/Resources/views/Hello/index.twig

  1. {% extends "HelloBundle::layout.twig" %}
  2.  
  3. {% block content %}
  4.     {% if non_mobile %}
  5.     <strong>non mobile!</strong>
  6.     {% else %}
  7.     <strong>mobile!</strong>
  8.     {% endif %}
  9.  
  10.     Hello {{ name }}!
  11. {% endblock %}

Webからアクセスしてみる

これで本来なら動くはずです。Webからアクセスしてみましょう。私の場合は以下のようなURLです。

http://sandbox-pr4.localweb02/app_dev.php/hello/uechoco
※uechocoの部分は、好きな英数字に置き換えても構いません。

すると、以下のようなエラーが発生して動きません。

「call_user_func() expects parameter 1 to be a valid callback, non-static method Net_UserAgent_Mobile::factory() should not be called statically」

原因を探る

困りました。何がいけなかったのか、エラーが発生したDIコンテナの該当部分を見てみます。

// /app/cache/dev/appDevDebugProjectContainer.php
[phpcode]
protected function getNet_Useragent_MobileService()
{
return $this->services[‘net.useragent.mobile’] = call_user_func(array(‘Net_UserAgent_Mobile’, ‘factory’));
}
[/phpcode]

どうやらstatic宣言していないメソッドを静的に呼ぼうとしているのがいけないようです。php4自体のコードを引き継いでいるのでしょうがないですよね。static宣言をしていないメソッドを静的に呼び出すための一番容易な解決策は、エラー制御演算子付きで呼び出すことです。call_user_func()の前に@マークが付けられれば問題ないのですが、このコードはSymfony2の内部で自動的に生成されるコードなのですが、エラー制御演算子をつけるオプションはないようです。

※今回はエラー制御演算子の使用の是非の議論はしません。単純かつ分かりやすい解決策として採用しています。

Net_UserAgent_Mobile::factory()メソッドを呼び出すラッパークラスを作成

幸いにも、Symfony2のサービス定義には、別のサービスのメソッドを用いてインスタンスを生成するような指定方法があります。つまり、ラッパークラスを作ってエラー制御演算子付きでfactory()メソッドを呼び出すようなメソッドを持たせて、そのラッパークラス経由でNet_UserAgent_Mobileのインスタンスを生成するようにしてやります。言葉だと分かりにくいと思うので、ソースコードで語りますw

まずはラッパークラスを作成します。Utilディレクトリはつくってください。
// /src/Application/HelloBundle/Util/PEARClassLoader.php
[phpcode]
HelloExtensionの修正

続いて、DIコンテナがラッパークラスからインスタンスを生成するようにHelloExtensionのサービス定義を修正します。

[phpcode]

/**
* Loads the Hello Bundle configuration.
*
* @param array $config An array of configuration settings
* @param ContainerBuilder $container A ContainerBuilder instance
*/
public function configLoad($config, ContainerBuilder $container)
{
$def = new Definition(‘Application\\HelloBundle\\Util\\PEARClassLoader’);
$def->setFile(__DIR__.’/../Util/PEARClassLoader.php’);
$container->setDefinition(‘pear.classloader’, $def);

$def = new Definition(‘Net_UserAgent_Mobile’);
$def->setFactoryService(‘pear.classloader’);
$def->setFactoryMethod(‘factoryNet_UserAgent_Mobile’);
$container->setDefinition(‘net.useragent.mobile’, $def);
}
[/phpcode]

新たにラッパークラスのサービス定義を追加しています。またNet_UserAgent_Mobileクラスのサービス定義は、ラッパークラスのfactoryNet_UserAgent_Mobile()メソッドからインスタンスを作成するように変更しています。

ちなみに、どうやってこれらのメソッドを見つけ出したかというと、DIコンテナのphpファイルを吐き出すPhpDumperクラスとDefinitionクラスと交互に見て、どのように定義すればどのようにDIコンテナが吐き出されるのかを追って見つけ出しました。

再びWebからアクセスしてみる

これで動くはずです。Webからアクセスしてみましょう。私の場合は以下のようなURLです。

http://sandbox-pr4.localweb02/app_dev.php/hello/uechoco
※uechocoの部分は、好きな英数字に置き換えても構いません。

PCから見ると「non mobile」と表示されます

FirefoxでFireMobileSimulatorでP903iをシミュレートすると「mobie」と表示されます

おまけ:自動生成されたDIコンテナのサービス定義を見る

一応インスタンス生成部分のコードを再確認しておきましょう。

// /app/cache/dev/appDevDebugProjectContainer.php
[phpcode]
/**
* Gets the ‘pear.classloader’ service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return Application\HelloBundle\Util\PEARClassLoader A Application\HelloBundle\Util\PEARClassLoader instance.
*/
protected function getPear_ClassloaderService()
{
require_once ‘/mnt/hgfs/localweb02/sandbox-pr4/src/Application/HelloBundle/DependencyInjection/../Util/PEARClassLoader.php’;

return $this->services[‘pear.classloader’] = new \Application\HelloBundle\Util\PEARClassLoader();
}

/**
* Gets the ‘net.useragent.mobile’ service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return Net_UserAgent_Mobile A Net_UserAgent_Mobile instance.
*/
protected function getNet_Useragent_MobileService()
{
return $this->services[‘net.useragent.mobile’] = $this->get(‘pear.classloader’)->factoryNet_UserAgent_Mobile();
}
[/phpcode]

ラッパークラスのインスタンス生成部分と、そのインスタンスを用いてfactoryNet_UserAgent_Mobile()メソッドを呼び出す部分がちゃんと動いていそうです。

まとめ

こんな感じでSymfony2でもPEARライブラリを使うことができそうです。使用するPEARライブラリが増えても、ある程度まではPEARClassLoaderのメソッドを増やして対応していくことで対応できそうです。

今回は既存のBundleに組み込む形で対応しました。PEARライブラリごとにBundleを作れば楽なんじゃないかという思いもあったのですが、それだとset_include_path()に登録するディレクトリが増えてしまいますので、PEARBundleとかでまとめてしまったほうが賢いかも知れません。

また繰り返しになりますが、私自身のSymfony2歴が浅いので、こんな方法もあるんだよというくらいで捉えてもらえると助かります。

次のアドベントカレンダーの記事は再びchobi_eさんの予定です。よろしくお願いします!

Symfony Advent 2010であなたの記事を公開してみませんか?

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyでイイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にし て公開していくイベントです。参加についてはATNDで参加表明の上、Google GroupのSymfony Advent 2010に追加リクエストを送信ください。Symfony Advent 2010チーム一同、あなたの参加をお待ちしております。

日本Symfonyユーザー会
Symfonyアドベントカレンダー 2010

※Syfony Advent 2010はsymfony好きな有志で集まったチームです。

No Responses to “[Symfony2]PEAR::Net_UserAgent_MobileをDIコンテナから呼び出す”

No comments yet.

Comments RSS rss うえちょこ@ぼろぐ TrackBack Identifier URI rss うえちょこ@ぼろぐ

Leave a comment