ConoHa のオブジェクトストレージを php-opencloud で操作する (2017年版)

# 概要

[ConoHa](https://www.conoha.jp/) の[オブジェクトストレージ](https://www.conoha.jp/objectstorage/?btn_id=header_objectstorage)を PHP から扱う方法を調べていたのですが、古い情報しか見付からず、動かすだけでも苦労したので、最低限こうすれば動いたよというのをここに書いておこうと思います。

* [php-opencloudでConoHa APIを使う - Qiita](https://qiita.com/hironobu_s/items/01c97f3e637f80ad7899)

こちらの記事によると、ConoHa のオブジェクトストレージは OpenStack で構成されていて、それを PHP から扱うには [php-opencloud](http://php-opencloud.com/) を使うのがおすすめだそうです。
ただ、この記事が書かれてから 2 年くらい経っていて、php-opencloud の使い方もだいぶ変わっていました。


# 新しくなった php-opencloud の使い方

## OpenStack のインスタンス作成

元記事ではまず、`OpenCloud\OpenStack` のインスタンスを作成していますが、ネームスペースが変更になったようで、代わりに `OpenStack\OpenStack` を使います。
インスタンスの作り方も変わっていて、全てのパラメータをひとつの配列で渡す形になっていました。

```php:vendor/php-opencloud/openstack/src/OpenStack.php
    /**
     * @param array    $options User-defined options
     *
     * $options['username']         = (string)            Your OpenStack username        [REQUIRED]
     *         ['password']         = (string)            Your OpenStack password        [REQUIRED]
     *         ['tenantId']         = (string)            Your tenant ID                 [REQUIRED if tenantName omitted]
     *         ['tenantName']       = (string)            Your tenant name               [REQUIRED if tenantId omitted]
     *         ['authUrl']          = (string)            The Keystone URL               [REQUIRED]
     *         ['debugLog']         = (bool)              Whether to enable HTTP logging [OPTIONAL]
     *         ['logger']           = (LoggerInterface)   Must set if debugLog is true   [OPTIONAL]
     *         ['messageFormatter'] = (MessageFormatter)  Must set if debugLog is true   [OPTIONAL]
     *         ['requestOptions']   = (array)             Guzzle Http request options    [OPTIONAL]
     *
     * @param Builder $builder
     */
    public function __construct(array $options = [], Builder $builder = null)
```

ConoHa の場合だと、次のようにしてインスタンスを作成します。

```php
$options = [
    'authUrl'    => 'https://identity.tyo1.conoha.io/v2.0',  // Identity Service の URL
    'username'   => 'gncu****',  // API ユーザーのユーザー名
    'password'   => '****',      // API ユーザーのパスワード
    'tenantName' => 'gnct****',  // テナント情報のテナント名
    'region'     => 'tyo1',      // エンドポイントのリージョン
];

$client = new OpenStack\OpenStack($options);
```

この後、認証を行うための Identity Service や、オブジェクトストレージを扱うための Object Store Service を扱うためのインスタンスを作成するのですが、その際にもここで渡したオプションが利用されます。
コンストラクタのコメントには説明のない `region` を設定していますが、これは後に各サービスで使うためのものです。


## 認証

元記事では `OpenCloud\OpenStack` クラスの `authenticate()` メソッドを呼んでいますが、新しい `OpenStack\OpenStack` にそんなものはありません。
OpenStack での認証は Identity Service が担当しているのですが、サービスごとにクラスを分けたようです。

ConoHa が用意しているのは Identity Service v2 なので、ここでは `OpenStack\Identity\v2\Service` を使います。
`OpenStack\Identity\v2\Service` のインスタンスは、`OpenStack\OpenStack` の `identityV2()` というメソッドを使って取得します。

```php:vendor/php-opencloud/openstack/src/OpenStack.php
    /**
     * Creates a new Identity v2 service.
     *
     * @param array $options Options that will be used in configuring the service.
     *
     * @return \OpenStack\Identity\v2\Service
     */
    public function identityV2(array $options = []): \OpenStack\Identity\v2\Service
```

引数に先ほどと同じくオプションを取りますが、先ほど設定したオプションが引き継がれるので、ここでは何も設定しません。

```php
$identityService = $client->identityV2();
```

この `OpenStack\Identity\v2\Service` が `authenticate()` などの認証のためのメソッドを持っているようです。
元記事では自分で `authenticate()` を呼んでいましたが、他のサービスを使用する際に自動的に呼ばれるようだったので、ここでは省略します。


## サービスを使う

元記事では `CloudStack\OpenStack` の `getCatalog()` メソッドを使ってサービスの一覧を取得していますが、これに対応するメソッドは見付けられませんでした。
サービスの一覧は `POST /v2.0/tokens` という [API](https://www.conoha.jp/docs/identity-post_tokens.html) で取得するようなので、あるとしたら `OpenStack\Identity\v2\Service` クラスにありそうなのですが、`authenticate()` の中で直接この API を呼んでいるだけで、結果を返すようなメソッドは見付けられませんでした :cry:

```php:vendor/php-opencloud/openstack/src/Identity/v2/Service.php
    public function authenticate(array $options = []): array
    {
        $definition = $this->api->postToken();

        $response = $this->execute($definition, array_intersect_key($options, $definition['params']));

        $token = $this->model(Token::class, $response);

        $serviceUrl = $this->model(Catalog::class, $response)->getServiceUrl(
            $options['catalogName'],
            $options['catalogType'],
            $options['region'],
            $options['urlType']
        );

        return [$token, $serviceUrl];
    }
```

## Object Store Service を使う

元記事では Compute Service を使っていましたが、今回はオブジェクトストレージを使うのが目的なので、Object Store Service を使います。

先ほどと同じように、Object Store Service を扱うための `OpenStack\ObjectStore\v1\Service` クラスのインスタンスを取得するのですが、単に `objectStoreV1()` を呼んだだけではエラーになってしまいます。

```php:NG
$objectStoreService = $client->objectStoreV1();
```

こうすると、`vendor/php-opencloud/openstack/src/Identity/v3/Models/Token.php:L104` で

```:InvalidArgumentException
Either a user or token must be provided.
```

というメッセージの `InvalidArgumentException` が投げられてしまいます。どうやら v2 ではなく v3 の Identity Service が使われてしまっているようです。
そこで、どの Identity Service を使うかをオプションで指定します。

```php:NG
$objectStoreService = $client->objectStoreV1([
    'identityService' => $identityService,
]);
```

ここで指定したオプションは、初めに `OpenStack\OpenStack` のインスタンスを作ったときのオプションとマージされて使用されます。

Identity Service を指定すると、認証が必要な際に、自動的に指定した Identity Service を使って認証を行ってくれるようです。
前述した `authenticate()` メソッドもここで呼ばれていました。

しかし、今度は `vendor/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php:L51` で

```:RuntimeException
Endpoint URL could not be found in the catalog for this service.
Name: swift
Type: object-store
Region: tyo1
URL type: publicURL
```

という内容の `RuntimeException` が投げられました。
これは `objectStoreV1()` メソッドでデフォルト値として指定されているカタログ名が、ConoHa が提供している `Object Storage Service` ではなく `swift` になっているために発生しています。

```php:vendor/php-opencloud/openstack/src/OpenStack.php
    /**
     * Creates a new Object Store v1 service.
     *
     * @param array $options Options that will be used in configuring the service.
     *
     * @return \OpenStack\ObjectStore\v1\Service
     */
    public function objectStoreV1(array $options = []): \OpenStack\ObjectStore\v1\Service
    {
        $defaults = ['catalogName' => 'swift', 'catalogType' => 'object-store'];
        return $this->builder->createService('ObjectStore\\v1', array_merge($defaults, $options));
    }
```

これもオプションで指定できるので、指定します。

```php:OK
$objectStoreService = $client->objectStoreV1([
    'identityService' => $identityService,
    'catalogName' => 'Object Storage Service',
]);
```

これで Object Store Service が使えるようになりました。

Object Store Service については[公式ドキュメント](http://docs.php-opencloud.com/en/latest/services/object-store/index.html)に説明が載っているので、あとは悩まなくても使うことができそうです。


# 終わりに

今までの内容をまとめるとこんな感じになります。
Identity Service は `OpenStack\OpenStack` のインスタンスを作成する際にも指定できるので、もう少しスマートに書けるかもしれませんが、ひとまずこれで ConoHa のオブジェクトストレージにアクセスすることができました。

```php
<?php

$options = [
    'authUrl'    => 'https://identity.tyo1.conoha.io/v2.0',  // Identity Service の URL
    'username'   => 'gncu****',  // API ユーザーのユーザー名
    'password'   => '****',      // API ユーザーのパスワード
    'tenantName' => 'gnct****',  // テナント情報のテナント名
    'region'     => 'tyo1',      // エンドポイントのリージョン
];

$client = new OpenStack\OpenStack($options);

$identityService = $client->identityV2();

$objectStoreService = $client->objectStoreV1([
    'identityService' => $identityService,
    'catalogName' => 'Object Storage Service',
]);


// コンテナ一覧の取得
$containers = $objectStoreService->listContainers();

foreach ($containers as $container) {
    var_dump($container);
}
```