読者です 読者をやめる 読者になる 読者になる

Pimple 2.0がリリースされたのでPimpleについて復習してみる

php

ぶっちゃけ出るなんて思ってませんでしたが、Pimple 2.0がリリースされたので、1.xの復習と2.0での変更について調べてみます。

Pimple - A simple PHP Dependency Injection Container

Pimple(1.x)の基本

DIコンテナとは何か、という説明をはぶいてPimpleの動作を単に説明すれば、

  1. コンテナ(Pimpleのインスタンス)は連想配列のように見えて、そこに値や無名関数をセットできる
  2. 値を取り出す時、セットされたものが値であれば、単にその値が返却される
  3. 値を取り出す時、セットされたものが無名関数であれば、その無名関数を呼び出した戻り値が返却される。この時、無名関数の第一引数にはコンテナそのものが渡される

というこれだけのものです。

Inversion of Control コンテナと Dependency Injection パターンの例でいえば

<?php
$c = new \Pimple();
$c['filename'] = 'movies1.txt';
$c['movie_finder'] = function($c) {
    return new ColonMovieFinder($c['filename']);
};
$c['movie_lister'] = function($c) {
    return new MovieLister($c['movie_finder']);
};

こんな感じで依存関係を定義します。どこからみてもPHPのコードに見えますがこれは設定ファイルみたいなものだと考えてください。ここまでの段階ではColonMovieFinderのインスタンスもMovieLiserのインスタンスも作成されていません。

使う側は、

<?php
$lister = $c['movie_lister'];

としてやれば、無名関数が2つ呼ばれてガラガラポンとMovieListerのインスタンスを取得できるという寸法です。

shareメソッド(1.x)

上の例では、$c['movie_lister']が参照されるたびに無名関数が呼ばれ、新しいMovieListerのインスタンスが返却されていました。そうではなくSingleton風に毎回同じインスタンスを返却してほしい場合は、

$c['movie_lister'] = $c->share(function($c) {
    return new MovieLister($c['movie_finder']);
});

と書く必要があります。

extendメソッド(1.x)

サービスを定義したところを直接変更できない。でもサービスに何かしら手を加えたい。でもまだインスタンスは作成したくない、みたいなときにextendメソッドを使います。実際どういうことなのか最初はピンとこなかったのですが、GitlistというGitリポジトリビュワーにfabpot(Pimple、Silexの作者)が投げたPullRequestをみてなるほどなと思いました。

https://github.com/klaussilveira/gitlist/commit/65e0bd402b3ca0ac47e361a87d5d86327960a1a9#diff-828e0013b8f3bc1bb22b4f57172b019d

元のコードは、

 // Register Git and Twig service providersclass_path
 $app->register(new Silex\Provider\TwigServiceProvider(), array(
     'twig.path'       => __DIR__.'/views',
     'twig.options'    => array('cache' => __DIR__.'/cache'),
 ));

この辺はSilexの決まり事なのですが、ここで$app['twig']を参照するとTwig_Environmentインスタンスを作成して色々設定してくれることになってます。で、このインスタンスに対して機能を追加したいので 、

// Add the md5() function to Twig scope
$app['twig']->addFilter('md5', new Twig_Filter_Function('md5'));

と書いていたのですが、これだと使うかどうかにかかわらずTwig_Environmentインスタンスが作成されてしまいます。もしTwig_Environmentインスタンスを作成する部分を自分で書いていたのであれば、

$app['twig'] = $app->share(function () {
    $twig = new Twig_Environment();
    $twig->addFilter('md5', new Twig_Filter_Function('md5'));
    return $twig;
});

と書けていたわけです。PRではこれを、

$app['twig'] = $app->share($app->extend('twig', function($twig, $app) {
    // Add the md5() function to Twig scope
    $twig->addFilter('md5', new Twig_Filter_Function('md5'));

    return $twig;
}));

と変更していました。SilexのServiceProvierを使う場合は覚えておきたいパターンですね。

2.0での変更点

shareがデフォルトに

2.0では、以前のshare()の動作がデフォルトになりました。すなわちセットされたものが無名関数であれば、常にキャッシュされた値を返します。またshareというメソッドは無くなりました。

別のインスタンスを返してほしい場合は、factoryメソッドを使います。

$c['movie_lister'] = $c->factory(function($c) {
    return new MovieLister($c['movie_finder']);
});

extendの変更

1.xでは、extendメソッドの戻り値をコンテナにアサインしなおしていましたが、その必要がなくなりました

// 1.x
$c['movie_finder'] = $c->extend('movie_finder', function($movie_finder, $c) {
    $movie_finder->setFilename($c['filename']);
    return $movie_finder;
});

// 2.0
$c->extend('movie_finder', function($movie_finder, $c) {
    $movie_finder->setFilename($c['filename']);
    return $movie_finder;
});