はじめに
この記事では
- Laravel Livewireを使用したファイルアップロードで実現できること
- その機能を実装する方法
についてご紹介します。
環境
- PHP: 8.1.25
- Laravel: 10.31.0
- Livewire: 2.12.8
Gitリポジトリ
参考
本記事の内容は、主に以下の公式ドキュメントに基づいています。
できること
Laravel Livewireを使用したファイルアップロードでは、フロントエンド処理を最小限に抑えつつ、以下のSPAライクな実装が可能です。
| 機能 | 説明 |
|---|---|
| リアルタイムバリデーション | フォームを送信しなくても、ファイルを選択した瞬間にバリデーションすることができる |
| 進捗インジケーター | 現在のアップロード状況を表す簡易的なインジケーターを簡単に実装できる |
| 非同期アップロード | ページ遷移をせずに、バックグラウンドでファイルをサーバーにアップロードできる |
そのため、Laravel標準のMPA(マルチページアプリケーション)と比較して、ユーザーはリアクティブに使用することができるため、ユーザー体験(UX)の向上を期待できます。
実装してみる
以下の要件で実装します。
- 画像ファイルをアップロードすることができる
- アップロードしたファイルを一覧で確認することができる
※ Livewire初期設定やルーティングなどは割愛します
※ 検証のため、セキュリティ面は考慮していません
1. Livewireコンポーネントファイルを作成する
コマンドを実行して、コンポーネントの実装に必要なファイルを作成します。
php artisan make:livewire PhotoUploader
以下のファイルが作成されます
- src/app/Http/Livewire/PhotoUploader.php
- resources/views/livewire/photo-uploader.blade.php
2. Livewireコンポーネントクラスの実装
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
class PhotoUploader extends Component
{
use WithFileUploads;
public $photo = null;
public array $uploadedPhotos = [];
/**
* Livewireライフサイクルフック
* アップロード済み画像一覧を設定する
*/
public function mount()
{
$this->uploadedPhotos = $this->getUploadedPhotos();
}
public function render()
{
return view('livewire.photo-uploader');
}
/**
* リアルタイムバリデーション
*
* Livewireライフサイクルフックにより、ファイル選択時に呼び出されることで、
* リアクティブなバリデーションが実現できる。
*/
public function updatedPhoto()
{
$this->validatePhoto();
}
/**
* ファイル保存処理
*
* ファイル自体は選択時にサーバーの一時ディレクトリにアップロードされているため、
* ここではそのファイルを永続化する処理を行う。
*/
public function save()
{
if (is_null($this->photo)) {
return;
}
$this->resetErrorBag();
$this->validatePhoto();
$this->photo->store('public/photos');
$this->photo = null;
$this->uploadedPhotos = $this->getUploadedPhotos();
}
/**
* アップロード済みファイル一覧を取得する
*/
private function getUploadedPhotos()
{
$uploadedPhotos = [];
$files = Storage::files('public/photos');
// リンクに変換
foreach ($files as $file) {
$uploadedPhotos[] = Storage::url($file);
}
return $uploadedPhotos;
}
/**
* ファイルのバリデーション
*/
private function validatePhoto()
{
$this->validate([
'photo' => ['file', 'max:1024', 'mimes:jpg,jpeg,png'],
]);
}
}
3. Bladeコンポーネントの実装
<div>
<h1>アップロード</h1>
<form wire:submit.prevent="save">
<p>
<input type="file" wire:model="photo">
</p>
{{-- 進捗インジケータ --}}
<div wire:loading wire:target="photo">アップロード中...</div>
@error('photo')
<p style="color:red;">
{{ $message }}
</p>
@enderror
<p>
<button type="submit">保存</button>
</p>
</form>
<h1>アップロード済み画像一覧</h1>
@forelse ($uploadedPhotos as $uploadedPhoto)
<ul>
<img src="{{ $uploadedPhoto }}"></img></li>
</ul>
@empty
<span>アップロードされたファイルはありません。</span>
@endforelse
</div>
上記を実装することで、以下の画面が作成されます
動かしてみる
リアルタイムバリデーション
ファイルを選択した瞬間にバリデーションされることを確認できます。
※ 下記はCSVファイルを選択した例
進捗インジケータ
画像ファイルを選択すると、一瞬だけ進捗インジケータが出現します。

今回の実装では「アップロード中か否か」のみの状況を取得しましたが、Alpine.jsと組み合わせることにより、より詳細な状況を取得することが可能になるようです。
大容量のファイルを扱う際に、導入を検討するのが良さそうです。
非同期のファイルアップロード
ファイルを選択して保存ボタンをクリックすると、
アップロード済み画像一覧へリアクティブに画像が追加されることを確認できます。
また、storage/app/public/photos/配下にファイルが作成されていることから、ファイルが永続化されることも確認できます。
プロパティ変数$photoの型について
プロパティ変数の$photoは初期状態はnull、ファイル選択時にLivewire\TemporaryUploadedFileが設定されるようでした
public function updatedPhoto()
{
dd($this->photo);
...
しかし、public ?TemporaryUploadedFile $photo;で型を指定すると、
Cannot assign string to property App\Http\Livewire\PhotoUploader::$photo of type ?Livewire\TemporaryUploadedFile
のエラーが出ました。
それもそもはずで、Livewireのプロパティで指定できる型は限定されており、Livewire\TemporaryUploadedFileは該当していないためですね。
参考
型を指定しなければエラーが起きない理由は、Livewire\WithFileUploadsTraitあたりがhydrate/dehydrate時などに、よしなにハンドリングしてくれているからだと推測しています。
そのため、型は無理に指定しない方が良さそうです。
Livewire\TemporaryUploadedFileについて
Livewire\TemporaryUploadedFileクラスを使用することで、
store()メソッドで永続化したり、
temporaryUrl()メソッドを使用することで、プレビュー用の一時的なURLを発行したり
とファイルを簡単に扱うことができます。
所感
Livewireで提供されているファイルアップロード機能を使用することで、モダンなUXが簡単に実装できることを実感しました。
しかし、モダンJS系と比較すると実現できる幅や利便性は少し下がってしまうかなとも感じました。
(例えば、input fileで表示される、選択中ファイルのファイルパスは、バインドしている変数と連動して変化してくれないなど...)
細かな課題はあるものの、実装コストの低さを鑑みれば、実用性は十分あるものだと感じています。
最後までご覧いただきありがとうございました!



