Laravel 9 image upload and validation example

How to do an image upload form and handler in Laravel 9 with file validation.

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

Laravel 9 image upload

The controller

Create the image upload controller with

php artisan make:controller ImageUploadController

This will handle showing the upload form blade file and the processing of the image upload (validation, renaming and storing).

Inside app/Http/Controllers/ImageUploadController.php paste the following:

<?php
namespace App\Http\Controllers;

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


class ImageUploadController extends Controller

{
    public function index()
    {
        return view('images.upload');
    }

    public function store(Request $request)
    {
        $request->validate([
            'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
        ]);

        $original_name = $request->image->getClientOriginalName();//Original image file name

        $file_size = $request->image->getSize();//Size in bytes

        $extension = $request->image->extension();//Image file extension eg. jpg or png

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

        $id = Str::random(8);//Random 8 character string

        $image_name = $id . '.' . $extension;

        $full_save_path_name = $directory . '/' . $image_name;

        $request->image->move(public_path($directory), $image_name);//Save into: public/images

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

        return back()
            ->with("success", "You have successfully uploaded $original_name saved as: $image_name")
            ->with("image", $full_save_path_name);
    }

}

The uploaded image will be validated with 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048' this means the form field is required, it must be an image in jpg/png/gif/svg format and the maximum allowed size is 2048 kilobytes.

The save directory will be public/uploads/YEAR/MONTH e.g public/uploads/2022/04

The image will be renamed as a randomly generated id, 8 characters long.

The routes

In routes/web.php add:

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

    Route::get('image-upload', 'index');

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

});

These two routes are protected by ‘auth’ meaning only logged in users can view them. The two routes are /image-upload as a GET which calls index and /image-upload as a POST which calls the store method from ImageUploadController.

Run php artisan route:cache to refresh the routes.

The upload form page

This will be the form upload page as a blade file, put in resources/views/images the file upload.blade.php with the following:

<!DOCTYPE html>
<html>
<head>
    <title>Upload image</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 image</h2>
                </div>
                <div class="panel-body">
                    @if ($message = Session::get('success'))
                        <div class="alert alert-success alert-block">
                            <strong>{{ $message }}</strong>
                        </div>
                        <img class="img-fluid" src="{{ Session::get('image') }}" alt="uploaded image">
                    @endif
                    <form action="{{ route('image.store') }}" method="POST" enctype="multipart/form-data">
                        @csrf
                        <div class="mb-3">
                            <label class="form-label" for="image_input">Image:</label>
                            <input type="file" name="image" id="image_input"
                                   class="form-control @error('image') is-invalid @enderror">
                            @error('image')
                            <span class="text-danger">{{ $message }}</span>
                            @enderror
                        </div>
                        <div class="mb-3">
                            <button type="submit" class="btn btn-success">Upload</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

Normally this blade file will be an extension of the main layout however for this example it encompasses the whole page with the header and CSS source.

The migration

This is if you want to insert details about the image upload into the database.

If you do not want to do this skip this section and delete the DB::table('images')->insert... section of the ImageUploadController store method.

Run: php artisan make:migration images

Find the database/migrations create_images_table.php file and paste:

<?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('images', 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->date('uploaded');
            $table->primary(['id']);
        });
    }

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

Run migrate to create this table: php artisan migrate

Usage

Once logged in navigate to /image-upload

If you upload a non-image you get this notice

Laravel 9 image upload error not an image

or if the image is oversized (greater than 2048 Kilobytes)

Laravel 9 image upload error too large

Upon a successful upload:

Laravel 9 image upload success