対象バージョン: Laravel 13.x / PHP 8.3+(laravel/framework v13.39.0 で動作確認)
Laravel 13.x には、アップロードした画像やストレージ上の画像をリサイズ・変換する Image API があります。この記事は、アバター画像を正方形の WebP に揃えたい方や、縦横比を保ったまま縮小したい方向けです。Intervention Image の導入から、cover / scale によるリサイズ、WebP / JPEG への変換、public ディスクへの保存、テストまでを順に説明します。ファイルの受け取りとストレージの基本は【PHP】Laravelでファイルアップロードとストレージを扱う方法で扱っており、この記事はその記事で対象外としたリサイズ・変換を補います。
この記事で分かること
- Intervention Image のインストールと、画像ドライバ(GD)の設定
$request->image()でアップロード画像を読み取る方法cover/scale/orientによるリサイズと向きの補正toWebp/toJpeg/quality/optimizeによる変換storePublicly/storePubliclyAsでの保存- ストレージ上の画像を
Image::fromStorageやStorage::disk()->image()で扱う方法 - バリデーションと Feature テスト
前提
Laravel 13.x は PHP 8.3 以上が必要です。Image API は laravel/framework v13.20.0 で追加されました。動作確認は、composer create-project laravel/laravel:^13.0 で新規作成したアプリ(laravel/framework v13.39.0、PHP 8.4、intervention/image 4.4.0、ドライバ gd)で行いました。プロジェクトの作成方法は【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成を参照してください。
public ディスクへの保存と php artisan storage:link による公開は、先のファイルアップロードの記事で説明しています。この記事ではそこを前提に、画像の加工に進みます。
Intervention Image を入れる
Image API を使う前に、Intervention Image をインストールします。公式ドキュメントのコマンドは次のとおりです。
composer require intervention/image:^4.0
動作確認では intervention/image 4.4.0 が入りました。フレームワーク側の illuminate/image は suggest として intervention/image ^4.2 を挙げています。^4.0 で入れた 4.4.0 はどちらも満たします。
PHP には画像ドライバ用の拡張が必要です。既定のドライバは GD なので、環境に GD が入っていることを確認してください(動作確認では php -m に gd が出る状態で進めました)。
ドライバの設定を公開する
画像ドライバの設定ファイルを公開します。
php artisan config:publish images
config/images.php が次の内容で作成されます。
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Image Driver
|--------------------------------------------------------------------------
|
| This option controls the default image processing driver that will be
| used when manipulating or converting images. This driver is always
| utilized unless another driver is explicitly specified instead.
|
| Supported: "gd", "imagick"
|
*/
'default' => env('IMAGE_DRIVER', 'gd'),
];
既定では IMAGE_DRIVER 未設定時に gd が使われます。Imagick 拡張が入っている環境では、.env で IMAGE_DRIVER=imagick を指定して切り替えることもできます。
アップロード用のフォームを用意する
画像を送るフォームには、method="POST"、enctype="multipart/form-data"、@csrf が必要です。resources/views/avatars/create.blade.php の例です。
<form method="POST" action="{{ url('/avatar') }}" enctype="multipart/form-data">
@csrf
<label>
アバター画像
<input type="file" name="avatar" accept="image/*" required>
</label>
<button type="submit">アップロード</button>
</form>
アップロード画像を読み取る
$request->image('フィールド名') で、アップロードされた画像を Illuminate\Image\Image として取得できます。ファイルが無い場合は null です。別の書き方として Image::fromUpload($request->file('avatar')) もあります。
次のルートは、アバターを正方形の WebP に揃えて public ディスクへ保存する例です。routes/web.php を次の内容にします(このあと別ルートも同じファイルへ追記します)。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;
Route::post('/avatar', function (Request $request) {
$request->validate([
'avatar' => ['required', 'image'],
]);
$path = $request->image('avatar')
->orient()
->cover(400, 400)
->toWebp()
->quality(80)
->storePublicly('avatars', 'public');
$full = Storage::disk('public')->path($path);
$size = @getimagesize($full);
return response()->json([
'path' => $path,
'exists' => Storage::disk('public')->exists($path),
'mime' => $size['mime'] ?? null,
'width' => $size[0] ?? null,
'height' => $size[1] ?? null,
]);
});
Image のインスタンスはイミュータブルです。各メソッドは新しいインスタンスを返し、変換処理はチェーンの順に進み、エンコードは末尾で一度だけ行われます。
リサイズ: cover と scale
この記事で動作を確認したリサイズは次の2つです。
| メソッド | 動き(確認結果) |
|---|---|
cover(400, 400) |
指定サイズを覆うようにリサイズとクロップ。cover 後に toWebp した確認結果では、800×600 の JPEG を渡すと 400×400 の WebP になる |
scale(width: 320) |
縦横比を保って縮小。640×480 の PNG を渡すと 320×240 になる。拡大はしない |
scale(height: 100) |
高さ基準で縮小。800×600 を渡すと 133×100 になる |
orient() は EXIF の向き情報を反映します。上の /avatar 例では、orient() のあとに cover(400, 400) をつないでいます。
形式の変換と品質
エンコードはチェーンの末尾で指定します。動作確認で使った組み合わせは次のとおりです。
| 指定 | 結果 |
|---|---|
->toWebp()->quality(80) |
MIME は image/webp |
->toJpeg()->quality(85) |
MIME は image/jpeg、拡張子は jpg |
->optimize() |
WebP、品質の既定値は 70 |
quality() に渡す値は 1〜100 です。optimize() は形式と品質をまとめて指定するショートカットで、引数なしのときの品質は 70 です(古い説明の 75 ではありません)。
JPEG へ変換してファイル名を指定して保存する例を、同じ routes/web.php に追記します。
Route::post('/avatar-file-rule', function (Request $request) {
$request->validate([
'avatar' => ['required', File::image()],
]);
$image = $request->image('avatar')
->scale(width: 320)
->toJpeg()
->quality(85);
$inspect = [
'width' => $image->width(),
'height' => $image->height(),
'mimeType' => $image->mimeType(),
'extension' => $image->extension(),
'dimensions' => $image->dimensions(),
];
$path = $image->storePubliclyAs('avatars', 'scaled-avatar.jpg', 'public');
return response()->json([
'path' => $path,
'inspect' => $inspect,
]);
});
width() / height() / mimeType() / extension() / dimensions() は、加工後の結果を返します。上の例では inspect に幅 320、高さ 240、image/jpeg、jpg が入ります。
optimize() だけの例です。
Route::post('/optimize', function (Request $request) {
$request->validate(['avatar' => ['required', 'image']]);
$path = $request->image('avatar')
->optimize()
->storePublicly('avatars', 'public');
return response()->json(['path' => $path]);
});
800×600 の画像を渡すと、サイズはそのままで WebP として保存されます。
保存: storePublicly / storePubliclyAs
| メソッド | 役割 |
|---|---|
storePublicly('avatars', 'public') |
public ディスクの avatars/ 配下へ、公開可視性で保存。戻り値は保存先パス(例: avatars/<hash>.webp) |
storePubliclyAs('avatars', 'scaled-avatar.jpg', 'public') |
ファイル名を自分で指定して同様に保存 |
保存に失敗すると false が返ります。公開 URL の出し方や storage:link は、ファイルアップロードの記事を参照してください。
ストレージ上の画像を加工する
すでにディスクにある画像は、Image::fromStorage($path, $disk) または Storage::disk($disk)->image($path) で読み取れます。次のルートを routes/web.php に追記します。実行前に、public ディスクへ fixtures/source.jpg を置いておいてください。
Route::post('/from-storage', function () {
$path = Image::fromStorage('fixtures/source.jpg', 'public')
->cover(200, 200)
->toWebp()
->quality(70)
->storePublicly('avatars', 'public');
$viaDisk = Storage::disk('public')
->image('fixtures/source.jpg')
->scale(height: 100)
->toPng()
->storePubliclyAs('avatars', 'via-disk.png', 'public');
return response()->json([
'fromStorage_path' => $path,
'viaDisk_path' => $viaDisk,
]);
});
800×600 の JPEG を渡した場合、fromStorage 側は 200×200 の WebP、viaDisk 側は 133×100 の PNG になります。
routes/web.php の全体
ここまでのルートをまとめた routes/web.php です。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;
Route::post('/avatar', function (Request $request) {
$request->validate([
'avatar' => ['required', 'image'],
]);
$path = $request->image('avatar')
->orient()
->cover(400, 400)
->toWebp()
->quality(80)
->storePublicly('avatars', 'public');
$full = Storage::disk('public')->path($path);
$size = @getimagesize($full);
return response()->json([
'path' => $path,
'exists' => Storage::disk('public')->exists($path),
'mime' => $size['mime'] ?? null,
'width' => $size[0] ?? null,
'height' => $size[1] ?? null,
]);
});
Route::post('/avatar-file-rule', function (Request $request) {
$request->validate([
'avatar' => ['required', File::image()],
]);
$image = $request->image('avatar')
->scale(width: 320)
->toJpeg()
->quality(85);
$inspect = [
'width' => $image->width(),
'height' => $image->height(),
'mimeType' => $image->mimeType(),
'extension' => $image->extension(),
'dimensions' => $image->dimensions(),
];
$path = $image->storePubliclyAs('avatars', 'scaled-avatar.jpg', 'public');
return response()->json([
'path' => $path,
'inspect' => $inspect,
]);
});
Route::post('/optimize', function (Request $request) {
$request->validate(['avatar' => ['required', 'image']]);
$path = $request->image('avatar')
->optimize()
->storePublicly('avatars', 'public');
return response()->json(['path' => $path]);
});
Route::post('/from-storage', function () {
$path = Image::fromStorage('fixtures/source.jpg', 'public')
->cover(200, 200)
->toWebp()
->quality(70)
->storePublicly('avatars', 'public');
$viaDisk = Storage::disk('public')
->image('fixtures/source.jpg')
->scale(height: 100)
->toPng()
->storePubliclyAs('avatars', 'via-disk.png', 'public');
return response()->json([
'fromStorage_path' => $path,
'viaDisk_path' => $viaDisk,
]);
});
バリデーション
アップロードが画像かどうかを確かめるには、次のどちらも使えます。
- 文字列ルール
'image'(/avatarの例) File::image()(/avatar-file-ruleの例)
画像以外(PDF など)を送ると、422 となり「The avatar field must be an image.」というエラーになります。バリデーション全般は【PHP】Laravelでフォームバリデーションを実装する方法を参照してください。
Feature テスト
Feature テストでは UploadedFile::fake()->image() と Storage::fake('public') を使えます。fake()->image() には GD 拡張が必要です。tests/Feature/ImageApiTest.php の例です(動作確認した5件のうち、本文のルートに対応する3件です)。
<?php
namespace Tests\Feature;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ImageApiTest extends TestCase
{
public function test_request_image_cover_webp_store_publicly(): void
{
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg', 800, 600);
$response = $this->post('/avatar', [
'avatar' => $file,
]);
$response->assertOk();
$path = $response->json('path');
$this->assertNotEmpty($path);
$this->assertTrue(Storage::disk('public')->exists($path));
$this->assertSame(400, $response->json('width'));
$this->assertSame(400, $response->json('height'));
$this->assertSame('image/webp', $response->json('mime'));
}
public function test_file_image_rule_scale_jpeg(): void
{
Storage::fake('public');
$file = UploadedFile::fake()->image('photo.png', 640, 480);
$response = $this->post('/avatar-file-rule', [
'avatar' => $file,
]);
$response->assertOk();
$this->assertSame(320, $response->json('inspect.width'));
$this->assertSame(240, $response->json('inspect.height'));
$this->assertSame('image/jpeg', $response->json('inspect.mimeType'));
$this->assertTrue(Storage::disk('public')->exists($response->json('path')));
}
public function test_validation_rejects_non_image(): void
{
Storage::fake('public');
$file = UploadedFile::fake()->create('doc.pdf', 100, 'application/pdf');
$this->post('/avatar', ['avatar' => $file])->assertSessionHasErrors('avatar');
}
}
php artisan test --filter=ImageApiTest
テストの基本は【PHP】Laravelでテストを実装する方法を参照してください。
処理が重い場合
画像の加工は CPU やメモリを使うことがあります。アップロードのリクエスト内で重い処理をしない方がよい場合は、キューのジョブへ回してください。キューの実装は【PHP】Laravelでキューとジョブを実装する方法とその利用シーンを参照してください。
S3 などのクラウドストレージや CDN、Imagick の詳細設定はこの記事では扱いません。
よくあるつまずき
| 症状 | 原因と対処 |
|---|---|
| Image API のメソッドが無い | フレームワークが v13.20.0 未満。13.x の新しいパッチへ上げる |
intervention/image が無い |
composer require intervention/image:^4.0 を実行する |
GD 関連のエラーや fake()->image() が失敗する |
PHP に GD 拡張が入っていない。php -m で gd を確認する |
| 保存したファイルにブラウザから届かない | public ディスクと storage:link の設定を確認する(ファイルアップロードの記事) |
| 画像以外を送っても通ってしまう | 'image' または File::image() のバリデーションを付ける |
optimize() の品質が想定と違う |
引数なしの既定品質は 70 |
まとめ
- Image API は Laravel 13.20.0 以降で使える。Intervention Image は
composer require intervention/image:^4.0で入れる - 既定ドライバは GD(
IMAGE_DRIVER)。設定はphp artisan config:publish imagesで公開する $request->image('avatar')でアップロード画像を読み取り、orient/cover/scaleのあとにtoWebpやtoJpeg、quality、optimizeをつなぐoptimize()の既定品質は 70。storePublicly/storePubliclyAsでpublicディスクへ保存できる- ディスク上の画像は
Image::fromStorageまたはStorage::disk()->image()で扱える - バリデーションは
'image'とFile::image()のどちらでもよい。Feature テストのfake()->image()には GD が必要 - 重い加工はキューへ回す
関連記事
- 【PHP】Laravelでファイルアップロードとストレージを扱う方法
- 【PHP】Laravelでフォームバリデーションを実装する方法
- 【PHP】Laravelでキューとジョブを実装する方法とその利用シーン
- 【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成
- 【PHP】Laravelでテストを実装する方法