はじめに
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'));