0. はじめに
大阪のLaravel初学者サウナーこと、kazumakishimoto(@kazuma_dev)です!
LaravelでAWS S3へ画像アップロードする方法について解説します!
0-1. 全体の流れ
0-2. 本記事の対象者
- LaravelでAWS S3へ画像アップロード
0-3. 事前準備
- IAMユーザー作成
- S3バケット作成
- ACL有効
0-4. 要件
- インフラはEC2ではなくHeroku
0-5. 使用画像のイメージ
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
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
-
FormRequest
のvalidated
メソッド -
Storage
ファサードのputFile
メソッド -
Eloquent
のfill
メソッド
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