Help us understand the problem. What is going on with this article?

SlimのDI-ContainerでRay.Diを使ってみる

この記事はPHP Advent Calendar 2019の13日目の記事です。

昨日はpolidogさんのhelicon/object-mapperを作ったでした。
型情報と連想配列のマッピングまでできるというのはアツいですね。僕もライブラリ公開していきたい。

さて本日はWeb Application FrameworkのSlimとDI ContainerのRay.Diを連携させる方法をご紹介します。

背景

僕はレガシーなフレームワークを使っている環境に関わっているのですが、
いい加減このフレームワークへの依存を切り離し、コードを減らしていきたいと常々思っています。

ある日、色々検討する過程で、「別のWAFでリクエストをハンドル・プロキシして古いコード動かせばよくね?」ということを思いつきました。
色々ある世のフレームワークの中で、PSRに準拠しており、学習コストが低い(コードすぐ読める)Slimを実験で使うことにしました。

また、該当のプロジェクトではコードの複雑な依存性を解消するために、DIコンテナとしてRay.Diを用いています。
極論を言ってしまえば、SlimでRay.Diを使えれば大体もうなんでもできるんじゃないか、ということですね。

Slimとは?

SlimはPHPのマイクロフレームワークです。

PSRに準拠した、ルーティングとDIを提供する小さなライブラリなので、
フルスタックフレームワークのネットワーク系の処理の差し替えに向いていると思い検討に入れました。

また、コード量が少なく、PSRに準拠した機構なので内容を把握しやすいです。
これらはフレームワークのレールから外れた対応を行うので、コードを読んで中身を把握しやすいという観点も重要です。

Ray.Diとは?

Ray.DiはPHPのDIコンテナです。
RESTFulアプリケーションのフレームワークであるBEAR.Sundayでも利用されています。(絶賛勉強中)

JavaのGuiceを参考に作られており、細かな挙動調整ができて非常に便利です。
アノテーションによるInjectionの制御やBindingの設定などの柔軟性が高く、既存の処理との兼ね合いを考慮した設定も可能なので、レガシーコードの改善で役立つシーンは多いと感じています。

なにより、フレームワークライブラリが提供するDIではないため、今回のようなフレームワーク移管に関する取り回しがとても効きます。

SlimとDI-Container

Slimは独自でDI-Conatinerの仕組みを持っていますが、これもPSRに準拠しているため差し替えが用意です。
参考: Dependency Container

SlimでDIを行う場合は以下のようにします。

<?php
use DI\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

// Create Container using PHP-DI
$container = new Container();

// Set container to create App with on AppFactory
AppFactory::setContainer($container);
$app = AppFactory::create();

// Add a service
$container->set('myService', function () {
    return new \MyService();
});

// Get a myService's instance
$app->get('/foo', function (Request $request, Response $response, $args) {
    $myService = $this->get('myService');
    // ...
    return $response;
});

このとき、DIはPSR-11で定義されているContainerInterfaceを期待しています。
なので、Ray.DiをContainerInterfaceでラップしたクラスを定義してやれば違和感なく利用できます。

Ray.Diのお手軽使い方

Redisを使った簡単データストア例です。昨今ならFirebaseとか使えよ感。

まずはInterfaceを定義。

src/Modules/Store/StoreInterface.php
<?php
namespace Example\Modules\Store;

interface StoreInterface
{
    public function get(string $id);
    public function set(string $id, $value);
}

本体を実装していきます。

src/Modules/Store/Store.php
<?php
namespace Example\Modules\Store;

use Predis\Client;
use Ray\Di\Di\Named;

final class Store implements StoreInterface
{
    private $client;

    /**
     * @Named("scheme=redis_scheme,host=redis_host,port=redis_port")
     */
    public function __construct(string $scheme, string $host, int $port)
    {
        $this->client = new Client([
            'scheme' => $scheme,
            'host' => $host,
            'port' => $port,
        ]);
    }

    public function get(string $id)
    {
        return json_decode($this->client->get($id), true);
    }

    public function set(string $id, $value)
    {
        $this->client->set($id, json_encode($value));
    }
}

モジュール定義します。

src/Modules/Store/StoreModule.php
<?php
namespace Example\Modules\Store;

use Ray\Di\AbstractModule;
use Ray\Di\Scope;

final class StoreModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(StoreInterface::class)->to(Store::class)->in(Scope::SINGLETON);
        $this->bind()->annotatedWith('redis_scheme')->toInstance($_ENV['REDIS_SCHEME']);
        $this->bind()->annotatedWith('redis_host')->toInstance($_ENV['REDIS_HOST']);
        $this->bind()->annotatedWith('redis_port')->toInstance($_ENV['REDIS_PORT']);
    }
}

呼び出し方。

<?php
use Ray\Di\Injector;
use Example\Modules\Store\StoreInterface;
use Example\Modules\Store\StoreModule;

$injector = new Injector(new StoreModule);
$store = $injector->getInstance(StoreInterface::class);

Slim with Ray.Di

実際にRay.DiをSlimと連携させていきます。

Containerのラッパーを作成

PSR-11のContainerInterfaceを実装します。
コンストラクタでAbstractModuleを受け取り、Injectorを作成します。

hasについてはRay.DiにそれっぽいI/Fが無く、UnboundのExceptionが発行されるようなのでとりあえずTry-Catchします。

src/Container.php
<?php
namespace Example;

use Psr\Container\ContainerInterface;
use Ray\Di\AbstractModule;
use Ray\Di\Injector;

final class Container implements ContainerInterface
{
    private $injector;

    public function __construct(AbstractModule $module)
    {
        $this->injector = new Injector($module);
    }

    /**
     * @inheritDoc
     */
    public function get($id)
    {
        return $this->injector->getInstance($id);
    }

    /**
     * @inheritDoc
     */
    public function has($id)
    {
        try {
            $this->get($id);
            return true;
        } catch (\Ray\Di\Exception\Unbound $e) {
            return false;
        }
    }
}

Ray.Diのモジュールを作成

モジュールをインストールするルートのModuleを定義します。
ここはBEAR.Sundayを参考にしました。ここでモジュールを一覧できるようになります。

src/AppModule.php
<?php
namespace Example;

use Example\Modules\Store\StoreModule;
use Ray\Di\AbstractModule;

final class AppModule extends AbstractModule
{
    protected function configure()
    {
        $this->install(new StoreModule());
    }
}

Slimアプリの作成

ContainerにAppModuleのインスタンスを渡して初期化したものを、AppFactoryに与えます。
これにより、Slimの処理からDI-Containerを呼び出せるようになります。

index.php
<?php
use Example\AppModule;
use Example\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

require __DIR__ . '/vendor/autoload.php';

$container = new Container(new AppModule());

AppFactory::setContainer($container);
$app = AppFactory::create();

$app->get('/', function (Request $req, Response $res, array $args) {
    $res->getBody()->write("Hello, slim");
    return $res;
});

DI-Containerと連携する

StoreInterfaceを元にモジュールをコールバックに注入する例です。
Controllerとか作ってコンストラクタでインジェクションすることもできます。

index.php
use Example\Modules\Store\StoreInterface;

$app->get('/posts/{id}', function (Request $req, Response $res, array $args) {
    $store = $this->get(StoreInterface::class);
    $id = $args['id'];
    $row = $store->get($id);
    if (!$row) {
        throw new \Slim\Exception\HttpNotFoundException($req, "id($id) post notfound");
    }
    $payload = json_encode($row);
    $res->getBody()->write($payload);
    return $res->withHeader('Content-Type', 'application/json');
});

$app->post('/posts', function (Request $req, Response $res) {
    $body = $req->getParsedBody();
    $id = com_create_guid();
    $store = $this->get(StoreInterface::class);
    $store->set($id, [
        'subject' => $body['subject'],
        'content' => $body['content'],
        'created_at' => (new \DateTime())->format(\DateTime::ATOM),
    ]);
    $payload = json_encode(['id' => $id]);
    $res->getBody()->write($payload);
    return $res->withHeader('Content-Type', 'application/json');
});

まとめ

今回作成したコードはこちらにおいてあります。参考にどうぞ。

Containerを一段覆わないと行けない点はありますが、少ないコードで移植性の高い基盤が作れそうです。

Slimは簡単にかけるフレームワークだなと思ってたんですが、基礎モジュールがPSRのInterface依存なので、非常に拡張しやすいですね。(昨今のフレームワークはだいたいそうかな?)

Ray.Diも拡張性が高く、結合度の高いプロジェクトの中で部分的に疎結合な空間を作っていく際にとても役立ちます。
依存性が複雑 & 密結合なレガシーコードの改善の参考になれたら嬉しいです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした