【PHP】App Engine なしでクライアントライブラリを使って Cloud Datastore を操作

# ケース
- Google Compute Engine とかでPHPプロジェクトが動いていて、ちょっとしたステータス管理のためにDBがほしい、など。
- ほんとは Google App Engine (SE) 使いたいけど、もろもろの事情でちょっと厳しい。

# 初期設定
- [Google Cloud Console](https://console.cloud.google.com/) にアクセス
- Cloud Datastore API を有効にする
- サービスアカウントを作成する
    - Role（役割）は `Datastore ユーザー` でOKです。
    - JSONのキーをダウンロードする。
- クライアントライブラリをダウンロードする（2018/07/30時点の最新はv2.2.2）
    - [ここから](https://github.com/google/google-api-php-client/releases) zipでダウンロードできます
    - composer でももちろんダウンロードできます。
        - `composer require google/apiclient:^2.0`
    - クライアントライブラリは適当なディレクトリに配置しますが、ここでは `Vendor/google-api-php-client` として配置したことにします。

ちなみに上記手順は [Google Analytics API のクイックスタート](https://developers.google.com/analytics/devguides/reporting/core/v4/quickstart/service-php) が参考になります。


# 使用準備
- `autoload.php` を読み込む

```php
<?php
require_once APP_DIR . '/Vendor/google-api-php-client/vendor/autoload.php';
```

- 認証
    - クライアントを作成するメソッドを準備する

```php
private function getGoogleClientByJson()
{
    $jsonPath = 'credential.json'; // ...（１）

    $scopes = [
        'https://www.googleapis.com/auth/datastore',
    ];

    $client = new Google_Client();
    $client->setAuthConfig($jsonPath);
    $client->setScopes($scopes);

    $tokenSer = './token.ser'; // ...（２）
    if (file_exists($tokenSer)) {
        $client->setAccessToken(unserialize(file_get_contents($tokenSer)));
    }
    if ($client->isAccessTokenExpired()) {
        $token = $client->fetchAccessTokenWithAssertion();
        file_put_contents($tokenSer, serialize($token));
    }

    return $client;
}
```
- （１）初期設定の手順でダウンロードした、キー情報が記述されているJSONファイル
    - 取り扱いに注意する。GitHubなどにアップロードしないように、環境変数や Cloud Storage などで管理。
- （２）アクセストークンを保持しておく
    - せっかく1時間有効なトークンなので、同階層に `token.ser` (serはシリアライズの意味) なるファイルをつくって保持しておく。


Datastoreサービスを初期化する

```php
$client = $this->getGoogleClientByJson();
$datastore = new Google_Service_Datastore($client);
```

# 使用方法
- 例として、以下のような構造とする
    - projectId: test-project (ご自身のプロジェクトIDに置き換えてください)
    - namespace: test_dev (_prodとかにすると開発と本番の環境分けて管理できる)
    - kind: TestKind
    - property
        - key: カスタム名（※）
        - status: ステータス（文字列）
        - groupId: グループID（文字列）
        - userId: ユーザID（文字列）

- **キーについて**
    - キーをカスタム名ではなく数値IDにすると、ユニークなIDが自動生成されます。
    - ここでは、例えばグループIDとユーザIDでユニークになるデータを想定して、`グループID-ユーザID` 形式で結合したものをキーにします。
        - キーでクエリ発行しやすくなるので便利。


## commit
- `upsert` を使えば、キーがあればupdate、なければinsertになります

```php
$projectId = 'test-project';
$namespace = 'test_dev';
$kind = 'TestKind';

$groupId = 'G01';
$userId = 'U01';
$name = $groupId . '-' . $userId; // キー

// クライアント取得
$client = $this->getGoogleClientByJson();
// Datastoreサービスの初期化
$datastore = new Google_Service_Datastore($client);

$mutation = new Google_Service_Datastore_Mutation([
    'upsert' => [
        'key' => [
            'path' => [
                [
                    'kind' => $kind,
                    'name' => $name, // キー（カスタム名）
                    // 'id' => $id, // 自動生成IDの場合はidにセット。name, id はどちらか一方をセットする
                ],
            ],
            'partitionId' => [
                'namespaceId' => $namespace,
            ],
        ],
        'properties' => [
            'status' => [
                'excludeFromIndexes' => true, // インデックスから除外
                'stringValue' => 'ON', // 例えば 'ON' というステータスを登録
                // 'integerValue' => 1, // statusが整数型の場合はintegerValueにセットすることになる
            ],
            'groupId' => [
                'stringValue' => $groupId,
            ],
            'userId' => [
                'stringValue' => $userId,
            ],
        ],
    ],
]);

$commitRequest = new Google_Service_Datastore_CommitRequest([
    'mode' => 'NON_TRANSACTIONAL',
    'mutations' => [$mutation],
]);

$result = $datastore->projects->commit($projectId, $commitRequest);

if (isset($result['mutationResults'])) {
    echo "success. upsert count: " . count($result['mutationResults']);
}
```

## lookup
- キーを使った検索

```php
$projectId = 'test-project';
$namespace = 'test_dev';
$kind = 'TestKind';

$groupId = 'G01';
$userId = 'U01';
$name = $groupId . '-' . $userId; // キー

$client = $this->getGoogleClientByJson();

$datastore = new Google_Service_Datastore($client);

$lookupRequest = new Google_Service_Datastore_LookupRequest();
$lookupRequest->setKeys([
    [
        'path' => [
            'kind' => $kind,
            'name' => $name,
        ],
        'partitionId' => [
            'namespaceId' => $namespace,
        ],
    ],
]);

$readOptions = new Google_Service_Datastore_ReadOptions([
    'readConsistency' => 'STRONG', // キーでの検索なのでSTRONGになるはずだけど、一応明示的に記述
]);
$lookupRequest->setReadOptions($readOptions);

$result = $datastore->projects->lookup($projectId, $lookupRequest);

if (isset($result['found'])) {
    foreach ($result['found'] as $entityResult) {
        $entity = $entityResult['entity'];
        $properties = $entity['properties'];

        $status = $properties['status']['stringValue'];
        // $status = $properties['status']['integerValue']; // statusを整数型にした場合、integerValueにセットされる
        echo 'status: ' . $status . "\n";
    }
}
```

- ちなみに以下のようなJSONレスポンスが返ってきます。

```json
{
  "found": [
    {
      "entity": {
        "key": {
          "partitionId": {
            "projectId": "test-project",
            "namespaceId": "test_dev"
          },
          "path": [
            {
              "kind": "TestKind",
              "name": "G01-U01"
            }
          ]
        },
        "properties": {
          "groupId": {
            "stringValue": "G01"
          },
          "status": {
            "stringValue": "ON"
          },
          "userId": {
            "stringValue": "U01"
          },
        }
      },
      "version": "1532942818114000"
    }
  ]
}
```

## delete
- キーを指定して削除

```php
$projectId = 'test-project';
$namespace = 'test_dev';
$kind = 'TestKind';

$groupId = 'G01';
$userId = 'U01';
$name = $groupId . '-' . $userId; // キー

$client = $this->getGoogleClientByJson();

$datastore = new Google_Service_Datastore($client);

$mutation = new Google_Service_Datastore_Mutation([
    'delete' => [
        'path' => [
            [
                'kind' => $kind,
                'name' => $name,
            ],
        ],
        'partitionId' => [
            "namespaceId" => $namespace,
        ],
    ],
]);

$commitRequest = new Google_Service_Datastore_CommitRequest([
    'mode' => 'NON_TRANSACTIONAL',
    'mutations' => [$mutation],
]);

$result = $datastore->projects->commit($projectId, $commitRequest);

if (isset($result['mutationResults'])) {
    echo "success. delete count: " . count($result['mutationResults']);
}
```

## runQuery
### filter

```php
$projectId = 'test-project';
$namespace = 'test_dev';
$kind = 'TestKind';

$client = $this->getGoogleClientByJson();

$datastore = new Google_Service_Datastore($client);

$query = new Google_Service_Datastore_Query([
    'kind' => [
        [
        'name' => $kind,
        ],
    ],
    'filter' => [
        'propertyFilter' => [
            'op' => 'EQUAL',
            'property' => [
                'name' => 'userId',
            ],
            'value' => [
                'stringValue' => 'U01',
            ],
        ],
    ],
]);

$runQueryRequest = new Google_Service_Datastore_RunQueryRequest([
    'query' => $query,
    'partitionId' => [
        'namespaceId' => $namespace,
    ]
]);

$result = $datastore->projects->runQuery($projectId, $runQueryRequest);

if (isset($result['batch']['entityResults'])) {
    foreach ($result['batch']['entityResults'] as $entityResult) {
        $entity = $entityResult['entity'];
        $key = $entity['key'];
        $properties = $entity['properties'];

        echo 'name(key): ' . $key['path'][0]['name'] . "\n"; // 親を持っているとpathが複数になる（親が0番目）
        echo 'status: ' . $properties['status']['stringValue'] . "\n";
        echo 'groupId: ' . $properties['groupId']['stringValue'] . "\n";
        echo 'userId: ' . $properties['userId']['stringValue'] . "\n";
    }
}
```

### gql

```php
$projectId = 'test-project';
$namespace = 'test_dev';
$kind = 'TestKind';

$client = $this->getGoogleClientByJson();

$datastore = new Google_Service_Datastore($client);

$query = "SELECT * FROM " . $kind . " WHERE groupId = 'G01'";

$runQueryRequest = new Google_Service_Datastore_RunQueryRequest([
    'gqlQuery' => [
        'queryString' => $query,
        'allowLiterals' => true,  // WHERE hoge = 'foo' みたいなクエリの書き方にしたいときにtrueにする
    ],
    'partitionId' => [
        'namespaceId' => $namespace,
    ],
]);

$result = $datastore->projects->runQuery($projectId, $runQueryRequest);

if (isset($result['batch']['entityResults'])) {
    foreach ($result['batch']['entityResults'] as $entityResult) {
        $entity = $entityResult['entity'];
        $key = $entity['key'];
        $properties = $entity['properties'];

        echo 'name(key): ' . $key['path'][0]['name'] . "\n";
        echo 'status: ' . $properties['status']['stringValue'] . "\n";
        echo 'groupId: ' . $properties['groupId']['stringValue'] . "\n";
        echo 'userId: ' . $properties['userId']['stringValue'] . "\n";
    }
}
```

## 実装方法について
- 上記のサンプルはほどよく連想配列とクラスのインスタンスを混ぜていますが、すべて連想配列でもいけると思います。
    - `～Request` 系のクラスはいろいろ必須パラメータがあるので、そこはインスタンス化しないと厳しい
- 実際にライブラリを覗くと、すべてクラスベースでsetter getter が準備されているので、連想配列をいっさい使わない書き方もできます。Javaっぽい書き方ですね。
- 連想配列を使うほうがPHPっぽいですが、見やすくなるようにほどよくクラスを使うといいかな、と思いました。

以下は、上記のgqlのサンプルを連想配列使わずに書いた例です。

```php
$projectId = 'test-project';
$namespace = 'test_dev';
$kind = 'TestKind';

$client = $this->getGoogleClientByJson();

$datastore = new Google_Service_Datastore($client);

$query = "SELECT * FROM " . $kind . " WHERE groupId = 'G01'";

$gql = new Google_Service_Datastore_GqlQuery();
$gql->setQueryString($query);
$gql->setAllowLiterals(true);

$partitionId = new Google_Service_Datastore_PartitionId();
$partitionId->setNamespaceId($namespace);

$runQueryRequest = new Google_Service_Datastore_RunQueryRequest();
$runQueryRequest->setPartitionId($partitionId);
$runQueryRequest->setGqlQuery($gql);

$result = $datastore->projects->runQuery($projectId, $runQueryRequest);
```