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

BEAR.Sundayでカスタムhostを利用する

はじめに

BEAR.Sundayアドベントカレンダー天重さんの記事ではカスタムスキームの登録でしたが、この記事ではhostの登録です。
HTTPのURLでいえばプロトコルは変わらずホストが違う場合ということです。

やりたいこと

例えば社内で共通の処理を独立した形で公開し、複数のアプリケーションにまたがって利用したいことなどないでしょうか?
最終的にアプリケーションからこんな感じで社内ライブラリのResourceを利用できたら便利そうですね。

$article = $this->resource->get('app://acme/article');

ライブラリ(AcmeFramework)を作る

各アプリケーションから共通機能を利用するためのブリッジになるような基盤の仕組みを作成します。
このAcmeFrameworkはcomposerのパッケージとして各アプリケーションにインストールされる想定です。

SchemeCollectionProvider

標準ではResourceアクセスの際のスキーマやホストの解決のために、BEAR\Resource\Module\SchemeCollectionProviderが用意されていますので、これを参考にします。

<?php
declare(strict_types=1);

namespace Acme\Framework\Provider;

use BEAR\Resource\AppAdapter;
use BEAR\Resource\HttpAdapter;
use BEAR\Resource\SchemeCollection;
use BEAR\Resource\SchemeCollectionInterface;
use Ray\Di\Di\Named;
use Ray\Di\InjectorInterface;
use Ray\Di\ProviderInterface;

class SchemeCollectionProvider implements ProviderInterface
{
    /**
     * @var string
     */
    private $appName;

    /**
     * @var InjectorInterface
     */
    private $appInjector;

    /**
     * @var InjectorInterface
     */
    private $appInjectorAcme;

    /**
     * @var string
     */
    private $appNameAcme;

    /**
     * @param string $appName
     * @param string $appNameAcme
     * @param InjectorInterface $appInjector
     * @param InjectorInterface $appInjectorAcme
     *
     * @Named("appName=app_name, appNameAcme=app_name_acme, appInjectorAcme=acme")
     */
    public function __construct(
        string $appName,
        string $appNameAcme,
        InjectorInterface $appInjector,
        InjectorInterface $appInjectorAcme
    ) {
        $this->appName = $appName;
        $this->appNameAcme = $appNameAcme;
        $this->appInjector = $appInjector;
        $this->appInjectorAcme = $appInjectorAcme;
    }

    /**
     * Return instance
     *
     * @return SchemeCollectionInterface
     */
    public function get() : SchemeCollectionInterface
    {
        $schemeCollection = new SchemeCollection;
        $pageAdapter = new AppAdapter($this->appInjector, $this->appName);
        $appAdapter = new AppAdapter($this->appInjector, $this->appName);
        $acmeFrameworkAdapter = new AppAdapter($this->appInjectorAcme, $this->appNameAcme);
        $schemeCollection->scheme('page')->host('self')->toAdapter($pageAdapter)
            ->scheme('app')->host('self')->toAdapter($appAdapter)
            ->scheme('http')->host('self')->toAdapter(new HttpAdapter($this->appInjector))
            ->scheme('app')->host('acme')->toAdapter($acmeFrameworkAdapter);

        return $schemeCollection;
    }
}

コストラクタで$appName$appNameAcme$appInjector$appInjectorAcmeとそれぞれ2種類扱っているのは、ライブラリを利用するアプリケーションとライブラリ自体、それぞれ2つのBEAR.Sundayアプリケーションを扱うためです。

AcmeFrameworkModule

上記のSchemeCollectionProviderが実際に適用されるようにしたいと思います。アプリケーションからはModuleをインストールすることで利用できます。

<?php
namespace Acme\Framework\Module;

use BEAR\Package\AppInjector;
use BEAR\Resource\SchemeCollectionInterface;
use Acme\Framework\Provider\SchemeCollectionProvider;
use Acme\Framework\Query\PrioritizedSqlQueryModule;
use Ray\Di\AbstractModule;
use Ray\Di\InjectorInterface;

class AcmeModule extends AbstractModule
{
    /**
     * @var AbstractAppMeta
     */
    private $appMeta;

    public function __construct(AbstractAppMeta $appMeta)
    {
        $appName = str_replace('\\Module', '', __NAMESPACE__);
        $rootDir = dirname(__DIR__, 2);
        $this->appMeta = new Meta($appName, 'appacme', $appMeta->appDir);
        $this->appMeta->appDir = $rootDir;
        parent::__construct();
    }

    /**
     * @throws \ReflectionException
     */
    public function configure()
    {
        $rootDir = dirname(__DIR__, 2);
        $appName = str_replace('\\Module', '', __NAMESPACE__);
        $this->bind(InjectorInterface::class)->annotatedWith('acme')
            ->toInstance(new AppInjector($appName, 'app'));
        $this->bind()->annotatedWith('app_name_acme')->toInstance($appName);
        $this->bind(SchemeCollectionInterface::class)->toProvider(SchemeCollectionProvider::class);
    }
}

コンストラクタでAppMetaを色々操作しているのは、以下のような設定を実現するためです。

  • appDir(アプリケーションルートディレクトリ)
    • パッケージがインストールされたディレクトリ vendor/acme-framework
  • コンテキスト
    • appacme に設定。何でも良いのですが、アプリケーションとバッティングしないように。
  • tmpDir(キャッシュなどの一時ファイル置き場)
    • アプリケーションに用意されたディレクトリ。通常は {$appDir}/var/tmp/{$context}
  • logDir(ログ置き場)
    • アプリケーションに用意されたディレクトリ。通常は {$appDir}/var/log/{$context}

アプリケーションでの利用

上記Moduleをinstallするだけです。BEAR\Package\PackageModuleよりも先にinstallする必要があります。

<?php
namespace Acme\Application\Module;

use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;
use Acme\Framework\Module\AcmeFrameworkModule;
use ReflectionException;

class AppModule extends AbstractAppModule
{
    /**
     * {@inheritdoc}
     *
     * @throws ReflectionException
     */
    protected function configure()
    {
        $this->install(new AcmeFrameworkModule($this->appMeta));
        $this->install(new PackageModule);
    }
}

これで以下のようにアプリケーション内でライブラリのリソースにアクセスできるようになっていると思います。

$article = $this->resource->get('app://acme/article');

おまけ

Resourceの中でSQLを扱う場合、コードからインフラに関わるコード(MySQLやKVS、APIへのアクセスなど)を排除するためのRay.QueryModuleというものがあり、よくSQLのInjectで利用されていると思います。
例えばこれが以下のようなルールで運用できたら更に処理の共通化がしやすいのではないでしょうか。

  • アプリケーションに該当のSQLファイルが用意されていたらアプリケーションのSQLファイルを使う
  • アプリケーションに該当のSQLファイルが用意されていなかったらライブラリのSQLファイルを使う

つまり、sqlファイルの探索を優先順位がつけられた二箇所から行うようにするということです。

PrioritizedSqlQueryModule

<?php

namespace Acme\Framework\Query;

use Ray\Di\AbstractModule;
use Ray\Query\Annotation\AliasQuery;
use Ray\Query\QueryInterface;
use Ray\Query\RowInterface;
use Ray\Query\RowListInterface;
use Ray\Query\SqlAliasInterceptor;
use Ray\Query\SqlQueryRow;
use Ray\Query\SqlQueryRowList;

class PrioritizedSqlQueryModule extends AbstractModule
{
    /**
     * @var string
     */
    private $sqlDirPrimary;

    /**
     * @var string
     */
    private $sqlDirSecondary;

    public function __construct(string $sqlDirPrimary, string $sqlDirSecondary, AbstractModule $module = null)
    {
        $this->sqlDirPrimary = $sqlDirPrimary;
        $this->sqlDirSecondary = $sqlDirSecondary;
        parent::__construct($module);
    }

    /**
     * {@inheritdoc}
     */
    protected function configure() : void
    {
        $this->bindSql($this->sqlDirSecondary);
        $this->bindSql($this->sqlDirPrimary);
        $this->bindInterceptor(
            $this->matcher->any(),
            $this->matcher->annotatedWith(AliasQuery::class),
            [SqlAliasInterceptor::class]
        );
    }

    private function bindSql(string $sqlDir) : void
    {
        foreach ($this->files($sqlDir) as $fileInfo) {
            /* @var \SplFileInfo $fileInfo */
            $fullPath = $fileInfo->getPathname();
            $name = pathinfo($fileInfo->getRealPath())['filename'];
            $sqlId = 'sql-' . $name;
            $this->bind(QueryInterface::class)->annotatedWith($name)->toConstructor(
                SqlQueryRowList::class,
                "sql={$sqlId}"
            );
            $this->bindCallableItem($name, $sqlId);
            $this->bindCallableList($name, $sqlId);

            $sql = trim(file_get_contents($fullPath));
            $this->bind('')->annotatedWith($sqlId)->toInstance($sql);
        }
    }

    protected function bindCallableItem(string $name, string $sqlId) : void
    {
        $this->bind(RowInterface::class)->annotatedWith($name)->toConstructor(
            SqlQueryRow::class,
            "sql={$sqlId}"
        );
    }

    protected function bindCallableList(string $name, string $sqlId) : void
    {
        $this->bind()->annotatedWith($name)->toConstructor(
            SqlQueryRowList::class,
            "sql={$sqlId}"
        );
        $this->bind(RowListInterface::class)->annotatedWith($name)->toConstructor(
            SqlQueryRowList::class,
            "sql={$sqlId}"
        );
    }

    private function files($dir) : \RegexIterator
    {
        return
            new \RegexIterator(
                new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator(
                        $dir,
                        \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS
                    ),
                    \RecursiveIteratorIterator::LEAVES_ONLY
                ),
                '/^.+\.sql$/',
                \RecursiveRegexIterator::MATCH
            );
    }
}

利用

以下のようにインストールします。引数で優先して探索するsqlディレクトリを指定します。

$this->install(new PrioritizedSqlQueryModule($primarySqlDir, $baseDir . '/var/db/sql'));
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
ユーザーは見つかりませんでした