Posted at

PHPのDIコンテナを作った話

DIというか Auto Wiring Container って書いたけど。

https://github.com/nishimura/simple-container

今まで何度かフレームワークを作っていて、その全てで必須だった機能だけ抜き出した感じなので作ったというか削ってまとめたというか。

使い方

composer require nish/simple-container

<?php

use Nish\Container\Container;
require_once 'vendor/autoload.php';
$c = Container::getInstance();

パッケージにしてるけど200行のソース1ファイル。

各フレームワークのコンポーネントやDI単体のパッケージがいくつもあることは知っているけれど、使わない機能のために何十ファイルとか何千行とかのプログラムを入れるのは好きじゃないので、ファイルを一つ置けば使えるレベルのものにした。

コンストラクタインジェクション。よくあるやつ。

// auto wiring

class Foo
{
private $obj;
public function __construct(stdClass $obj)
{
$this->obj = $obj;
}
}

$c = Container::getInstance();
$foo = $c->get(Foo::class);

あらかじめ登録する場合と、ファクトリー的なやつ。

interface Uri {}

class MyUri implements Uri {}

$c->set(Uri::class, new MyUri);
$uri = $c->get(Uri::class);
// こちらはsingleton

// こちらは毎回newする
$c->setFactory(Uri::class, function($c){
return new MyUri();
});

// 自分でnewしつつsingletonにしたい場合
$c->setFactory(Uri::class, function($c){
$myUri = new MyUri();
$c->set(Uri::class, $myUri);
return $myUri;
});

あとはauto wiringしつつメソッドを実行する機能も個人的には必須だったので。

// メソッド実行

$c->call(Foo::class, 'staticMethod');
$foo = new Foo();
$c->call($foo, 'method');

こういうものと、それから型で一意に特定できない場合の対応

// 引数名によるインジェクション

namespace MyProject;
class Db {
private $dsn;
public function __construct(string $dsn){
$this->dsn = $dsn;
}
// ...
}
$c->set('MyProject\\Db#__construct.dsn', 'mysql://dbname...');
$db = $c->get(MyProject\\Db::class);

引数の名前でインジェクションする機能も使う。urlのidを取得して担当クラスのメソッドを呼び出す、みたいなことを自動でするときに使う。

ここまで来れば、あとは FastRoute などを使って

class MyUserAction

{
public static function index(Db $db, MyForm $myForm, $userId)
{
// ここがアプリの処理
}
}

// ルーティング設定ファイル的な
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{userId:\d+}', ['myuser.html', MyUserAction::class, 'index']);
});

// ここからフレームワーク的な処理
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
// ... 404 Not Found
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
// ... 405 Method Not Allowed
break;
case FastRoute\Dispatcher::FOUND:
[$template, $cls, $method] = $routeInfo[1];
$vars = $routeInfo[2];
$result = $c->call($cls, $method, $vars);
break;
}

$te = new TemplateEngine();
$te->output($template, $result);

フロントコントローラーからアクションを呼び出してテンプレートエンジンに渡すというよくある処理がすぐできる。

この辺の処理は 12年ぐらい前 にやっていて、多機能を求めたり シンプルを求めたり行ったり来たりしている。

アノテーションで色んなことをする版は、composer update でアノテーション解析が究極に遅くなったのでその次から止めた。

最近はとにかくシンプルにしたくてクラスじゃなくて関数にしたいんだけど、autoloadが働かないから仕方なくstaticメソッドにしている。