Laravel + GCS でstorage管理していたファイルをCGSで管理するようにした

# はじめに

この記事は`Laravel`からアップロードしたファイルを`GCS`に保存するようにした方法をまとめる

# やり方

## 事前準備:GCPのプロジェクト・バケットの作成

以下の記事を参考にしました🙇‍♂️

https://blog.capilano-fw.com/?p=3359

## `.env`に登録したバケット名を入力する

```text:.env
GCS_BUCKET_NAME="hoge-bucket"
```

## `.env`の値を`app.php`から参照する

```php:src/laravel/config/app.php
'gcp_bucket_name' => env('GCS_BUCKET_NAME', 'hoge-bucket'),
```

## GCSを操作するパッケージをインストール

```bash
composer require google/cloud-storage
```

https://packagist.org/packages/google/cloud-storage

https://googleapis.github.io/google-cloud-php/#/

## google/cloud-storageをインスタンス化する

- 事前準備で作成したバケットの認証キーを`storage`直下のディレクトリに移動します


`Config('app.gcp_bucket_name');`:`app.php`からバケット名を参照します

`$url = file_get_contents()`:事前準備で作成したキーファイルを参照します

`new StorageClient([バケット名,サービスアカウントのキーファイル])`:

`$client->bucket();`:バケットを生成します

```php:src/laravel/app/Services/Utilities/UtilityService.php
<?php

namespace App\Services\Utilities;

use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\StorageClient;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use stdClass;

class UtilityService
{

    public Bucket $bucket;
    public array $bucketOptions;

    public function __construct()
    {
        $projectId = Config('app.google_project_bucket_name');
        $url = file_get_contents(storage_path('app/local-service-account.json', true));
        $client = new StorageClient([
            'projectId' => $projectId,
            'keyFile' => json_decode($url, true)
        ]);
        $this->bucket = $client->bucket($projectId);
        $this->bucketOptions = [
            'resumable' => true, // アップロードを再開可能に行うか
            'name' => '',
            'metadata' => [
                // 日本語化
                'contentLanguage' => 'ja'
            ]
        ];
    }

    /**
     * admin権限のアクセスチェック
     *
     * @return bool
     */
    public function isAccessByAdmin(): bool
    {
        return Gate::allows('admin');
    }

    /**
     * 仮登録GCS保存名を作成
     *
     * @param UploadedFile $file
     * @param string $dirName
     * @param string $uniqueKey
     * @param array $options
     * @return array
     */
    public function setBucketOptions(UploadedFile $file, string $dirName, string $uniqueKey, array $options): array
    {
        $extension = $file->extension();
        $fileName = "{$uniqueKey}_{$dirName}.{$extension}";

        // 保存ディレクトリ生成
        $options['name'] = "tmp/skill_sheet/{$uniqueKey}/{$dirName}/{$fileName}";
        return $options;
    }

    /**
     * サムネイル画像情報をクラスに詰める
     * @param $thumbnailFile
     * @param $targetId
     * @param $model
     * @param $thumbnailFileName
     * @param $thumbnailFileObject
     * @return stdClass
     */
    public function setThumbnailClass($thumbnailFile, $targetId, $model, $thumbnailFileName, $thumbnailFileObject): stdClass
    {
        $thumbnailClass = new stdClass();
        $thumbnailClass->original_name = is_null($thumbnailFile) && !is_null($targetId) ? $model->thumbnail_file_name : $thumbnailFile->getClientOriginalName();
        $thumbnailClass->name = is_null($thumbnailFile) && !is_null($targetId) ? $model->thumbnail_path : $thumbnailFileName;
        $thumbnailClass->single_url = is_null($thumbnailFile) && !is_null($targetId) ? $model->thumbnail_path : $thumbnailFileObject->signedUrl(time() + 3600);
        return $thumbnailClass;
    }
}
```

## 使い方

### `Controller`から必要な要素を`Service`に渡す

```php:src/laravel/app/Http/Controllers/Admin/SkillSheetController.php
class SkillSheetController extends Controller
{
    protected SkillSheetService $service;
    protected UtilityService $utilityService;

    public function __construct()
    {
        $this->service = new SkillSheetService();
        $this->utilityService = new UtilityService();
    }

 /**
     * 確認
     *
     * @param StoreSkillSheetRequest $request
     * @return View|Factory
     */
    public function confirm(StoreSkillSheetRequest $request)
    {
        $data = collect($request->input());

        // ファイル送信があれば
        if (!empty($request->file())) {
            //パスに指定するユニークな鍵tmp/skill_sheet/{ユニークな値}
            $uniqueKey = uniqid('', true);

            // 仮登録パス名をセッションで受け渡す
            $saveNames = $this->service->tmpStorageToGcsBucket($request, $uniqueKey);

            $collect = collect($saveNames);

            // バケット初期化しセッションからGCSに保存したファイル名を取得
            $bucket = $this->utilityService->bucket;
            $prImageName = $collect->get('pr_image');
            $prVideoName = $collect->get('pr_video');
            $otherDocumentName = $collect->get('other_document');

            // 署名付きアップロードURLを作成
            $prImageSignedPath = $prImageName && $bucket->object($prImageName)->exists() ? $bucket->object($prImageName)->signedUrl(new \DateTime('tomorrow')) : null;
            $prVideoSignedPath = $prVideoName ? $bucket->object($prVideoName)->signedUrl(new \DateTime('tomorrow')) : null;
            $otherDocumentSignedPath = $otherDocumentName ? $bucket->object($otherDocumentName)->signedUrl(new \DateTime('tomorrow')) : null;

            // 確認画面で使用する仮保存名と認可パスをセッションに保持
            $imagePath->pr_image_name = $prImageName;
            $imagePath->pr_video_name = $prVideoName;
            $imagePath->other_document_name = $otherDocumentName;
            $imagePath->pr_image_path = $prImageSignedPath;
            $imagePath->pr_video_path = $prVideoSignedPath;
            $imagePath->other_document_path = $otherDocumentSignedPath;
        }

        return view('admin.skill_sheet.conf', [
            'skillSheet' => $data,
            'imagePath' => $imagePath,
            'jobTypes' => $jobTypes,
            'partnerGroups' => $partnerGroups,
            'partner' => $partner
        ]);
    }
}
```

## 送信時にGCSに一時保存する

`tmp/images/一意な値`に保存するようにします

`images/一意な値・ユーザーID`に保存するようにします
セッションに一時保存したファイル名を保持している状態と仮定します

```php:src/laravel/app/Services/SkillSheet/SkillSheetService.php
protected UtilityService $utilityService;

class SkillSheetService
{
    protected UtilityService $utilityService;

    public function __construct()
    {
        $this->utilityService = new UtilityService();
    }

    /**
     * 詳細
     *
     * @param integer $id
     * @return SkillSheet
     */
    public function detail(int $id): SkillSheet
    {
        $skillSheet = SkillSheet::findOrFail($id);

        // バケット初期化
        $bucket = $this->utilityService->bucket;
        $prImagePath = $skillSheet->pr_image_path;
        $prVideoPath = $skillSheet->pr_video_path;
        $otherDocumentPath = $skillSheet->other_document_path;

        // 署名付きアップロードURLを作成
        $prImageSignedPath = $prImagePath && $bucket->object($prImagePath)->exists() ? $bucket->object($prImagePath)->signedUrl(new \DateTime('tomorrow')) : null;
        $prVideoSignedPath = $prVideoPath ? $bucket->object($prVideoPath)->signedUrl(new \DateTime('tomorrow')) : null;
        $otherDocumentSignedPath = $otherDocumentPath ? $bucket->object($otherDocumentPath)->signedUrl(new \DateTime('tomorrow')) : null;

        // 上書き
        $skillSheet->pr_image_path = $prImageSignedPath;
        $skillSheet->pr_video_path = $prVideoSignedPath;
        $skillSheet->other_document_path = $otherDocumentSignedPath;
        return $skillSheet;
    }
}

   /**
     * 登録
     *
     * @param Collection $storeData
     * @return void
     */
    public function store(Collection $storeData): void
    {
        DB::transaction(function () use (
            $storeData,
        ) {

            $loginUser = Auth::user()->name;

            $storeSkillSheet = array_merge(
                (new SkillSheet())->fill($storeData->toArray())->toArray(),
                ['create_user' => $loginUser],
                ['update_user' => $loginUser],
            );

            $skillSheetId = DB::table('skill_sheets')->insertGetId($storeSkillSheet);

            $target = SkillSheet::findOrFail($skillSheetId);

            $updSkillSheet = [];

            // 画像が送信された場合は仮登録を本番に差し替え
            if (Session::has('saveNames')) {
                $updSkillSheet = $this->storageToGcsBucket($skillSheetId, $target, $updSkillSheet);
            }

            $target->fill($updSkillSheet)->save();
        });
    }

/**
     * GCSへの仮保存
     *
     * @param StoreSkillSheetRequest $request
     * @param string $uniqueKey
     * @return array
     */
    public function tmpStorageToGcsBucket(StoreSkillSheetRequest $request, string $uniqueKey): array
    {
        // バケットイニシャライズ
        $bucket = $this->utilityService->bucket;

        // 登録した保存名を保持していく
        $saveNames = [];

        // PR写真・動画・参考資料をぐるぐる回す
        foreach ($request->file() as $key => $files) {
            // name属性に応じて保存先を切り分ける
            foreach ($files as $file) {
                $options = $this->utilityService->setBucketOptions(
                    $file,
                    $key,
                    $uniqueKey,
                    $this->utilityService->bucketOptions
                );

                $saveNames[$key] = $options['name'];

                $bucket->upload(
                    fopen($file, 'rb'),
                    $options
                );
            }
        }

        return $saveNames;
    }

    /**
     * GCS保存
     *
     * @param integer $id
     * @param SkillSheet $target
     * @param array $updSkillSheet
     * @return array
     */
    public function storageToGcsBucket(int $id, SkillSheet $target, array $updSkillSheet): array
    {
        // セッションを破棄する
        $saveNames = Session('saveNames');
        session()->forget('saveNames');

        // バケットイニシャライズ
        $bucket = $this->utilityService->bucket;

        // 仮登録したデータをコピーして移動させてから削除する
        foreach ($saveNames as $key => $v) {
            switch ($key) {
                case Element::PR_IMAGE:
                    // 仮登録したオブジェクトがバケットに存在する場合は移管する
                    $targetObject = $bucket->object($v);
                    $extension = pathinfo($v)['extension'];
                    $name = "skill_sheet/{$id}/{$key}/{$id}_{$key}.{$extension}";

                    // オブジェクトが存在する場合は仮置きからコピーして移動する・仮置きは削除する
                    if ($targetObject->exists()) {
                        $targetObject->copy($bucket, ['name' => $name]);
                        $targetObject->delete();
                    }

                    // 登録データが既にある場合は差し替える(更新)
                    if ($target->pr_image_path) {
                        // 差し替え前のオブジェクトを取得
                        $beforeObject = $bucket->object($target->pr_image_path);

                        // 差し替え前のオブジェクトがあれば削除
                        if ($beforeObject->exists()) {
                            $beforeObject->delete();
                        } else {
                            // 差し替え前のオブジェクトがないのにDBに登録されている場合はリセットする
                            $updSkillSheet['pr_image_path'] = null;
                        }
                    }
                    $updSkillSheet = array_merge(
                        $updSkillSheet,
                        ["pr_image_path" => $name]
                    );
                    break;
                case Element::PR_VIDEO:
                    // 仮登録したオブジェクトがバケットに存在する場合は移管する
                    $targetObject = $bucket->object($v);
                    $extension = pathinfo($v)['extension'];
                    $name = "skill_sheet/{$id}/{$key}/{$id}_{$key}.{$extension}";

                    // オブジェクトが存在する場合は仮置きからコピーして移動する・仮置きは削除する
                    if ($targetObject->exists()) {
                        $targetObject->copy($bucket, ['name' => $name]);
                        $targetObject->delete();
                    }

                    // 登録データが既にある場合は差し替える(更新)
                    if ($target->pr_video_path) {
                        // 差し替え前のオブジェクトを取得
                        $beforeObject = $bucket->object($target->pr_video_path);

                        // 差し替え前のオブジェクトがあれば削除
                        if ($beforeObject->exists()) {
                            $beforeObject->delete();
                        } else {
                            // 差し替え前のオブジェクトがないのにDBに登録されている場合はリセットする
                            $updSkillSheet['pr_video_path'] = null;
                        }

                        // 問題なければ登録する
                    }
                    $updSkillSheet = array_merge(
                        $updSkillSheet,
                        ["pr_video_path" => $name]
                    );
                    break;
                default:
                    // 仮登録したオブジェクトがバケットに存在する場合は移管する
                    $targetObject = $bucket->object($v);
                    $extension = pathinfo($v)['extension'];
                    $name = "skill_sheet/$id/$key/{$id}_$key.$extension";

                    // オブジェクトが存在する場合は仮置きからコピーして移動する・仮置きは削除する
                    if ($targetObject->exists()) {
                        $targetObject->copy($bucket, ['name' => $name]);
                        $targetObject->delete();
                    }

                    // 登録データが既にある場合は差し替える(更新)
                    if ($target->other_document_path) {
                        // 差し替え前のオブジェクトを取得
                        $beforeObject = $bucket->object($target->other_document_path);

                        // 差し替え前のオブジェクトがあれば削除
                        if ($beforeObject->exists()) {
                            $beforeObject->delete();
                        } else {
                            // 差し替え前のオブジェクトがないのにDBに登録されている場合はリセットする
                            $updSkillSheet['other_document_path'] = null;
                        }

                        // 問題なければ登録する
                    }
                    $updSkillSheet = array_merge(
                        $updSkillSheet,
                        ["other_document_path" => $name]
                    );
                    break;
            }
        }
        return $updSkillSheet;
    }

    /**
     * GCSの画像をモーダルに表示するための処理
     *
     * @param integer $id
     * @return array|void
     */
    public function setPdfDownloadElement(int $id)
    {
        $skillSheet = SkillSheet::findOrFail($id);

        if ($skillSheet->other_document_path) {
            $bucket = $this->utilityService->bucket;
            $targetObject = $bucket->object($skillSheet->other_document_path);
            $isExists = $targetObject->exists();
            $signedPath = $isExists ? $targetObject->signedUrl(new \DateTime('tomorrow')) : null;
            $name = $isExists ? $targetObject->name() : null;
            $extension = mb_strtolower(pathinfo($name)['extension']);

            switch ($extension) {
                case 'pdf':
                    header('Content-Type:application/pdf');

                    break;
                case 'jpeg':
                case 'jpg':
                case 'png':
                    header('Content-Type:image/png,image/jpeg,image/jpg');
                    break;
            }

            if ($signedPath) {
                return [
                    'signedPath' => $signedPath,
                    'name' => $name
                ];
            }

            abort(404);
        }
    }
```





## 移動先にファイルの実体が移動されます

![スクリーンショット 2022-01-02 21.31.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/bd655655-e750-72c4-5327-0fe7d03535b8.png)