LoginSignup
0

posted at

updated at

【Laravel】HerokuでAWS S3画像アップロード

0. はじめに

大阪のLaravel初学者サウナーこと、kazumakishimoto(@kazuma_dev)です!
LaravelでAWS S3へ画像アップロードする方法について解説します!

0-1. 全体の流れ

1.設定
2.画像アップロード機能
3.補足
Reference

0-2. 本記事の対象者

  • LaravelでAWS S3へ画像アップロード

0-3. 事前準備

  • IAMユーザー作成
  • S3バケット作成
  • ACL有効

grfl - S3 bucket.png

grfl - S3 bucket (1).png

0-4. 要件

  • インフラはEC2ではなくHeroku

0-5. 使用画像のイメージ

image.png

image.png

image.png

1. 設定

1-1.環境変数

  • HerokuのConfig Vars編集(LOG_CHANNELは推奨)
AWS_ACCESS_KEY_ID= csvの認証情報に記載されているAccess key ID
AWS_SECRET_ACCESS_KEY= csvの認証情報に記載されているSecret access key
AWS_DEFAULT_REGION=ap-northeast-1 (リージョンをアジアパシフィック東京で作成したため)
AWS_BUCKET= 作成したバケット名
LOG_CHANNEL= errorlog

grfl · Settings _ Heroku.png

1-2. S3パッケージのインストール

$ composer require league/flysystem-aws-s3-v3 ^1.0

1-3. ファイルシステムの編集

config/filesystems.php
return [

    'default' => env('FILESYSTEM_DRIVER', 'local'),

    'cloud' => env('FILESYSTEM_CLOUD', 's3'),

    'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app'),
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => env('APP_URL').'/storage',
            'visibility' => 'public',
        ],

        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'url' => env('AWS_URL'),
            'endpoint' => env('AWS_ENDPOINT'),
        ],

    ],

];

2. 画像アップロード機能

2-1. Route

routes/web.php
# ユーザー投稿関係(create, store, edit, update, destroy)
Route::get('/', 'ArticleController@index')->name('articles.index');
Route::resource('/articles', 'ArticleController')->except(['index', 'show'])->middleware('auth');
Route::resource('/articles', 'ArticleController')->only(['show']);

### ログイン状態で使用可能 ###
Route::middleware('auth')->group(function () {
    Route::prefix('users/{name}')->name('users.')->group(function () {
        // プロフィール編集
        Route::get('/edit', 'UserController@edit')->name('edit');
        // プロフィール更新
        Route::patch('/update', 'UserController@update')->name('update');
    });
});

2-2. Migration

$ php artisan make:model User --migration
$ php artisan make:model Article --migration

create_user_table

database/migrations/XXXXXXXXXXX_create_users_table.php
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('twitter_id')->nullable();
            $table->string('name')->unique();
            $table->string('age')->nullable();
            $table->string('gender')->nullable();
            $table->string('avatar')->nullable();
            $table->text('introduction')->nullable();
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }

create_articles_table

database/migrations/XXXXXXXXXXX_create_articles_table.php
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('user_id')->unsigned();
            $table->bigInteger('pref_id')->nullable();
            $table->text('title');
            $table->text('body');
            $table->string('image')->nullable();
            $table->timestamps();
            $table->softDeletes();

            $table->foreign('user_id')
            ->references('id')
            ->on('users')
            ->onDelete('cascade');
        });
    }
$ php artisan migrate

2-3. Model

User.php

app/Models/User.php
    protected $fillable = [
        'twitter_id', 'name', 'age', 'gender', 'avatar', 'introduction', 'email', 'password',
    ];

Article.php

app/Models/Article.php
    protected $fillable = [
        'pref_id', 'title', 'body', 'image',
    ];

2-4. Controller

$ php artisan make:controller UserController
$ php artisan make:controller ArticleController

UserController

  • FormRequestvalidatedメソッド
  • StorageファサードのputFileメソッド
  • Eloquentfillメソッド
app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\UserRequest;
use App\Models\User;

    // プロフィール編集処理
    public function update(UserRequest $request, string $name)
    {
        $validated = $request->validated();

        $user = User::where('name', $name)->first();

        // 画像アップロード
        if (isset($validated['avatar'])) {
            $image = $request->file('avatar');
            $path = Storage::disk('s3')->putFile('avatar', $image, 'public');
            $validated['avatar'] = Storage::disk('s3')->url($path);
        }

        // UserPolicyのupdateメソッドでアクセス制限
        $this->authorize('update', $user);

        // バリデーションにかけた値だけをDBに保存
        $user->fill($validated)->save();

        $data = [
            "name" => $user->name
        ];

        return redirect()->route('users.show', $data);
    }

ArticleController

app/Http/Controllers/ArticleController.php
<?php

namespace App\Http\Controllers;

use App\Models\Article;
use App\Models\Tag;
use App\Http\Requests\ArticleRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;

    // 投稿処理
    public function store(ArticleRequest $request, Article $article)
    {
        $article->user_id = $request->user()->id;
        $all_request = $request->all();
        $article->pref_id = $request->pref;

        // 画像アップロード
        if (isset($all_request['image'])) {
            $image = $request->file('image');
            $path = Storage::disk('s3')->putFile('image', $image, 'public');
            $all_request['image'] = Storage::disk('s3')->url($path);
        }

        $article->fill($all_request)->save();

        $request->tags->each(function ($tagName) use ($article) {
            $tag = Tag::firstOrCreate(['name' => $tagName]);
            $article->tags()->attach($tag);
        });

        return redirect()->route('articles.index');
    }

    // 編集処理
    public function update(ArticleRequest $request, Article $article)
    {
        $article->user_id = $request->user()->id;
        $all_request = $request->all();
        $article->pref_id = $request->pref;

        // 画像アップロード
        if (isset($all_request['image'])) {
            $image = $request->file('image');
            $path = Storage::disk('s3')->putFile('image', $image, 'public');
            $all_request['image'] = Storage::disk('s3')->url($path);
        }

        $article->fill($all_request)->save();

        $article->tags()->detach();
        $request->tags->each(function ($tagName) use ($article) {
            $tag = Tag::firstOrCreate(['name' => $tagName]);
            $article->tags()->attach($tag);
        });

        return redirect()->route('articles.index');
    }

2-5. Request

$ php artisan make:request UserRequest
$ php artisan make:request ArticleRequest

UserRequest

app\Http\Requests\UserRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;

class UserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // ゲストユーザーログイン時に、ユーザー名とメールアドレスを変更できないよう対策
        if (Auth::id() == config('user.guest_user.id')) {
        return [
            'age' => ['numeric', 'min:1', 'max:100', 'nullable'],
            'avatar' => ['image', 'nullable'],
            'introduction' => ['max:200', 'nullable'],
            ];
        }

        return [
            'name' => ['required','regex:/^(?!.*\s).+$/u', 'regex:/^(?!.*\/).*$/u', 'max:15', Rule::unique('users')->ignore(Auth::id())],
            'age' => ['numeric', 'min:1', 'max:100', 'nullable'],
            'introduction' => ['max:200', 'nullable'],
            'avatar' => ['image', 'nullable'],
            'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore(Auth::id())],
            // 'password' => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }

    public function attributes()
    {
        return [
            'name' => 'ユーザー名',
            'age' => '年齢',
            'introduction' => '自己紹介',
            'avatar' => 'プロフィール画像',
            'email' => 'メールアドレス',
            // 'password' => 'パスワード',
        ];
    }

    public function messages()
    {
        return [
            'name.regex' => ':attributeに「/」と半角スペースは使用できません。'
        ];
    }
}

ArticleRequest

app\Http\Requests\ArticleRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ArticleRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required|max:50|not_regex:/<\/*script>/u',
            'body' => 'required|max:500|not_regex:/<\/*script>/u',
            'image' =>'image',
            'tags' => 'json|regex:/^(?!.*\s).+$/u|regex:/^(?!.*\/).*$/u',
        ];
    }

    public function attributes()
    {
        return [
            'title' => 'タイトル',
            'body' => '本文',
            'image' => '画像',
            'tags' => 'タグ',
        ];
    }

    public function messages()
    {
        return [
            'tags.regex' => ':attributeに「/」と半角スペースは使用できません。'
        ];
    }

    public function passedValidation()
    {
        $this->tags = collect(json_decode($this->tags))
            ->slice(0, 5)
            ->map(function ($requestTag) {
                return $requestTag->text;
            });
    }
}

2-6. View

users

resources/views/users/edit.blade.php
<form action="{{ route('users.update', ['name' => $user->name]) }}" method="POST" enctype="multipart/form-data">
    @method('PATCH')
    @csrf
    @if(Auth::id() != config('user.guest_user.id'))
    <label class="form-label d-block" for="avatar">
        <img src="{{ $user->avatar }}" id="img" class="img-fuild rounded-circle" width="80" height="80">
        <input type="file" name="avatar" id="avatar" class="d-none" onchange="previewImage(this);">
    </label>
    @endif
    <button type="submit" class="btn btn-block cyan darken-3 text-white col-lg-6 col-md-7 col-sm-8 col-xs-10 mx-auto mt-5 waves-effect">更新する</button>
</form>

// 略

@include('footer')
@endsection

<script>
    function previewImage(obj) {
        var fileReader = new FileReader();
        fileReader.onload = (function() {
            document.querySelector('#img').src = fileReader.result;
        });
        fileReader.readAsDataURL(obj.files[0]);
    }
</script>
resources/views/users/user.blade.php
<a href="{{ route('users.show', ['name' => $user->name]) }}" class="text-dark">
    <img src="{{ $user->avatar }}" class="img-fuild rounded-circle" width="60" height="60">
</a>

articles

resources/views/articles/form.blade.php
<form method="POST" action="{{ route('articles.update', ['article' => $article]) }}" enctype="multipart/form-data">
    @method('PATCH')
    @csrf
    <input id="image" type="file" name="image" accept="image/*" onchange="previewImage(this);">
    <button type="submit" class="btn blue-gradient btn-block">更新する</button>
</form>
resources/views/articles/card.blade.php
<img src="{{ $article->image }}" width="200px" class="mt-3 mb-1">

3. 補足

3-1. 開発環境(FW/ツールのバージョンなど)

ツール バージョン
Vue.js 2.6.14
jQuery 3.4.1
PHP 7.4.1
Laravel 6.20.43
MySQL 5.7.36
Nginx 1.18.0
Composer 2.0.14
npm 6.14.6
Git 2.33.1
Docker 20.10.11
docker-compose v2.2.1
PHPUnit 8.0
CircleCI 2.1
heroku 7.59.4
MacBook Air M1,2020
macOS Monterey 12.3
Homebrew 3.3.8

3-2. ディレクトリ構造

【ルートディレクトリ】
├─ .circleci
│   └─ config.yml
├─ aws / CloudFormation
│   └─ ec2.yml
├─ docker
│   └─ mysql
│   └─ nginx
│   └─ php
│   └─ phpmyadmin
├─ src
│   └─ 【Laravelのパッケージ】
│─ .env
│─ .gitignore
└─ docker-compose.yml

Reference

  • 【Laravel × AWS S3】プロフィール機能を実装する(画像アップロード) - Qiita

  • Rails, Laravel(画像アップロード)向けAWS(IAM:ユーザ, S3:バケット)の設定 - Qiita

  • LaravelでAWS S3へ画像をアップロードする - Qiita

  • 【Laravel】【AWS S3】LaravelでAWS S3に画像をアップロードできない(400 Bad Request

  • AWSにデプロイしたポートフォリオをHerokuへ移行しようとしたらかなり苦労した - Qiita

  • AWS S3のパッケージインストール時に、composer require league/flysystem-aws-s3-v3を叩くと、Your requirements could not be resolved to an installable set of packages.とエラーが出るときの解決策。 - Qiita

  • Laravel(+Vue.js)でSNS風Webサービスを作ろう! | Techpit

  • Laravel × CircleCI × AWSで学ぶCI/CD | Techpit

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0