Laravel 12 File Upload: How to Upload PDF Files Easily

Author

Kritim Yantra

May 28, 2025

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:

  1. local - stores files in your app's storage/app directory
  2. public - stores files in storage/app/public (accessible via web)
  3. 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:

  1. Validation: Always validate file types, sizes, and content
  2. File Names: Don't use original file names directly - generate safe names
  3. Virus Scanning: Consider integrating virus scanning for uploaded files
  4. Permissions: Set proper file permissions (644 for files, 755 for directories)
  5. Direct Access: Don't allow direct execution of uploaded files
  6. Content-Disposition: Serve PDFs with Content-Disposition: attachment when 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:

  1. Setting up the filesystem configuration
  2. Creating upload forms with proper validation
  3. Storing file information in the database
  4. Displaying and managing uploaded PDFs
  5. Advanced features like thumbnail generation and queue processing
  6. Security considerations and testing
LIVE MENTORSHIP ONLY 5 SPOTS

Laravel Mastery
Coaching Class Program

KritiMyantra

Transform from beginner to Laravel expert with our personalized Coaching Class starting June 13, 2025. Limited enrollment ensures focused attention.

Daily Sessions

1-hour personalized coaching

Real Projects

Build portfolio applications

Best Practices

Industry-standard techniques

Career Support

Interview prep & job guidance

Total Investment
$200
Duration
30 hours
1h/day

Enrollment Closes In

Days
Hours
Minutes
Seconds
Spots Available 5 of 10 remaining
Next cohort starts:
June 13, 2025

Join the Program

Complete your application to secure your spot

Application Submitted!

Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.

What happens next?

  • Confirmation email with program details
  • WhatsApp message from our team
  • Onboarding call to discuss your goals

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts