【Laravel × Flutter】FlutterアプリからLaravel経由でS3に画像をアップロードする

ユーザー登録系のアプリでプロフィール画像を保存するためにFlutterからLaravel経由で画像をS3にアップロードする必要があったのですが、実装する時にかなり詰まったので投稿しました。

# 開発環境
Laravel Framework 7.30.6
Flutter 3.0.3

# 全体の流れ
全体の流れとしては

Flutter側で画像選択　→ 選択した画像をbase64エンコードしてLaravel側に送信　→ Laravel側でbase64エンコードされた画像を受け取ってデコード → S3にアップロード

という流れになります。

ちなみにですが、別にわざわざbase64エンコードしてテキストに変換してから送信しなくても画像アップロードはできると思うのですが、色々試してもうまくいかなかったので今回はbase64エンコードすることにしました。

（もっと簡単な方法があるならとても知りたい。。）

# Flutterアプリ側の実装

画像のアップロードはimage_pickerを使用しました。

https://pub.dev/packages/image_picker

image_pickerを使うことでカメラロールから画像を選択したり、撮影した写真をアプリで使用することができます。

image_pickerの使用には色々と設定が必要なのですが、image_pickerの設定については別の記事を参考にしてください。

https://qiita.com/hmatsu47/items/e4f7e310e88376d54009

flutterアプリでカメラロールから画像を選択してアップロードする部分については下記のような形になります。

```dart:upload_image_sample.dart
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

class UploadImageSample extends StatefulWidget {
  const UploadImageSample({Key? key}) : super(key: key);

  @override
  UploadImageSampleState createState() => UploadImageSampleState();
}

class UploadImageSampleState extends State<UploadImageSample> {
  final picker = ImagePicker();
  File? image;

  Future<void> _setImage() async {
    final pickedFile = await picker.pickImage(source: ImageSource.gallery);

    setState(() {
      if (pickedFile != null) {
        image = File(pickedFile.path);
      }
    });
  }

  Future<void> _upload() async {
    if (image == null) return;

    // ここで画像をFileからBase64（String）にエンコードする
    Uint8List imageBytes = image!.readAsBytesSync();
    String imageBytesBase64 = base64Encode(imageBytes);

    // Laravel側のエンドポイントURL（環境に合わせて設定してください）
    Uri url = Uri.parse("https://hogegoge.jp");
    String body = jsonEncode({image: imageBytesBase64});

    // LaravelにPOST
    await http.post(url, body: body);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(
      children: [
        // タップするとカメラロールが開いて画像を選択するボタン
        ElevatedButton(
            onPressed: () {
              _setImage();
            },
            child: const Text("画像選択")),
        // タップすると選択した画像をLaravelに送信するボタン
        ElevatedButton(
            onPressed: () {
              _upload();
            },
            child: const Text("アップロードする")),
      ],
    ));
  }
}

```


# Laravel側の実装

次にLaravel側の実装です。
flutter側の実装で設定したURLを叩くと下記UploadController.phpのuploadメソッドが動く想定です。

```php: UploadController.php
public function upload(Request $request){
  $image_base64 = $request->input("image");
  
  // 画像名をランダムで作成（本当は拡張子判定とかするんだろうけどとりあえずjpegで固定）
  $image_name = Str::random(20) . '.' . 'jpeg';

  // デコードする前に不要な文字列を置換する
  $image_base64 = str_replace('data:image/jpeg;base64,', '', $image_base64);
  $image_base64 = str_replace(' ', '+', $image_base64);
  $image_file = base64_decode($image_base64);

  // S3に保存
  $upload_path = "img/hogehoge"
  Storage::disk('s3')->put($upload_path, $image_file, 'public');

  // 読み取り用のURLを取得
  $url = Storage::disk('s3')->url($upload_path);

  // 取得したURLを返却
  return response()->json([
    "url" => $url,
  ]); 
}
```

これで無事アップロードができるはずです。