Laravel 9 multiple file upload with validation example

Creating a form to upload multiple files using Laravel 9 with file type and size validation.

Included is a controller, model, upload form and migration for the database table to store information about the files uploaded.

Laravel 9 upload multiple files

The controller

Create the file upload controller:

php artisan make:controller FileUploadController

This handles the upload form view and the upload POST request.

Included are 3 validation checks, the first checks if there is at least one file POSTED. The second is if the uploaded file is an approved file extension type.

Lastly a check is done to ensure each file is below the max allowed file size.

If a file fails validation it will be skipped so that the upload process isn’t aborted because one of the files fails validation.

If validated successfully the files will be uploaded into public/uploads/YYYY/MM e.g: public/uploads/2022/04

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;

class FileUploadController extends Controller
{
    public function index()
    {
        return view('files.upload');
    }

    public function store(Request $request): \Illuminate\Http\RedirectResponse
    {
        if (!$request->hasFile('files')) {
            return back()->with("fail", "You must select least 1 file");
        }

        $max_allowed_size = 1024;//As Kilobytes

        $allowed_extensions = ['jpg', 'jpeg', 'png'];

        $upload_directory = 'uploads/' . date('Y') . '/' . date('m');//Upload and move image into this directory

        $uploaded_files = $request->file('files');

        $invalid_files = [];
        $uploaded_file_count = 0;
        foreach ($uploaded_files as $file) {

            $file_extension = $file->extension();
            $file_name = $file->getClientOriginalName();

            if (!in_array($file_extension, $allowed_extensions, true)) {
                $invalid_files[] = array('file' => $file_name, 'reason' => "File type '$file_extension' is not allowed to be uploaded");
                continue;
            }

            $file_size = number_format(($file->getSize() / 1024), 0, '');//Kilobytes

            if ($file_size > $max_allowed_size) {
                $invalid_files[] = array('file' => $file_name, 'reason' => "Too large ($file_size KB) must be under $max_allowed_size KB");
                continue;
            }

            $file_id = Str::random(8);//Random 8 character string
            $save_as_name = $file_id . '.' . $file_extension;

            $file->move(public_path($upload_directory), $save_as_name);//Save into: public/images

            DB::table('file_uploads')->insert([
                'id' => $file_id,
                'user_id' => Auth::id(),
                'size' => $file_size,
                'original_name' => $file_name,
                'directory' => $upload_directory,
                'extension' => $file_extension,
                'uploaded' => date('Y-m-d H:i:s')
            ]);

            $uploaded_file_count++;
        }

        if ($uploaded_file_count > 0) {
            return back()->with("success", "Successfully uploaded $uploaded_file_count files")
                ->with("fail", $invalid_files);
        }

        return back()->with("fail", $invalid_files);

    }

}

 

The routes

Two routes are needed, the first is the upload form page on a GET method and the second is a POST for the upload validation and processing.

Route::controller(FileUploadController::class)->middleware('auth')->group(function(){

    Route::get('files/upload', 'index');

    Route::post('files/upload', 'store')->name('files.store');

});

Both are only accessible by authenticated users and the URI is files/upload

Run php artisan route:cache to refresh the routes.

The upload form view

The file upload form page as a blade view, in resources/views create a folder called files and then inside this folder create upload.blade.php with the following:

<html>
<head>
    <title>Upload files</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://write.corbpie.com/wp-content/litespeed/localres/aHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS8=ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="card">
        <div class="card-body">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h2>Upload files</h2>
                </div>
                <div class="panel-body">
                    @if ($message = Session::get('success'))
                        <div class="alert alert-success alert-block">
                            <strong>{{ $message }}</strong>
                        </div>
                    @endif
                    @if(!empty(session()->get('fail')))
                        <div class="alert alert-danger alert-block">
                            @if(isset(session()->get('fail')[0]['reason']))
                                <strong>The following file/s failed</strong>
                                <ul>
                                    @foreach(session()->get('fail')  as $in)
                                        <li>File: <b>{{$in['file']}}</b> Reason: <b>{{$in['reason']}}</b></li>
                                    @endforeach
                                </ul>
                            @else
                                <strong>{{ Session::get('fail')}}</strong>
                            @endif
                        </div>
                    @endif
                    <form action="{{ route('files.store') }}" method="POST" enctype="multipart/form-data">
                        @csrf
                        <div class="mb-3">
                            <label class="form-label" for="file-input">Files:</label>
                            <input type="file" name="files[]" id="file-input"
                                   class="form-control @error('image') is-invalid @enderror" multiple>
                        </div>
                        <div class="mb-3">
                            <button type="submit" class="btn btn-success">Upload</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

 

Note for this example only, the blade file is the full page rather than utilizing @extends and @sections.

The migration

Only if you want to store information about the files uploaded into the database.

Run: php artisan make:migration FileUpload

Find the database/migrations/create_file_uploads_table.php file and paste into it:

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up()
    {
        Schema::create('file_uploads', function (Blueprint $table) {
            $table->char('id', 8);
            $table->bigInteger('user_id');
            $table->integer('size');
            $table->string('original_name');
            $table->string('directory');
            $table->string('extension');
            $table->dateTime('uploaded');
            $table->primary(['id']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('file_uploads');
    }
};

Now run migrate to create this table: php artisan migrate

Validation error examples

Here are some examples of return messages from the uplaod attempts

The file was too large:

Laravel 9 multiple file upload errors

Some files uploaded successfully whilst others failed validation:
Laravel 9 multiple file upload success and errors

All files passed validation:

Laravel 9 multiple file upload success

If no files are selected:

Laravel 9 multiple file upload error none selected