Laravel 12: Upload Single & Multiple Images or Files Using Raw PHP (Step-by-Step Guide)

By monirul islam

  • Web Development, Laravel

Laravel 12: Upload Single & Multiple Images or Files Using Raw PHP (Step-by-Step Guide)

Uploading multiple images or files in Laravel 12 is a common requirement for web applications. Whether you're working with user profile pictures, product images, or documents, handling file uploads efficiently is crucial. In this guide, we'll explore two methods for multiple file uploads in Laravel 12: using Raw PHP.

Prerequisites

Before proceeding, ensure you have the following setup:

  • Laravel 12 installed 

    composer create-project laravel/laravel project-name
  • A database connection configured in .env

  • Composer installed

Setting Up File Upload in Laravel 12

1. create a migration and model to store uploaded file details in the database.

php artisan make:model File -m

Modify the migration file located in database/migrations/xxxx_xx_xx_xxxxxx_create_files_table.php:

public function up()
{
    Schema::create('files', function (Blueprint $table) {
        $table->id();
        $table->string('file_name');
        $table->string('file_path');
        $table->timestamps();
    });
}

Run the migration:

php artisan migrate

Modify app/Models/File.php:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class File extends Model
{
    use HasFactory;
    protected $fillable = ['file_name', 'file_path'];
}

2. Configure Routes & Cotroller

Modify routes/web.php to create the routes:

Route::get('upload', [FileUploadController::class, 'create']);
Route::post('upload', [FileUploadController::class, 'store'])->name('file.upload');

Create a new controller:

php artisan make:controller FileUploadController

Modify app/Http/Controllers/FileUploadController.php:

<?php

namespace App\Http\Controllers;

use App\Models\File;
use Illuminate\Http\Request;

class FileUploadController extends Controller
{
    public function create()
    {
        return view('upload');
    }

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

        if ($request->hasFile('files')) {
            foreach ($request->file('files') as $file) {
                $fileName = time() . '_' . $file->getClientOriginalName();
                $filePath = $file->storeAs('uploads', $fileName, 'public');

                File::create(['file_name' => $fileName, 'file_path' => '/storage/' . $filePath]);
            }
        }

        return response()->json(['success' => 'Files uploaded successfully!']);
    }
}

3. Configure blade file to show on frontend 

Create a file at resources/views/upload.blade.php:

Css :

<style>
        @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');

        body {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Montserrat', sans-serif;
        }

        .form-container {
            width: 100vw;
            height: 100vh;
            background-color: #7b2cbf;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .upload-files-container {
            background-color: #f7fff7;
            width: 420px;
            padding: 30px 60px;
            border-radius: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            box-shadow: rgba(0, 0, 0, 0.24) 0px 10px 20px, rgba(0, 0, 0, 0.28) 0px 6px 6px;
        }

        .drag-file-area {
            border: 2px dashed #7b2cbf;
            border-radius: 40px;
            margin: 10px 0 15px;
            padding: 30px 50px;
            width: 350px;
            text-align: center;
        }

        .drag-file-area .upload-icon {
            font-size: 50px;
        }

        .drag-file-area h3 {
            font-size: 26px;
            margin: 15px 0;
        }

        .drag-file-area label {
            font-size: 19px;
        }

        .drag-file-area label .browse-files-text {
            color: #7b2cbf;
            font-weight: bolder;
            cursor: pointer;
        }

        .browse-files span {
            position: relative;
            top: -25px;
        }

        .default-file-input {
            opacity: 0;
        }

        .cannot-upload-message {
            background-color: #ffc6c4;
            font-size: 17px;
            display: flex;
            align-items: center;
            margin: 5px 0;
            padding: 5px 10px 5px 30px;
            border-radius: 5px;
            color: #BB0000;
            display: none;
        }

        @keyframes fadeIn {
            0% {
                opacity: 0;
            }

            100% {
                opacity: 1;
            }
        }

        .cannot-upload-message span,
        .upload-button-icon {
            padding-right: 10px;
        }

        .cannot-upload-message span:last-child {
            padding-left: 20px;
            cursor: pointer;
        }

        .file-block {
            color: #f7fff7;
            background-color: #7b2cbf;
            transition: all 1s;
            width: 390px;
            position: relative;
            display: none;
            flex-direction: row;
            justify-content: space-between;
            align-items: center;
            margin: 10px 0 15px;
            padding: 10px 20px;
            border-radius: 25px;
            cursor: pointer;
        }

        .file-block.show {
            display: flex;
        }

        .file-info {
            display: flex;
            align-items: center;
            font-size: 15px;
        }

        .file-icon {
            margin-right: 10px;
        }

        .file-name,
        .file-size {
            padding: 0 3px;
        }

        .remove-file-icon {
            cursor: pointer;
        }

        .progress-bar {
            display: flex;
            position: absolute;
            bottom: 0;
            left: 4.5%;
            width: 0;
            height: 5px;
            border-radius: 25px;
            background-color: #4BB543;
        }

        .upload-button {
            font-family: 'Montserrat';
            background-color: #7b2cbf;
            color: #f7fff7;
            display: flex;
            align-items: center;
            font-size: 18px;
            border: none;
            border-radius: 20px;
            margin: 10px;
            padding: 7.5px 50px;
            cursor: pointer;
        }
    </style>

HTML :

{{-- File Upload --}}
    <form class="form-container" enctype="multipart/form-data" method="POST" action="{{ route('file.upload') }}">
        @csrf
        <div class="upload-files-container">
            <div class="drag-file-area">
                <span class="material-icons-outlined upload-icon"> file_upload </span>
                <h3 class="dynamic-message"> Drag & drop any file here </h3>
                <label class="label"> or <span class="browse-files">
                        <!-- Use Multiple to upload multiple -->
                        <input type="file" class="default-file-input" name="files[]" />
                        <span class="browse-files-text">browse file</span>
                        <span>from device</span>
                    </span>
                </label>
            </div>
            <span class="cannot-upload-message"> <span class="material-icons-outlined">error</span> Please select a file
                first <span class="material-icons-outlined cancel-alert-button">cancel</span> </span>
            <div class="file-block">
                <div class="file-info"> <span class="material-icons-outlined file-icon">description</span> <span
                        class="file-name"> </span> | <span class="file-size"> </span> </div>
                <span class="material-icons remove-file-icon">delete</span>
                <div class="progress-bar"> </div>
            </div>
            <button type="button" class="upload-button"> Upload </button>
        </div>
    </form>

Use multiple on the input to upload multiple picture at the same time.

Add JS :

<script>
        var isAdvancedUpload = function() {
            var div = document.createElement('div');
            return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window &&
                'FileReader' in window;
        }();

        let draggableFileArea = document.querySelector(".drag-file-area");
        let browseFileText = document.querySelector(".browse-files");
        let uploadIcon = document.querySelector(".upload-icon");
        let dragDropText = document.querySelector(".dynamic-message");
        const fileInput = document.querySelector(".default-file-input");
        const fileListContainer = document.querySelector(".upload-files-container");
        const uploadButton = document.querySelector(".upload-button");
        let cannotUploadMessage = document.querySelector(".cannot-upload-message");
        let cancelAlertButton = document.querySelector(".cancel-alert-button");
        let uploadedFile = document.querySelector(".file-block");
        let fileName = document.querySelector(".file-name");
        let fileSize = document.querySelector(".file-size");
        let progressBar = document.querySelector(".progress-bar");
        let removeFileButton = document.querySelector(".remove-file-icon");
        let fileFlag = 0;
        const uploadedFiles = [];
        fileInput.addEventListener("click", () => {
            fileInput.value = '';
            console.log(fileInput.value);
        });

        fileInput.addEventListener("change", () => {
            uploadedFiles.length = 0;
            document.querySelectorAll(".file-block").forEach(e => e.remove());

            Array.from(fileInput.files).forEach((file, index) => {
                uploadedFiles.push(file);

                const fileBlock = document.createElement("div");
                fileBlock.classList.add("file-block");
                fileBlock.classList.add("show");

                fileBlock.innerHTML = `
                <div class="file-info">
                    <span class="material-icons-outlined file-icon">description</span>
                    <span class="file-name">${file.name}</span> |
                    <span class="file-size">${(file.size / 1024).toFixed(1)} KB</span>
                </div>
                <span class="material-icons remove-file-icon" data-index="${index}">delete</span>
                <div class="progress-bar" id="progress-${index}"></div>
            `;

                fileListContainer.insertBefore(fileBlock, uploadButton);
            });
        });

        uploadButton.addEventListener("click", () => {
            if (uploadedFiles.length === 0) {
                alert("Please select files first.");
                return;
            }
            let uploadsCompleted = 0;
            uploadedFiles.forEach((file, index) => {
                const formData = new FormData();
                formData.append("files[]", file);

                // Create file block with progress bar
                const fileBlock = document.createElement("div");
                fileBlock.className = "file-block";
                fileBlock.innerHTML = `
                    <div class="file-info">
                        <span class="material-icons-outlined file-icon">description</span>
                        <span class="file-name">${file.name}</span> |
                        <span class="file-size">${(file.size / 1024).toFixed(1)} KB</span>
                    </div>
                    <div class="progress-bar" id="progress-${index}" style="width:0%"></div>
                `;
                document.querySelector(".upload-files-container").appendChild(fileBlock);

                // Create AJAX request with progress tracking
                const xhr = new XMLHttpRequest();
                xhr.open("POST", "{{ route('file.upload') }}", true);
                xhr.setRequestHeader("X-CSRF-TOKEN", "{{ csrf_token() }}");

                xhr.upload.addEventListener("progress", (e) => {
                    if (e.lengthComputable) {
                        const percent = (e.loaded / e.total) * 100;
                        const progressBar = document.getElementById(`progress-${index}`);
                        progressBar.style.width = `${percent}%`;
                        progressBar.innerText = percent < 100 ? "Uploading..." : "Processing...";
                    }
                });

                xhr.onload = () => {
                    const progressBar = document.getElementById(`progress-${index}`);
                    if (xhr.status === 200) {
                        progressBar.style.backgroundColor = "#4caf50";
                        progressBar.innerText = "Uploaded ✔";
                        console.log("Uploaded:", xhr.responseText);
                    } else {
                        progressBar.style.backgroundColor = "#f44336";
                        progressBar.innerText = "Failed ✘";
                        console.error("Upload error:", xhr.responseText);
                    }
                    uploadsCompleted++;
                    if (uploadsCompleted === uploadedFiles.length) {
                        showSuccessAlert(); // ✅ Only runs once
                    }
                };

                xhr.onerror = () => {
                    const progressBar = document.getElementById(`progress-${index}`);
                    progressBar.style.backgroundColor = "#f44336";
                    progressBar.innerText = "Failed ✘";
                };

                xhr.send(formData);
            });
        });

        function showSuccessAlert() {
            const successMessage = document.createElement("div");
            successMessage.innerHTML = "🎉 All uploads finished!";
            successMessage.style.cssText = `
        background: #4caf50;
        color: white;
        padding: 12px;
        margin-top: 20px;
        border-radius: 6px;
        text-align: center;
        font-weight: bold;
        animation: fadeIn 0.3s ease-in-out;
    `;
            document.querySelector(".upload-files-container").appendChild(successMessage);
        }


        cancelAlertButton.addEventListener("click", () => {
            cannotUploadMessage.style.cssText = "display: none;";
        });

        if (isAdvancedUpload) {
            ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(evt =>
                draggableFileArea.addEventListener(evt, e => {
                    e.preventDefault();
                    e.stopPropagation();
                })
            );

            ["dragover", "dragenter"].forEach(evt => {
                draggableFileArea.addEventListener(evt, e => {
                    e.preventDefault();
                    e.stopPropagation();
                    uploadIcon.innerHTML = 'file_download';
                    dragDropText.innerHTML = 'Drop your file here!';
                });
            });

            draggableFileArea.addEventListener("drop", e => {
                uploadIcon.innerHTML = 'check_circle';
                dragDropText.innerHTML = 'File Dropped Successfully!';
                document.querySelector(".label").innerHTML =
                    `drag & drop or <span class="browse-files"> <input type="file" class="default-file-input" style=""/> <span class="browse-files-text" style="top: -23px; left: -20px;"> browse file</span> </span>`;
                uploadButton.innerHTML = `Upload`;

                let files = e.dataTransfer.files;
                fileInput.files = files;
                console.log(files[0].name + " " + files[0].size);
                console.log(document.querySelector(".default-file-input").value);
                fileName.innerHTML = files[0].name;
                fileSize.innerHTML = (files[0].size / 1024).toFixed(1) + " KB";
                uploadedFile.style.cssText = "display: flex;";
                progressBar.style.width = 0;
                fileFlag = 0;
            });
        }

        document.addEventListener("click", function(e) {
            if (e.target.classList.contains("remove-file-icon")) {
                const index = e.target.getAttribute("data-index");
                uploadedFiles.splice(index, 1);
                e.target.closest(".file-block").remove();
            }
        });
    </script>

 

All the required steps have been done, now you have to run the Laravel app to see the update:

output will look like this when you go into the /upload route:

 

Hope this helps you, Happy coding.

Let's chat with me? - Online
Please fill out the form below to start chatting with me directly.