Laravelのカスタムファイルシステムを使用してCloudStorageを統合する

Laravelの[ファイルストレージ](https://readouble.com/laravel/8.x/ja/filesystem.html)を利用してCloudStorageを扱えるようにしてみます。Laravelに統合するにあたってゼロからつくるのではなくすでに[パッケージ](https://github.com/Superbalist/laravel-google-cloud-storage)が有志の方によって作られているのでこれを利用したいと思います。

## 導入

[README](https://github.com/Superbalist/laravel-google-cloud-storage/blob/master/README.md)に書かれている通りcomposeをつかってインストールします。インストールした後は、.envとfilesystem.phpに必要な情報を追加してください。


## エラーを回避する

バケットのアクセスコントロールが「均一」になっているとlaravel-google-cloud-storageを使ってオブジェクトをアップロードすると失敗します。

```
Cannot insert legacy ACL for an object when uniform bucket-level access is enabled
```

おそらく原因はgithubのissueのコメントに書かれていることなのではないかと思います。
https://github.com/Superbalist/laravel-google-cloud-storage/issues/80#issuecomment-616557477

なので、このままではバケットのアクセスコントロールが「均一」になっているとこのファイルシステムでのアプロード機能が使えなくなるので、少し手を加えます。

### ServiceProviderを作成する

エラーが起きる原因は、laravel-google-cloud-storageがアダプターとして[flysystem-google-cloud-storage](https://github.com/Superbalist/flysystem-google-cloud-storage)を利用していることにあります。エラーを回避する方法が[プルリクエスト](https://github.com/Superbalist/flysystem-google-cloud-storage/pull/114)で提案されています。要は、GoogleStorageAdapterクラスのgetOptionsFromConfigメソッドをいじりたいので、GoogleStorageAdapterクラスを拡張したクラスを作成して、getOptionsFromConfigメソッドをオーバーライドしてアクセスコントロールが「均一」のバケットにも対応できるように処理を修正して、このクラス自身を返すというものです。これをLaravelのカスタムファイルシステムで実現させるために、独自のサービスクラスを作ります。作成したサービスプロバイダーの中身は下記になります。

```php:app/providers/GoogleCloudStorageServiceProvider.php
<?php

namespace App\Providers;

use Google\Cloud\Storage\StorageClient;
use Illuminate\Support\Arr;
use Superbalist\LaravelGoogleCloudStorage\GoogleCloudStorageServiceProvider as GCSProvider;
use Superbalist\Flysystem\GoogleStorage\GoogleStorageAdapter;

class GoogleCloudStorageServiceProvider extends GCSProvider
{
    /**
     * Create a new StorageClient
     *
     * @param  mixed $config
     * @return \Google\Cloud\Storage\StorageClient
     */
    private function createClient($config)
    {
        $keyFile = Arr::get($config, 'key_file');
        if (is_string($keyFile)) {
            return new StorageClient([
                'projectId' => $config['project_id'],
                'keyFilePath' => $keyFile,
            ]);
        }

        if (! is_array($keyFile)) {
            $keyFile = [];
        }
        return new StorageClient([
            'projectId' => $config['project_id'],
            'keyFile' => array_merge(["project_id" => $config['project_id']], $keyFile)
        ]);
    }

    public function boot()
    {
        $factory = $this->app->make('filesystem');
        /* @var FilesystemManager $factory */
        $factory->extend('gcs', function ($app, $config) {
            $storageClient = $this->createClient($config);

            $bucket = $storageClient->bucket($config['bucket']);
            $pathPrefix = Arr::get($config, 'path_prefix');
            $storageApiUri = Arr::get($config, 'storage_api_uri');

            $adapter = $this->resolveAdapter($storageClient, $bucket, $pathPrefix, $storageApiUri);

            return $this->createFilesystem($adapter, $config);
        });
    }

    public function resolveAdapter($storageClient, $bucket, $pathPrefix = null, $storageApiUri = null)
    {
        return new class ($storageClient, $bucket, $pathPrefix, $storageApiUri) extends GoogleStorageAdapter {
            protected function getOptionsFromConfig(\League\Flysystem\Config $config)
            {
                $options = [];

                if (empty($this->bucket->info()['iamConfiguration']['uniformBucketLevelAccess']['enabled'])) {
                    if ($visibility = $config->get('visibility')) {
                        $options['predefinedAcl'] = $this->getPredefinedAclForVisibility($visibility);
                    } else {
                        $options['predefinedAcl'] = $this->getPredefinedAclForVisibility(AdapterInterface::VISIBILITY_PRIVATE);
                    }
                }

                if ($metadata = $config->get('metadata')) {
                    $options['metadata'] = $metadata;
                }

                return $options;
            }
        };
    }
}

```
createFilesystemに渡す$adapterに、GoogleStorageAdapterを拡張したクラスを渡しています。またこのサービスクラスもlaravel-google-cloud-storageのGoogleCloudStorageServiceProviderを拡張しています。
作成したサービスプロバイダーをapp.phpに登録します。

```php:config/app.php
...
'providers' => [
    ...
    App\Providers\GoogleCloudStorageServiceProvider::class,
],
...

```

必要な作業は終わりました。これでアクセスコントロールが「均一」のバケットに対してもオブジェクトがアップロードできるようになります。

```php
    public function upload(Request $request)
    {
        $file = $request->file('test');
        $disk = Storage::disk('gcs');
        $disk->put('test.txt', file_get_contents($file));
    }

```

Laravelでカスタムファイルシステムを作成する方法については、下記の記事で解説しているのでぜひ参考にしてみてください。

https://qiita.com/kiyo27/items/d26c5016a76f317c3da4