PHP Advent Calendar 2014の11日目です。昨日は普通じゃないモッキングフレームワークAspectMockがパワフル過ぎるでした。明日は @shin1x1 さんです。
おくすりをきめキメた。
経緯
AspectMockに続いて本日はAOPです。DIもあるよ。
去年のPHP Advent CalendarではRay.DiとRay.Aopをキメました。
cf. PHP でRay.DiとRay.Aopをやってみる http://c4se.hatenablog.com/entry/2013/12/20/015945
お花屋さんでWebサイトを作ってゐるうちにDIが欲しくなりPimpleを使うてゐましたが、いつか見たannotationでしゅるっとDIするやつやりたい>ω< でもアレめんどくさい><← といふのでPimpleを拡張して自作しました。
cf. PHPでDI (依存性の注入) framworkを作るのはあなたでn人目です! http://c4se.hatenablog.com/entry/2014/10/31/190156
それを拡張し、0.2.0をリリースしました。
cf. ranyuen/di https://packagist.org/packages/ranyuen/di#0.2.0
特徴
READMEに書いてあることを、少しづつ書いてゆきます。但しREADMEと違ひ、以下のcodeはtestしてをらず目測で書いてゐます。
おおまかな特徴は以下の3つです。
- Pimple 3と100%互換です。Ranyuen\Di\ContainerはPimple\Containerを継承してゐます。Pimple 3を使うてゐれば、
use Pimple\Container;
をuse Ranyuen\Di\Contaier;
に書き換へるだけです。Pimple 2以下を使うてゐれば、Pimple 3に移行するのと同じ手続きが必要になります。また今後Pimpleが更新した場合、追隨する予定です。 - Pimpleと同様に、設定は不要です。設定を行なふ場合、YamlやXMLやPHPの設定を書くことなく無く、簡単なannotationを書くだけで依存を解決します。
- 設定すること無くAOPを行なふことができます。設定を行なふ場合、簡単なannotationを書くだけです。
Ranyuen/Diはdoctrine/annotationsに依存してゐます。phpDocumentor形式の@varアノテーションの型記載をFQNに展開する為だけに使うてをり、Ranyuen/Diのアノテーションは軽量な方法で実装してゐます。
参考にしたものは以下の通りです。
cf. Pimple http://pimple.sensiolabs.org/
cf. Ray.Di & Ray.Aop https://code.google.com/p/rayphp/
cf. PHP-DI http://php-di.org/
cf. Go! AOP http://go.aopphp.com/
cf. Laravel http://laravel.com/
install方法
Composerを使ひます。
composer require ranyuen/di *0.2
PHPの5.4以降 (5.4, 5.5, 5.6) と最新のHHVM (3.4.0) をサポートします。
GPL v3 或いはそれ以降でライセンスします。
DIの使用例
DI (Dependency Injection) やAOP (Aspect Oriented Programming) を何故使ふのかは他の文書に譲ります。このやうなことができます。
Pimpleと互換ですので、同じ使ひ方ができます。
<?php require_once 'vendor/autoload.php'; use Ranyuen\Di\Container; class Momonga { public $id; public function __construct($id = '') { $this->id = $id; } } $c = new Container; // コンテナに値を入れ、取り出せます。 $c['id'] = 'Some ID'; var_dump('Some ID' === $c['id']); // コンテナから取り出されるときに、コンテナの別の値を使へます。これでひとつひとつ依存を解決します。 // 同じ名前でコンテナから取り出した値は、デフォルトでは毎回同一のobjectです。 $c['momonga'] = function ($c) { return new Momonga($c['id']); }; var_dump($c['momonga'] instanceof Momonga); var_dump($c['id'] === $c['momonga']->id); var_dump($c['momonga'] === $c['momonga']); // 取り出す度に新たにobjectを作ることができます。 $c['factory'] = $c->factory(function ($c) { return new Momonga; }); var_dump($c['factory'] instanceof Momonga); var_dump($c['factory'] !== $c['factory']); // Closureをコンテナに格納するときに、二重のClosureにせず簡単に書けます。 $c['protect'] = $c->protect(function () { return new Momonga; }); var_dump($c['protect']() instanceof Momonga); // objectを生成し依存を注入するClosure自体を参照できます。 $c['raw'] = function ($c) { return new Momonga($c['id']); }; $raw = $c->raw('raw'); var_dump($raw($c) instanceof Momonga); // 一度格納した注入Closureを、あとから拡張できます。 $c['base'] = function ($c) { return new Momonga; }; $c->extend('base', function ($base, $c) { $base->id = $c['id']; return $base; }); var_dump($c['base'] instanceof Momonga); var_dump($c['id'] === $c['base']->id); // 同じ注入を多くのアプリケーションで行なふ場合、注入をclassに切り出せます。 class MomongaProvider implements Ranyuen\Di\ServiceProviderInterface { public function register(Container $c) { $c['register'] = function ($c) { return new Momonga; }; } } $c->register(new MomongaProvider); var_dump($c['register'] instanceof Momonga);
以上はPimpleの機能です。以下ではrequireとuseを省略します。
型 (interface名、FQN) を指定してコンテナに値を格納し取り出せます。この機能は主にRanyuen/Diの内部で使うてゐます。
<?php namespace Momonga; class Momonga { } $c = new Container; $c->bind('Momonga\Momonga', 'momonga', function ($c) { return new Momonga; }); var_dump($c->getByType('Momonga\Momonga') instanceof Momonga);
ここからがRanyuen/Diの機能です。constructorの引数に自動で注入できます。type hintか引数の名前から依存を解決します。type hintから解決する場合、PimpleのArrayAccess形式ではなく、bindメソッドを使ふ必要があります (名前から解決する場合はArrayAccess形式で充分です。以下同じ)。
<?php class Momonga { } class Yuraru { public function __construct($id, $momonga, Momonga $m, $another = null) { var_dump('Some ID' === $id); var_dump($momonga instanceof Momonga); var_dump($m instanceof Momonga); } } $c = new Container; $c['id'] = 'Some ID'; $c->bind('Momonga', 'momonga', function ($c) { return new Momonga; }); $yuraru = $c->newInstance('Yuraru'); // 注入しない引数は自由に指定できます。 $yuraru = $c->newInstance('Yuraru', ['Another']); $yuraru = $c->newInstance('Yuraru', [ 'another' => 'Another', 'id' => 'Another ID', ]);
@Inject
アノテーションを使うて、propertyに注入することができます。constructorではphpDocumentor形式の@paramは無視しtype hintだけを参照しますが、propertyでは@var
アノテーションをtype hintの代はりに使ひます。
<?php class Momonga { } class Yuraru { /** @Inject */ private $momonga; /** * @var Momonga * @Inject */ private $m; public function ok() { var_dump($this->momonga instanceof Momonga); var_dump($this->m instanceof Momonga); } } $c = new Container; $c->bind('Momonga', 'momonga', function ($c) { return new Momonga; }); $yuraru = $c->newInstance('Yuraru'); $yuraru = new Yuraru; $c->inject($yuraru);
@Injectアノテーションに名前を書き、コンテナに登録したのとは別の名前で取り出すこともできます。同じ機能である@Namedアノテーションは説明を省略します。
<?php class Yuraru { /** @Inject('m=momonga') */ private $m; /** * @Inject('m=momonga,momo=momonga') */ public function __construct($m, $momo) { var_dump('Some mOmonga' === $m); var_dump('Some mOmonga' === $momo); } public function ok() { var_dump('Some mOmonga' === $this->m); } } $c = new Container(['momonga' => 'Some mOmonga']); $yuraru = $c->newInstance('Yuraru');
値を取り出す部分でのキャッシュはPimpleの機能で行なってゐます。newInstanceメソッドとinjectメソッドによるDIは、依存グラフをキャッシュします。キャッシュは次versionで更に改善します。
これらのRanyuen/Diの機能はPimpleの機能と同時に使へます。factoryの例を示します。
<?php class Momonga { } class Yuraru { /** @Inject */ public $momonga; /** * @var Momonga * @Inject */ public $factory; } $c = new Container; $c['momonga'] = function ($c) { return new Momonga; }; $c->bind('Momonga', 'factory', $c->factory(function ($c) { return new Momonga; })); $yuraru1 = $c->newInstance('Yuraru'); $yuraru2 = $c->newInstance('Yuraru'); var_dump($yuraru1->momonga === $yuraru2->momonga); var_dump($yuraru1->factory !== $yuraru2->factory);
次の機能をFacadeと呼ぶのは、名称が広範囲すぎる気がしますが、Laravelに倣つてFacadeと呼んでおきます。コンテナから値を取り出すとき、コンテナ自体を手元に持ってゐなくても、staticアクセスの記法でメソッドを呼び出せます。
<?php class Momonga { public function m() { return 42; } } $c = new Container; $c['momonga'] = function ($c) { return new Momonga; }; $c->facade('Gardea', 'momonga'); var_dump(42 === Gardea::m());
AOPの使用例
継承できる全てのメソッドへインターセプトできます。インターセプト対象のメソッドは、メソッド名かメソッド名の正規表現で指定できます。
<?php class Momonga { public function eat($a = null, $b = null) { echo "eat\n"; } public function act() { echo "act\n"; } } $c = new Container; $c->wrap('Momonga', ['eat', 'act'], functon ($ivk, $args, $me) { list($a, $b) = $args; echo "Before eat or act\n"; $result = $ivk($a, $b); echo "After eat or act\n"; }); $c->wrap('Momonga', ['/t$/'], functon ($ivk, $args, $me) { list($a, $b) = $args; echo "Before end with t\n"; $result = $ivk($a, $b); echo "After end with t\n"; }); $momonga = $c->newInstance('Momonga'); $momonga->eat(); $momonga->act();
内部では、ターゲットのclassを継承したクラスを生成します。生成されたclassへは、コンテナを通してしかアクセスできません。
staticメソッドも継承されるので、インターセプトできます。RubyやJavaScriptから来ると誤解しますが、PHPのstatic修飾は「静的」であって、クラスメソッドではありません。PHPにクラスメソッドやクラスプロパティは無く、静的メソッドと静的プロパティです。
<?php class Momonga { public static function eat() { } } $c = new Container; $c->wrap('Momonga', ['eat'], function ($ivk, $args, $me) { return $ivk(); }); $momonga = $c->newInstance('Momonga'); $momonga::eat();
@Wrap
アノテーションを書き自動でインターセプトできます。
<?php class Momonga { /** @Wrap('fruit,strange') */ public function eat() { } } $c = new Container; $c['strange'] = $c->propect(function ($ivk, $args, $me) { echo 'Strange '; return $ivk(); }); $c['fruit'] = $c->propect(function ($ivk, $args, $me) { echo "fruit\n"; return $ivk(); }); $c->newInstance('Momonga')->eat();
開発方法
CONTRIBUTINGをご覧ください。
些事ですがlocalでは更に,/test.sh
に以下のファイルを置いて実行してゐます。
#!/bin/bash set -e phpenv local 5.6.1 phpunit phpenv local system phpunit phpenv local 5.6.1 hhvm vendor/bin/phpunit phpenv local 5.6.1
以上です。次のリリースは0.2.1になります。