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://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:
Some files uploaded successfully whilst others failed validation:
All files passed validation:
If no files are selected: