Hack向けDIコンテナライブラリその2 公開

DI Container For HHVM/Hack

だいぶ前に作った軽量なものから、
HackのDIコンテナ向けインターフェース hack-interface-standards/container に準拠し、
HHVM4.0以降に対応しました。

github.com

下記のDIライブラリを意識していますが、作っていくと少しずつ変わっていきました。

github.com

利用方法を紹介します。

installation

インストール方法は、Composer経由で利用できます。
HHVM4.2+composer 1.8.5の組み合わせでもインストールできますので、
まだ安心してComposerを利用できます。

実際に利用する場合は、hhvm/hhvm-autoload 等も必要になりますので、
Composer経由でインストールします。

composer.jsonは下記のもので試しに動かすことができます。

{
  "name": "acme/testing",
  "minimum-stability": "stable",
  "require": {
    "hhvm": "^4.0",
    "hhvm/hhvm-autoload": "^2.0.0",
    "nazg/glue": "^1.1.2"
  }
}

HHVM/Hackのクラスマップ等はcomposer.jsonに記述しなくても、
hhvm-autoloadで吸収できますので、 hh_autoload.json を下記の通りに記述します。

{
  "roots": [
    "src/"
  ]
}

簡単な使い方

シンプルなクラスのインスタンス生成を登録します。
下記のクラスを、src/A.hack ファイルに記述します。
.hackはHHVM4.0から利用できるようになった、デフォルトで厳格モードになる拡張子です

final class A {
}

このクラスをコンテナに登録例として
src/main.hackファイルを作成してそのまま記述します。

require_once __DIR__ . '/../vendor/hh_autoload.php';

<<__EntryPoint>>
function main(): void {
  $t1 = microtime(true);
  $builder = new Nazg\Glue\ContainerBuilder();
  $container = $builder->make();
  $container->bind(A::class)
    ->to(A::class)
    ->in(Nazg\Glue\Scope::PROTOTYPE);
  $container->get(A::class);
}

上記の記述は、コンストラクタにAクラスがあれば(bind)、
Aクラスのインスタンスをそのまま渡す(to)指定で、
インスタンスは、都度インスタンス生成を行います(Nazg\Glue\Scope::PROTOTYPE enum)。
生成されたインスタンスはPSR-7と同様にgetメソッドで取得します。

enum Nazg\Glue\Scope

インスタンス生成は、enumで下記の二種が利用できます。

enum スコープ
Nazg\Glue\PROTOTYPE 都度インスタンス生成を行う
Nazg\Glue\SINGLETON 一度だけインスタンスを生成し、以降はそれを利用します

インターフェースを利用する場合

PHPの一般的なDIコンテナと同じように利用できます。

interface AInterface {
}

class A implements AInterface{
}

上記のインターフェースとクラスは、次の通りに記述してインスタンスを取得できます。

require_once __DIR__ . '/../vendor/hh_autoload.php';

<<__EntryPoint>>
function main(): void {
  $builder = new Nazg\Glue\ContainerBuilder();
  $container = $builder->make();
  $container->bind(AInterface::class)
    ->to(A::class)
    ->in(Nazg\Glue\Scope::PROTOTYPE);

  var_dump($container->get(AInterface::class));
}

bindメソッドでAInterface::class を指定し、 getメソッドでbindメソッド名を指定するだけです。

実装の裏側

getメソッドやbindメソッドなどは存在するクラス名のみを指定できます。

下記はbindメソッドのコードですが、
typename<T>を利用しこれ以外の型は利用できません。
したがってPHPライブラリなどである任意のサービス名などを使うことはできません。

  public function bind<T>(
    typename<T> $id
  ): Bind<T> {
    return new Bind($this, $id, $this->factory);
  }

依存解決

下記の場合はどうすればいいのでしょうか?

interface AInterface {
}

class A implements AInterface{
}

class B {
  public function __construct(
    public AInterface $a
  ) {}
}

複数のクラスを組み合わせる場合、
特殊なものがなければ次の記述で解決できます。

require_once __DIR__ . '/../vendor/hh_autoload.php';

<<__EntryPoint>>
function main(): void {

  $builder = new Nazg\Glue\ContainerBuilder();
  $container = $builder->make();

  $container->bind(B::class)
    ->to(B::class)
    ->in(Nazg\Glue\Scope::PROTOTYPE);
  $container->bind(AInterface::class)
    ->to(A::class)
    ->in(Nazg\Glue\Scope::PROTOTYPE);
  var_dump($container->get(B::class));
}

簡単ですね。

Provider

上記の依存解決方法では解決できないものは、
Providerインターフェースを使って依存解決を記述できます。
先ほどの例をそのまま使います。

interface AInterface {
}

class A implements AInterface{
}

class B {
  public function __construct(
    public AInterface $a
  ) {}
}

Providerインターフェースを使う場合は次の通りです。

class BProvider implements Nazg\Glue\ProviderInterface<B> {

  public function get(
    \Nazg\Glue\Container $container
  ): B {
    return new B(new A());
  }
}

Nazg\Glue\ProviderInterfaceにジェネリクスでB、
getメソッドで返却される型はジェネリクスに記述したクラスと同じ型を指定します。

Providerインターフェースを実装したクラスをコンテナに登録しなければ利用できないため、
下記の記述で登録します。

require_once __DIR__ . '/../vendor/hh_autoload.php';

<<__EntryPoint>>
function main(): void {

  $builder = new Nazg\Glue\ContainerBuilder();
  $container = $builder->make();
  $container->bind(B::class)
    ->provider(new BProvider())
    ->in(Nazg\Glue\Scope::PROTOTYPE);
  var_dump($container->get(B::class));
}

上記の登録方法を利用することで様々なインスタンス生成を行うことができます。

他にもいくつかありますが、
開発時に利用することがあれば是非色々試してみてください。