BEAR.SundayのResourceにカスタムスキームを追加する方法

# はじめに
BEAR.Sundayを利用している方向けの記事となります

https://bearsunday.github.io/index.html

BEAR.SundayにはRay.Di, Ray.Aop, BEAR.Resourceという3つの大きな要素があるのですが、今回はBEAR.Resourceのスキームのカスタマイズについて紹介します

# デフォルトは2つのスキームで

```php
$ro = $this->resource->get('app://self/weekday', ['year' => '2001', 'month' => '1', 'day' => '1']);
```

Resourceの呼び出しは上記のように行います

BEAR.Sundayではデフォルトで `app://`, `page://` のスキームが用意されています

小規模であれば、この2つのスキームだけで問題ないのですが、規模が大きくなってくると、スキームを増やし役割ごとに分割したくなることもあるでしょう

今回は `app://`, `page://` 以外のスキームで呼び出しができるようにカスタマイズする方法を紹介します

# デモリポジトリ

https://github.com/gamu1012/MyVendor.CustomScheme

# スキームの追加
serviceスキームを定義して `service://` で呼び出しできるようにカスタマイズすると仮定して、実装していきます

呼び出しイメージ

```php
$summationResource = $this->resource->get('service://self/summation', ['a' => 10, 'b' => 7]);
```

## MySchemeCollectionProviderを実装する

スキームを管理しているのは `SchemeCollection` で

`SchemeCollection`　にスキームを設定しているのは `SchemeCollectionProvider` です

`BEAR\Resource\Module\BEAR\Resource\Module\SchemeCollectionProvider`を参考に

カスタマイズした `MySchemeCollectionProvider` を実装します

`service://` を実現するために下記の1行を追加します

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

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

    /**
     * @param string $appName
     *
     * @AppName("appName")
     */
    public function __construct($appName, InjectorInterface $injector)
    {
        $this->appName = $appName;
        $this->injector = $injector;
    }

    /**
     * Return instance
     *
     * @return SchemeCollection
     */
    public function get()
    {
        $schemeCollection = new SchemeCollection;
        $pageAdapter = new AppAdapter($this->injector, $this->appName);
+       $appAdapter = new AppAdapter($this->injector, $this->appName);
        $serviceAdapter = new AppAdapter($this->injector, $this->appName);
        $schemeCollection->scheme('page')->host('self')->toAdapter($pageAdapter);
        $schemeCollection->scheme('app')->host('self')->toAdapter($appAdapter);
+       $schemeCollection->scheme('service')->host('self')->toAdapter($serviceAdapter);

        return $schemeCollection;
    }
}
```

## 実装したMySchemeCollectionProviderをSchemeCollectionInterfaceに束縛する

`BEAR.Sunday(Ray.Di)` では先の束縛が優先されるので

デフォルトの束縛が行われている `PackageModule` の `install` より先に

実装した `MySchemeCollectionProvider` を `SchemeCollectionInterface` に束縛します

```diff
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $appDir = $this->appMeta->appDir;
        require_once $appDir . '/env.php';

+        $this->bind(SchemeCollectionInterface::class)->toProvider(SchemeCollectionProvider::class);

        $this->install(new PackageModule);
    }
```

## serviceスキームを利用する

これでserviceスキームが定義でき、利用できるようになりました

下記のように `service://self/summation` と呼び出せます

CLI等で実行してみてください

```php
    public function onGet(string $name = 'BEAR.Sunday') : ResourceObject
    {
        $weekdayResource = $this->resource->get('app://self/weekday', ['year' => '2001', 'month' => '1', 'day' => '1']);
        $summationResource = $this->resource->get('service://self/summation', ['a' => 10, 'b' => 7]);

        $this->body = [
            'greeting' => 'Hello ' . $name,
            'weekday' => $weekdayResource->body,
            'summationResource' => $summationResource->body,
        ];

        return $this;
    }
```

## まとめ

スキームの追加そのものはとっても簡単です！

これで規模が大きくなっても問題なし！役割ごとに分割して、肥大化を防ぎましょう！

Lets BEAR.Sunday！

# なぜSchemeCollectionProviderをカスタマイズすればいいのか

ここからは `Resource` の処理を追いながら、なぜ `SchemeCollectionProvider` をカスタマイズすればいいのか解説します

## Resourceの実行
```php
/**
 * @var ResourceInterface
 */
protected $resource;

$ro = $this->resource->get('app://self/weekday', ['year' => '2001', 'month' => '1', 'day' => '1']);
```

この処理が実行されたときに、どういう順番でBEAR.Sundayが処理していくのか追っていきます

## BEAR\Resource\Resource
`$this->resource` は `ResourceInterface` で実態は `BEAR\Resource\Resource` であり、このクラスの `get` メソッドが呼び出されます

```php
    public function get(string $uri, array $query = []) : ResourceObject
    {
        $this->method = Request::GET;

        return $this->uri(new Uri($uri))($query);
    }
```

自身の `uri` メソッドを呼び出して、その結果を実行しているので、`uri` メソッドを確認します

```php
    /**
     * {@inheritdoc}
     */
    public function uri($uri) : RequestInterface
    {
        if (is_string($uri)) {
            $uri = new Uri($uri);
        }
        $uri->method = $this->method;
        $ro = $this->newInstance($uri);
        $ro->uri = $uri;
        $this->request = new Request($this->invoker, $ro, $uri->method, $uri->query, [], $this->linker);
        $this->method = 'get';

        return $this->request;
    }
```

`Request` を生成して、返しています

ということは `get` メソッドでは `Request` の `__invoke` が実行されることがわかりました

## BEAR\Resource\Request,  BEAR\Resource\AbstractRequest

```php
    /**
     * {@inheritdoc}
     */
    public function __invoke(array $query = null) : ResourceObject
    {
        if (is_array($query)) {
            $this->query = array_merge($this->query, $query);
        }
        $this->resourceObject->uri->query = $this->query;
        if ($this->links && $this->linker instanceof LinkerInterface) {
            return $this->linker->invoke($this);
        }

        return $this->invoker->invoke($this);
    }
```

`__invoke` が実装されているのは `AbstractRequest` で

実際の処理は `$invoker` の `invoke` メソッドが担当しているので、そちらを見にいきます

## BEAR\Resource\Invoker
```php
    /**
     * {@inheritdoc}
     */
    public function invoke(AbstractRequest $request) : ResourceObject
    {
        $onMethod = 'on' . ucfirst($request->method);
        if (! method_exists($request->resourceObject, $onMethod) === true) {
            return $this->methodNoExists($request);
        }
        $params = $this->params->getParameters([$request->resourceObject, $onMethod], $request->query);
        $response = call_user_func_array([$request->resourceObject, $onMethod], $params);
        if (! $response instanceof ResourceObject) {
            $request->resourceObject->body = $response;
            $response = $request->resourceObject;
        }

        return $response;
    }
```

`$response = call_user_func_array([$request->resourceObject, $onMethod], $params);`

`$request` の `resourceObject` の対応しているメソッドを実行していることがわかりました

この `resourceObject` は `Request` を生成するときにコンストラクタで渡されています

## BEAR\Resource\Resource 

`BEAR\Resource\Resource` の `uri` メソッドに戻ってみると、自身の `newInstance` メソッドで生成されていることがわかります

```php
$ro = $this->newInstance($uri);
```

```php
    /**
     * {@inheritdoc}
     */
    public function newInstance($uri) : ResourceObject
    {
        return $this->factory->newInstance($uri);
    }
```

実際は `factory` の `newInstance` メソッドで生成されているので、`Factory` を確認しにいきます

## BEAR\Resource\Factory
```php
    /**
     * Resource adapter biding config
     *
     * @var SchemeCollectionInterface
     */
    private $scheme;

    /**
     * {@inheritdoc}
     *
     * @throws \BEAR\Resource\Exception\UriException
     */
    public function newInstance($uri) : ResourceObject
    {
        if (is_string($uri)) {
            $uri = new Uri($uri);
        }
        $adapter = $this->scheme->getAdapter($uri);

        return $adapter->get($uri);
    }
```

`SchemeCollectionInterface` を実装したクラス経由で、`Adapter` をうけとり、`Adapter` が `ResourceObject` を生成していることがわかりました

`SchemeCollectionInterface` の実態を確認しにいきましょう

### BEAR\Resource\Module\ResourceClientModule

`ResourceClientModule` を確認すると

`SchemeCollectionInterface` に `SchemeCollectionProvider` を束縛されていることがわかりました

```php
$this->bind(SchemeCollectionInterface::class)->toProvider(SchemeCollectionProvider::class);
```

### BEAR\Resource\Module\SchemeCollectionProvider
```php
    /**
     * Return instance
     *
     * @return SchemeCollection
     */
    public function get()
    {
        $schemeCollection = new SchemeCollection;
        $pageAdapter = new AppAdapter($this->injector, $this->appName);
        $appAdapter = new AppAdapter($this->injector, $this->appName);
        $schemeCollection->scheme('page')->host('self')->toAdapter($pageAdapter);
        $schemeCollection->scheme('app')->host('self')->toAdapter($appAdapter);

        return $schemeCollection;
    }
```

`SchemeCollectionProvider` が `SchemeCollection` に 各種 `Adapter` を設定しています

### BEAR\Resource\SchemeCollection
```php
final class SchemeCollection implements SchemeCollectionInterface
{
    /**
     * Scheme
     *
     * @var string
     */
    private $scheme = '';

    /**
     * Application name
     *
     * @var string
     */
    private $app = '';

    /**
     * @var AdapterInterface[]
     */
    private $collection = [];

    /**
     * {@inheritdoc}
     */
    public function scheme(string $scheme) : SchemeCollectionInterface
    {
        $this->scheme = $scheme;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function host(string $host) : SchemeCollectionInterface
    {
        $this->app = $host;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function toAdapter(AdapterInterface $adapter) : SchemeCollectionInterface
    {
        $this->collection[$this->scheme . '://' . $this->app] = $adapter;

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @throws SchemeException
     */
    public function getAdapter(AbstractUri $uri) : AdapterInterface
    {
        $schemeIndex = $uri->scheme . '://' . $uri->host;
        if (! array_key_exists($schemeIndex, $this->collection)) {
            throw new SchemeException($uri->scheme . '://' . $uri->host);
        }

        return $this->collection[$schemeIndex];
    }
}
```

`scheme` と `host` を `index` として、`adapter` を管理していることがわかりました

利用している `Adapter` は `AppAdapter` なので中身を確認しておきましょう

### BEAR\Resource\Module\AppAdapter

```php
    /**
     * {@inheritdoc}
     *
     * @throws ResourceNotFoundException
     * @throws \Ray\Di\Exception\Unbound
     */
    public function get(AbstractUri $uri) : ResourceObject
    {
        if (substr($uri->path, -1) === '/') {
            $uri->path .= 'index';
        }
        $path = str_replace('-', '', ucwords($uri->path, '/-'));
        $class = sprintf('%s%s\Resource\%s', $this->namespace, $this->path, str_replace('/', '\\', ucwords($uri->scheme) . $path));
        try {
            $instance = $this->injector->getInstance($class);
        } catch (Unbound $e) {
            throw $this->getNotFound($uri, $e, $class);
        }

        return $instance;
    }
```

`Ray.Di` の `Injector` で各 `Resource` のインスタンスを取得していることがわかりました

ここまで追えれば `SchemeCollectionProvider` をカスタマイズして、

`SchemeCollection` に `service` スキーマを追加してやれば、やりたいことが実現できることがわかります！


### 備考

記事ではどういう束縛が設定されていて、結果的になにが注入されているのかという部分の確認方法は触れませんでした

束縛を確認するには `var/tmp/{context}` フォルダを確認するとコンパイルされたファイルが並ぶので、各Interface名のファイルをみれば、どういう束縛が行われているか確認できます

https://bearsunday.github.io/manuals/1.0/ja/di.html#デバック