Laravel 9 から Google Drive API で共有ドライブを操作する

:::note
Laravel Advent Calendar 2023 9日目の記事です🎄
:::

## はじめに
みなさん、お久しぶりです！リバネス開発チームのトミー（@tomyf）です！

今回は Google Drive の共有ドライブをWebサービスから管理したいという要望があったので、動かす所まで検証しました。

システムの概要としては、複数のGoogleドキュメントをまとめて、冊子にする工程を補助するアプリケーションとなっています。
APIを使って共有ドライブを操作する目的は、フォルダやファイルの構成と名称を執筆者の裁量に任せず、システム側で制御して管理しやすくするためです。

サービスアカウントの認証情報をJSONファイルで管理している記事が多数見受けられますが、今回は Heroku でホスティングすることを想定しているため、JSONファイルではなく環境変数で管理するようにしています。

## 環境
- Docker v24.0.6
- Laravel v9.25.1
- PHP v8.0.2
- google/apiclient v2.15.1

## Google Cloud 側の設定

まずは Google Cloud のコンソールから作業を行います。

[Google Cloud コンソール](https://console.cloud.google.com)

### 新しいプロジェクトを作成する

Webサービスで利用する用のプロジェクトを新しく作成します。

![スクリーンショット 2023-12-08 16.16.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304335/6a3c67ee-a9a2-bc83-3f47-200df2e97568.png)

### サービスアカウントを追加する

クイックアクセスから「APIとサービス」に遷移し、「認証情報」を開きます。

![スクリーンショット 2023-12-08 16.16.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304335/016c1f00-6cba-f0d7-05b9-bf545a921d1f.png)

![スクリーンショット 2023-12-08 16.17.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304335/03ec7a80-9d1c-d189-aeca-20366b1fb501.png)

「認証情報を作成」から「サービスアカウント」を選択し、手順に沿ってサービスアカウントを作成します。

![スクリーンショット 2023-12-08 16.18.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304335/5987a2df-c880-27ff-caca-da953b67f43f.png)

サービスアカウントの作成が完了すると、秘密鍵の情報を含むJSONファイルが生成されるので、ダウンロードしてください。

__※ サービスアカウントは、不正使用されるとセキュリティ上のリスクになる可能性がありますので、取り扱いに気をつけてください。今回のシステムではJSONファイルをそのまま格納せずに、環境変数で管理するように工夫をしています。__

### Google Drive API の権限を有効にする

このままでは作成したサービスアカウントが Google Drive API を利用することができないため、権限を有効にする必要があります。

「有効なAPIとサービス」を開き、「APIとサービスの有効化」に遷移してください。

![スクリーンショット 2023-12-08 16.26.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304335/26074de4-e511-9b19-7e46-a647cc68e467.png)

APIライブラリの一覧から「Google Drive API」を探して有効にします。

![スクリーンショット 2023-12-08 16.28.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304335/633c7136-18a0-edc0-2c2e-ce2af65412c7.png)

## Laravel 側の実装

ここから Laravel の実装に入っていきます。

Google が紹介しているライブラリの一覧がありますので、

https://developers.google.com/api-client-library

こちらのライブラリを使用させていただきます。

https://github.com/googleapis/google-api-php-client



### Google API PHP Client のインストール

Laravel Sail コマンドが使えるので、これでインストールしていきます。

```sh
sail composer require google/apiclient
```

```sh
Using version ^2.15 for google/apiclient
./composer.json has been updated
Running composer update google/apiclient
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - google/apiclient[v2.15.0, ..., 2.x-dev] require monolog/monolog ^2.9||^3.0 -> found monolog/monolog[dev-main, 2.9.0, 2.9.1, 2.9.2, 2.x-dev, 3.0.0-RC1, ..., 3.x-dev (alias of dev-main)] but the package is fixed to 2.8.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
    - Root composer.json requires google/apiclient ^2.15 -> satisfiable by google/apiclient[v2.15.0, v2.15.1, 2.x-dev].

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.
You can also try re-running composer require with an explicit version constraint, e.g. "composer require google/apiclient:*" to figure out if any version is installable, or "composer require google/apiclient:^2.1" if you know which you need.

Installation failed, reverting ./composer.json and ./composer.lock to their original content.
```

どうやらすでに monolog/monolog がインストールされており、バージョンが固定されていたため、エラーが発生しました。
エラー文に記載がある通り `--with-all-dependencies` をつければ回避できます。

```sh
sail composer require google/apiclient --with-all-dependencies
```

これで問題なくインストールできました。

### Google Drive API を利用する準備

Google API PHP Client の Readme では 環境変数 `GOOGLE_APPLICATION_CREDENTIALS` にダウンロードしたJSONファイルのパスを設定してください。とありますが、今回はJSONファイルを直接扱いたくないので、以下の方法で設定を行います。

```php
use Google\Client;

$client = new Client;
$client->setAuthConfig([
    'type' => 'service_account',
    'private_key' => config('services.google.private_key'),
    'client_email' => config('services.google.client_email'),
    'client_id' => config('services.google.client_id'),
]);
$client->setScopes([Drive::DRIVE]);
```

```php:config/services.php
<?php

return [
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_email' => env('GOOGLE_CLIENT_EMAIL'),
        'private_key' => env('GOOGLE_PRIVATE_KEY'),
        'drive_id' => env('GOOGLE_DRIVE_ID'),
    ],
];
```

実はAPI利用に必要な情報は以下の3つなので、これをJSONファイルから抜き取って環境変数に設定します。
- client_id
- client_email
- private_key

また、今回のWebアプリケーションでは、一つの共有ドライブをベースに操作して行くため、予め共有ドライブを作成してサービスアカウントに編集権限を割り振ります。

### Google Drive API で共有ドライブの操作

共有ドライブを扱う場合には、オプションの指定が必要になるので注意してください！

共有ドライブにファイルを作成する場合

```php
use Google\Client;
use Google\Service\Drive;
use Google\Service\Drive\DriveFile;

$client = new Client;
$client->setAuthConfig([
    'type' => 'service_account',
    'private_key' => config('services.google.private_key'),
    'client_email' => config('services.google.client_email'),
    'client_id' => config('services.google.client_id'),
]);
$client->setScopes([Drive::DRIVE]);
$driveId = config('services.google.drive_id');
$drive = new Drive($client);

// ファイル作成
$document = $drive->files->create(new DriveFile([
    'name' => 'test',
    'mimeType' => 'application/vnd.google-apps.document', // ドキュメント
    'parents' => [$driveId],
]),[
    'supportsAllDrives' => true, // 共有ドライブを対象にする場合は、このオプションが必須
]);
$documentId = $document->id;
```

共有ドライブにフォルダを作成する場合

```php
use Google\Client;
use Google\Service\Drive;
use Google\Service\Drive\DriveFile;

$client = new Client;
$client->setAuthConfig([
    'type' => 'service_account',
    'private_key' => config('services.google.private_key'),
    'client_email' => config('services.google.client_email'),
    'client_id' => config('services.google.client_id'),
]);
$client->setScopes([Drive::DRIVE]);
$driveId = config('services.google.drive_id');
$drive = new Drive($client);

// フォルダ作成
$folder = $drive->files->create(new DriveFile([
    'name' => 'test',
    'mimeType' => 'application/vnd.google-apps.folder', // フォルダ
    'parents' => [$driveId],
]),[
    'supportsAllDrives' => true, // 共有ドライブを対象にする場合は、このオプションが必須
]);
$folderId = $folder->id;
```

共有ドライブ直下にあるフォルダとファイルの一覧を取得する場合

```php
use Google\Client;
use Google\Service\Drive;
use Google\Service\Drive\DriveFile;

$client = new Client;
$client->setAuthConfig([
    'type' => 'service_account',
    'private_key' => config('services.google.private_key'),
    'client_email' => config('services.google.client_email'),
    'client_id' => config('services.google.client_id'),
]);
$client->setScopes([Drive::DRIVE]);
$driveId = config('services.google.drive_id');
$drive = new Drive($client);

// ファイルとフォルダの一覧取得
$list = $drive->files->listFiles([
    'q' => "'{$driveId}' in parents",
    'includeItemsFromAllDrives' => true, // 共有ドライブを対象にする場合は、このオプションが必須
    'supportsAllDrives' => true, // 共有ドライブを対象にする場合は、このオプションが必須
]);
```

共有ドライブ内にある全てのフォルダとファイルの一覧を取得する場合

```php
use Google\Client;
use Google\Service\Drive;
use Google\Service\Drive\DriveFile;

$client = new Client;
$client->setAuthConfig([
    'type' => 'service_account',
    'private_key' => config('services.google.private_key'),
    'client_email' => config('services.google.client_email'),
    'client_id' => config('services.google.client_id'),
]);
$client->setScopes([Drive::DRIVE]);
$driveId = config('services.google.drive_id');
$drive = new Drive($client);

// ファイルとフォルダの一覧取得
$list = $drive->files->listFiles([
    'corpora' => 'drive',
    'driveId' => $driveId,
    'includeItemsFromAllDrives' => true, // 共有ドライブを対象にする場合は、このオプションが必須
    'supportsAllDrives' => true, // 共有ドライブを対象にする場合は、このオプションが必須
]);
```

## おわりに

Google Drive API を Laravel (PHP) で実装した記事が少なく、少々手間取ったので備忘録として公開します！
実装で困っている方の助けになれば嬉しいです。