c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

PHPで簡単に華麗にDIとAOPをキメる

PHP Advent Calendar 2014の11日目です。昨日は普通じゃないモッキングフレームワークAspectMockがパワフル過ぎるでした。明日は @ さんです。

おくすりをきめキメた。

経緯

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つです。

  1. Pimple 3と100%互換です。Ranyuen\Di\ContainerPimple\Containerを継承してゐます。Pimple 3を使うてゐれば、use Pimple\Container;use Ranyuen\Di\Contaier;に書き換へるだけです。Pimple 2以下を使うてゐれば、Pimple 3に移行するのと同じ手続きが必要になります。また今後Pimpleが更新した場合、追隨する予定です。
  2. Pimpleと同様に、設定は不要です。設定を行なふ場合、YamlXMLPHPの設定を書くことなく無く、簡単なannotationを書くだけで依存を解決します。
  3. 設定すること無く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メソッドも継承されるので、インターセプトできます。RubyJavaScriptから来ると誤解しますが、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になります。