Kritim Yantra
May 28, 2025
Uploading files is a common requirement in modern web applications. Whether you're building a document management system, a job application portal, or an educational platform, handling PDF uploads is an essential skill. In this comprehensive guide, we'll walk through how to implement PDF file uploads in Laravel 12 with proper validation, storage, and security considerations.
PDF (Portable Document Format) has become the standard for document sharing because:
In Laravel 12, handling file uploads is straightforward thanks to its powerful filesystem abstraction and built-in validation features.
Before we dive into file uploads, let's ensure you have a proper Laravel 12 environment:
composer create-project laravel/laravel laravel-pdf-upload
cd laravel-pdf-upload
For this tutorial, we'll assume you're working with a fresh Laravel installation.
Laravel's filesystem configuration is located in config/filesystems.php
. By default, Laravel supports several drivers:
local
- stores files in your app's storage/app
directorypublic
- stores files in storage/app/public
(accessible via web)s3
- Amazon S3 cloud storageFor PDF uploads that need to be publicly accessible, we'll use the public
disk.
First, create a symbolic link from public/storage
to storage/app/public
:
php artisan storage:link
This allows web users to access stored files while keeping them outside the public directory for security.
Let's create a basic form for uploading PDF files. We'll add this to a new route and view.
Add this to your routes/web.php
:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PdfController;
Route::get('/upload', [PdfController::class, 'showUploadForm']);
Route::post('/upload', [PdfController::class, 'uploadPdf'])->name('pdf.upload');
Create a new controller:
php artisan make:controller PdfController
Now edit app/Http/Controllers/PdfController.php
:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PdfController extends Controller
{
public function showUploadForm()
{
return view('upload');
}
public function uploadPdf(Request $request)
{
// We'll implement the upload logic here next
}
}
Create a new file resources/views/upload.blade.php
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Upload</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md overflow-hidden p-6">
<h1 class="text-2xl font-bold text-gray-800 mb-6">Upload PDF Document</h1>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
@if($errors->any())
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('pdf.upload') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
@csrf
<div>
<label for="pdf" class="block text-sm font-medium text-gray-700 mb-1">Select PDF File</label>
<input type="file" id="pdf" name="pdf" accept=".pdf" class="block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100">
</div>
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Document Title</label>
<input type="text" id="title" name="title" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm
focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border">
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded
focus:outline-none focus:shadow-outline transition duration-150">
Upload PDF
</button>
</form>
</div>
</div>
</body>
</html>
Now let's complete the uploadPdf
method in our controller with proper validation and file handling:
public function uploadPdf(Request $request)
{
// Validate the request
$validated = $request->validate([
'title' => 'required|string|max:255',
'pdf' => 'required|file|mimes:pdf|max:10240', // Max 10MB
]);
// Store the PDF file
$pdfPath = $request->file('pdf')->store('pdfs', 'public');
// Create a record in the database (optional)
$pdf = new \App\Models\Pdf();
$pdf->title = $validated['title'];
$pdf->file_path = $pdfPath;
$pdf->original_name = $request->file('pdf')->getClientOriginalName();
$pdf->save();
return back()->with('success', 'PDF uploaded successfully!');
}
To store information about uploaded PDFs, let's create a model and migration:
php artisan make:model Pdf -m
Edit the migration file in database/migrations/xxxx_create_pdfs_table.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('pdfs', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('file_path');
$table->string('original_name');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('pdfs');
}
};
Run the migration:
php artisan migrate
Let's add functionality to display uploaded PDFs. First, add a new route:
Route::get('/pdfs', [PdfController::class, 'index'])->name('pdf.list');
Then add the method to the controller:
public function index()
{
$pdfs = \App\Models\Pdf::latest()->get();
return view('pdfs', compact('pdfs'));
}
Create a new view resources/views/pdfs.blade.php
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Uploaded PDFs</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-800">Uploaded PDF Documents</h1>
<a href="{{ route('pdf.upload') }}" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded
focus:outline-none focus:shadow-outline transition duration-150">
Upload New PDF
</a>
</div>
@if($pdfs->isEmpty())
<div class="bg-white rounded-lg shadow-md p-6 text-center">
<p class="text-gray-600">No PDFs have been uploaded yet.</p>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($pdfs as $pdf)
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div class="p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-2">{{ $pdf->title }}</h2>
<p class="text-gray-600 mb-4">Uploaded: {{ $pdf->created_at->format('M d, Y') }}</p>
<div class="flex space-x-2">
<a href="{{ asset('storage/'.$pdf->file_path) }}" target="_blank"
class="bg-blue-100 text-blue-800 px-3 py-1 rounded text-sm font-medium hover:bg-blue-200 transition">
View PDF
</a>
<a href="{{ asset('storage/'.$pdf->file_path) }}" download="{{ $pdf->original_name }}"
class="bg-green-100 text-green-800 px-3 py-1 rounded text-sm font-medium hover:bg-green-200 transition">
Download
</a>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
</body>
</html>
You might want to add more specific validation for PDFs. Create a custom rule:
php artisan make:rule ValidPdf
Edit app/Rules/ValidPdf.php
:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Smalot\PdfParser\Parser;
class ValidPdf implements Rule
{
public function passes($attribute, $value)
{
try {
$parser = new Parser();
$pdf = $parser->parseContent(file_get_contents($value->getRealPath()));
return !empty($pdf->getText());
} catch (\Exception $e) {
return false;
}
}
public function message()
{
return 'The :attribute must be a valid, non-corrupt PDF file.';
}
}
Install the PDF parser package:
composer require smalot/pdfparser
Now use it in your validation:
use App\Rules\ValidPdf;
// In your validation rules
'pdf' => [
'required',
'file',
'mimes:pdf',
'max:10240',
new ValidPdf(),
],
To generate thumbnails for PDFs, you can use the Imagick extension:
sudo apt-get install php-imagick # For Ubuntu
Then modify your upload method:
public function uploadPdf(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'pdf' => 'required|file|mimes:pdf|max:10240',
]);
$pdfPath = $request->file('pdf')->store('pdfs', 'public');
// Generate thumbnail
$thumbnailPath = $this->generatePdfThumbnail($request->file('pdf'));
$pdf = new \App\Models\Pdf();
$pdf->title = $validated['title'];
$pdf->file_path = $pdfPath;
$pdf->thumbnail_path = $thumbnailPath;
$pdf->original_name = $request->file('pdf')->getClientOriginalName();
$pdf->save();
return back()->with('success', 'PDF uploaded successfully!');
}
protected function generatePdfThumbnail($pdfFile)
{
$imagick = new \Imagick();
$imagick->readImage($pdfFile->getRealPath().'[0]'); // First page
$imagick->setImageFormat('jpg');
$imagick->setImageCompressionQuality(90);
$imagick->thumbnailImage(200, 200, true, true);
$thumbnailPath = 'thumbnails/'.uniqid().'.jpg';
Storage::disk('public')->put($thumbnailPath, $imagick);
return $thumbnailPath;
}
For large PDFs or intensive processing, use Laravel queues:
First, create a job:
php artisan make:job ProcessPdfUpload
Edit app/Jobs/ProcessPdfUpload.php
:
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Pdf;
use Illuminate\Support\Facades\Storage;
class ProcessPdfUpload implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $pdf;
public function __construct(Pdf $pdf)
{
$this->pdf = $pdf;
}
public function handle()
{
// Perform your PDF processing here
// Example: Extract text, generate thumbnails, send notifications, etc.
$pdfPath = Storage::disk('public')->path($this->pdf->file_path);
// Example processing - extract text
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($pdfPath);
$text = $pdf->getText();
$this->pdf->text_content = substr($text, 0, 1000); // Store first 1000 chars
$this->pdf->save();
}
}
Then modify your upload method to dispatch the job:
public function uploadPdf(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'pdf' => 'required|file|mimes:pdf|max:10240',
]);
$pdfPath = $request->file('pdf')->store('pdfs', 'public');
$pdf = new \App\Models\Pdf();
$pdf->title = $validated['title'];
$pdf->file_path = $pdfPath;
$pdf->original_name = $request->file('pdf')->getClientOriginalName();
$pdf->save();
// Dispatch the job
ProcessPdfUpload::dispatch($pdf);
return back()->with('success', 'PDF uploaded successfully! Processing in background...');
}
When handling file uploads, security should be a top priority:
Content-Disposition: attachment
when appropriateCreate a test to ensure your upload functionality works correctly:
php artisan make:test PdfUploadTest
Edit tests/Feature/PdfUploadTest.php
:
<?php
namespace Tests\Feature;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class PdfUploadTest extends TestCase
{
public function test_pdf_upload()
{
Storage::fake('public');
$file = UploadedFile::fake()->create('document.pdf', 500, 'application/pdf');
$response = $this->post(route('pdf.upload'), [
'title' => 'Test PDF',
'pdf' => $file,
]);
$response->assertRedirect();
$response->assertSessionHas('success');
Storage::disk('public')->assertExists('pdfs/'.$file->hashName());
$this->assertDatabaseHas('pdfs', [
'title' => 'Test PDF',
'original_name' => 'document.pdf',
]);
}
public function test_invalid_file_upload()
{
Storage::fake('public');
$file = UploadedFile::fake()->create('document.txt', 500, 'text/plain');
$response = $this->post(route('pdf.upload'), [
'title' => 'Test PDF',
'pdf' => $file,
]);
$response->assertSessionHasErrors('pdf');
}
}
Run the tests:
php artisan test
In this comprehensive guide, we've covered everything you need to implement PDF file uploads in Laravel 12:
Transform from beginner to Laravel expert with our personalized Coaching Class starting June 13, 2025. Limited enrollment ensures focused attention.
1-hour personalized coaching
Build portfolio applications
Industry-standard techniques
Interview prep & job guidance
Complete your application to secure your spot
Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google