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

# はじめに
[BEAR.Sundayアドベントカレンダー](https://qiita.com/advent-calendar/2019/bear-sunday)の[天重さんの記事](https://qiita.com/amashigeseiji/items/2d6c497afeedce2e0f2c)ではカスタムスキームの登録でしたが、この記事ではhostの登録です。
HTTPのURLでいえばプロトコルは変わらずホストが違う場合ということです。

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

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

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


## SchemeCollectionProvider
標準ではResourceアクセスの際のスキーマやホストの解決のために、[BEAR\Resource\Module\SchemeCollectionProvider](https://github.com/bearsunday/BEAR.Resource/blob/1.x/src/Module/SchemeCollectionProvider.php)が用意されていますので、これを参考にします。

```php
<?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種類扱っているのは、ライブラリを利用するアプリケーションとライブラリ自体、それぞれ２つのBEAR.Sundayアプリケーションを扱うためです。

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

```php
<?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
<?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);
    }
}
```
これで以下のようにアプリケーション内でライブラリのリソースにアクセスできるようになっていると思います。

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

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

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

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

## PrioritizedSqlQueryModule

```php
<?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ディレクトリを指定します。

```php
$this->install(new PrioritizedSqlQueryModule($primarySqlDir, $baseDir . '/var/db/sql'));
```