Laravel 12 File Upload: How to Upload PDF Files Easily
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.
Why PDF Uploads Matter in Web Applications
PDF (Portable Document Format) has become the standard for document sharing because:
- It preserves formatting across devices and platforms
- It's widely supported and can contain text, images, and interactive elements
- It's more secure than editable formats for official documents
In Laravel 12, handling file uploads is straightforward thanks to its powerful filesystem abstraction and built-in validation features.
Setting Up Your Laravel 12 Project
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.
Configuring the Filesystem
Laravel's filesystem configuration is located in config/filesystems.php. By default, Laravel supports several drivers:
local- stores files in your app'sstorage/appdirectorypublic- stores files instorage/app/public(accessible via web)s3- Amazon S3 cloud storage
For 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.
Creating the PDF Upload Form
Let's create a basic form for uploading PDF files. We'll add this to a new route and view.
1. Create the Route
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');
2. Generate the Controller
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
}
}
3. Create the View
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>
Implementing the PDF Upload Logic
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!');
}
Creating the PDF Model and Migration
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
Displaying Uploaded PDFs
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>
Advanced Features
1. PDF Validation with Custom Rules
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(),
],
2. PDF Thumbnail Generation
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;
}
3. Queueing PDF Processing
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...');
}
Security Considerations
When handling file uploads, security should be a top priority:
- Validation: Always validate file types, sizes, and content
- File Names: Don't use original file names directly - generate safe names
- Virus Scanning: Consider integrating virus scanning for uploaded files
- Permissions: Set proper file permissions (644 for files, 755 for directories)
- Direct Access: Don't allow direct execution of uploaded files
- Content-Disposition: Serve PDFs with
Content-Disposition: attachmentwhen appropriate
Testing Your PDF Upload
Create 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
Conclusion
In this comprehensive guide, we've covered everything you need to implement PDF file uploads in Laravel 12:
- Setting up the filesystem configuration
- Creating upload forms with proper validation
- Storing file information in the database
- Displaying and managing uploaded PDFs
- Advanced features like thumbnail generation and queue processing
- Security considerations and testing
Projects to explore next
Source-code projects from the KritiMyantra catalogue. Check each project's details before buying.
Flask Authentication System (Free Version) - Kritim Yantra
🛠 Features✔ Google & GitHub Authentication✔ User Registration & Login✔ Profile/Dashboard Access✔ Secure & Mi...
Flask Blog CMS with Admin Dashboard — Python Source Code
A Flask-based blogging website with a built-in admin dashboard. Manage articles, images, categories, tags and featured p...
Laravel 12 + ReactJS Starter Kit: Add Multi-Language Support (Arabic, English, Spanish) with Blog CRUD
🚀 Installation & Setup Download & Extract Get the ZIP from kritimyantra.com and extract it. Includes preconfigu...
Comments
No comments yet. Be the first to comment.
Sign in to join the discussion.