Development

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.

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://cdnjs.cloudflare.com/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:

Some files uploaded successfully whilst others failed validation:

All files passed validation:

If no files are selected:

Share

Recent Posts

Kennington reservoir drained drone images

A drained and empty Kennington reservoir images from a drone in early July 2024. The…

1 year ago

Merrimu Reservoir drone images

Merrimu Reservoir from drone. Click images to view larger.

1 year ago

FTP getting array of file details such as size using PHP

Using FTP and PHP to get an array of file details such as size and…

2 years ago

Creating Laravel form requests

Creating and using Laravel form requests to create cleaner code, separation and reusability for your…

2 years ago

Improving the default Laravel login and register views

Improving the default Laravel login and register views in such a simple manner but making…

2 years ago

Laravel validation for checking if value exists in the database

Laravel validation for checking if a field value exists in the database. The validation rule…

2 years ago