k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

マイクロフレームワークをつくろう - Pimpleの上に(Pimple拡張編)

DIコンテナの効能が謳われだして久しい昨今、業務でもSilexでいくつか小さなアプリケーションを作ったりする中で、ようやくその利点を実感するようになりました。

そのうち、実は自分が惚れたのはSilexではなくて、その母体たるPimpleだったんじゃないかと考えるに至ったので、Pimpleを自分好みに使うための小さなクラスを作成してみました。

以下、コードはPHP5.4で動作確認しています。

<?php
namespace Acme;

class Application extends \Pimple
{

    private $handlers;

    public function __construct(array $values = array())
    {
        parent::__construct($values);
        $this->handlers = array();
    }

    public function __get($name)
    {
        return parent::offsetGet($name);
    }

    public function __set($name, $value)
    {
        parent::offsetSet($name, $value);
    }

    public function __call($name, $args)
    {
        if (parent::offsetExists($name)) {
            $value = parent::offsetGet($name);
            if (is_callable($value)) {
                return call_user_func_array($value, $args);
            }
            return $value;
        }
        if (array_key_exists($name, $this->handlers)) {
            switch (count($args)) {
                case 0:
                    return $this->execute($name);
                case 1:
                    return $this->execute($name, $args[0]);
                case 2:
                    return $this->execute($name, $args[0], $args[1]);
                case 3:
                    return $this->execute($name, $args[0], $args[1], $args[2]);
                case 4:
                    return $this->execute($name, $args[0], $args[1], $args[2], $args[3]);
            }
        }
        throw new \BadMethodCallException(
            sprintf('Undefined Method "%s" called.', $name)
        );
    }

    public function registerEvent($event, $handlers = null)
    {
        if (array_key_exists($event, $this->handlers)) {
            throw new \InvalidArgumentException(
                sprintf('The event "%s" is already defined.', $event)
            );
        }
        $this->handlers[$event] = array();
        if (isset($handlers)) {
            if (!is_array($handlers)) {
                throw new \InvalidArgumentException(
                    sprintf('The event "%s" handlers is not array. type:%s', $name, gettype($handlers))
                );
            }
            foreach ($handlers as $handler) {
                $this->addHandler($event, $handler);
            }
        }
        return $this;
    }

    public function addHandler($event, callable $handler)
    {
        if (!array_key_exists($event, $this->handlers)) {
            throw new \InvalidArgumentException(
                sprintf('The event "%s" is not defined.', $event)
            );
        }
        $this->handlers[$event][] = $handler;
        return $this;
    }

    public function execute($event)
    {
        if (!array_key_exists($event, $this->handlers)) {
            throw new \InvalidArgumentException(
                sprintf('The event "%s" is not defined.', $event)
            );
        }
        $args = func_get_args();
        $args[0] = $this;
        foreach ($this->handlers[$event] as $handler) {
            $result = call_user_func_array($handler, $args);
        }
        if (isset($result)) {
            return $result;
        }
        return $this;
    }

}

何かというと、ArrayAccessによる利用を前提としたPimpleにプロパティアクセスとメソッドコールを実装して、任意のコールバックの配列をイベントとして実行できるようにしたものです。

プロパティアクセスの例

素のPimpleの場合

<?php
$app = new \Pimple();

$app['parent'] = $app->share(function(\Pimple $app) {
    $parent = new \stdClass();
    $parent->name = 'parent';
    return $parent;
});

$app['child'] = $app->share(function(\Pimple $app) {
    $child = new \stdClass();
    $child->name = 'child';
    $child->parent = $app['parent'];
    return $child;
});

echo $app['parent']->name; // parent
echo $app['child']->name; // child
echo $app['child']->parent->name; // parent

上記Applicationクラスの場合

<?php
namespace Acme;

$app = new Application();

$app->parent = $app->share(function(Application $app) {
    $parent = new \stdClass();
    $parent->name = 'parent';
    return $parent;
});

$app->child = $app->share(function(Application $app) {
    $child = new \stdClass();
    $child->name = 'child';
    $child->parent = $app->parent;
    return $child;
});

echo $app->parent->name; // parent
echo $app->child->name; // child
echo $app->child->parent->name; // parent

こうすることで、たとえばSmartyのようなオブジェクトのプロパティと配列の要素へのアクセスを区別するようなテンプレートエンジンにおいても、ストレスなく記述できるようになります。

({$app.child->parent->name} ではなく {$app->child->parent->name} と書ける)

メソッドコールの例

Pimple::offsetGet() では返却対象が "__invoke()" マジックメソッドを実装したオブジェクトの場合は自身を引数にしてinvokeするよう実装されています。

素のPimpleの場合

<?php
namespace Acme;

class Invokable
{
    public function __invoke()
    {
        return 'Invokable';
    }
}

$app = new \Pimple();
$app['invoker'] = new Invokable();

echo $app['invoker']; // Invokable

上記Applicationクラスの場合

<?php
namespace Acme;

class Invokable
{
    public function __invoke()
    {
        return 'Invokable';
    }
}

$app = new Application();
$app->invoker = new Invokable();

echo $app->invoker; // Invokable
echo $app->invoker(); // Invokable

プロパティアクセスも可能ですが、更にメソッドコールにより分かりやすい形で実行できます。

また Pimple::protect() で登録したコールバック関数(クロージャ)を実行する場合も同様です。

あまり実用的な例ではありませんが…。

素のPimpleの場合

<?php
namespace Acme;

$counter = 0;

$app = new \Pimple();
$app['count'] = $app->protect(function() use (&$counter) {
    $counter++;
    return $counter;
});

echo $app['count'](); // 1
echo $app['count'](); // 2

上記Applicationクラスの場合

<?php
namespace Acme;

$counter = 0;

$app = new Application();
$app->count = $app->protect(function() use (&$counter) {
    $counter++;
    return $counter;
});

echo $app->count(); // 1
echo $app->count(); // 2

まだ試してませんが、PHPTALなどメソッドとプロパティを同じ記述で参照するようなテンプレートエンジンにおいても、有効に機能するんじゃないでしょうか。

イベントおよびハンドラ関数の登録と実行

ここまでは単にコンテナ内のオブジェクトへのアクセス方法を追加したというだけですが、registerEvent(), addHandler(), execute() については独自の機能になります。

これまた実用的な例ではありませんが…。

<?php
namespace Acme;

$app = new Application();

$app->registerEvent('init');
$app->addHandler('init', function(Application $app) {
    $app->counter = 0;
    return $app->counter;
});

$app->registerEvent('count');
$app->addHandler('count', function(Application $app) {
    $app->counter++;
    return $app->counter;
});

echo $app->execute('init'); // 0
echo $app->execute('count'); // 1
echo $app->execute('count'); // 2
echo $app->count(); // 3
echo $app->count(); // 4
echo $app->init(); // 0

registerEvent() でイベントを登録、addHandler($event, $function) でイベントに対するコールバックを登録、 execute($event) でイベントを実行します。

また、イベント名によるメソッドコールも可能です。

addHandler() で登録したコールバックの第1引数には必ず Application オブジェクトが渡されてくるので、これによって連鎖的にイベントを実行することも可能です。

<?php
namespace Acme;

$app = new Application();
$app->counter = 0;

$app->registerEvent('foo');
$app->addHandler('foo', function(Application $app) {
    $app->counter++;
    return $app->execute('bar');
});

$app->registerEvent('bar');
$app->addHandler('bar', function(Application $app) {
    $app->counter++;
    return $app->execute('baz');
});

$app->registerEvent('baz');
$app->addHandler('baz', function(Application $app) {
    $app->counter++;
    return $app->counter;
});

echo $app->foo(); // 3
echo $app->bar(); // 5
echo $app->baz(); // 6
echo $app->foo(); // 9
echo $app->bar(); // 11
echo $app->baz(); // 12

これをベースに、マイクロフレームワーク的なものを組み上げていこうという試みです。

PHP 5.5リリースで沸いている時機にこんなネタというのもなんですが、自分、いわゆるレイトマジョリティを自負してますのでご容赦ください。

なお、記事のタイトルはPimpleの生みの親でもあるFabien先生の素晴らしい記事 Create your own framework... on top of the Symfony2 Components (part 1) - Fabien Potencier の日本語訳 Web フレームワークをつくろう - Symfony コンポーネントの上に からパク…オマージュ?させていただきました。