Advanced Laravel

Advanced File Handling & Media Libraries

15 min Lesson 14 of 40

Advanced File Handling & Media Libraries

Managing files and media in Laravel applications requires more than basic file uploads. In this lesson, we'll explore the powerful Spatie Media Library package, advanced image manipulation techniques, file conversions, cloud storage integration, signed URLs, and streaming capabilities for building robust media management systems.

Installing Spatie Media Library

Spatie Media Library is a feature-rich package for associating files with Eloquent models.

# Install the package composer require "spatie/laravel-medialibrary:^11.0" # Publish migration and config php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations" php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config" # Run migrations php artisan migrate # Optional: Install image manipulation dependencies composer require "spatie/image" # For advanced image processing, install Imagick or GD # Ubuntu/Debian: sudo apt-get install php-imagick # Or: sudo apt-get install php-gd
Note: The Spatie Media Library requires either the Imagick or GD PHP extension for image manipulation. Imagick provides better performance and more features.

Basic Media Library Usage

Add media capabilities to your models using the HasMedia interface and InteractsWithMedia trait.

namespace App\Models; use Illuminate\Database\Eloquent\Model; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; class Product extends Model implements HasMedia { use InteractsWithMedia; // Register media collections public function registerMediaCollections(): void { $this->addMediaCollection('images') ->useDisk('public'); $this->addMediaCollection('documents') ->acceptsMimeTypes(['application/pdf', 'application/msword']) ->useDisk('s3'); // Single file collection $this->addMediaCollection('thumbnail') ->singleFile() ->useDisk('public'); } // Register media conversions (thumbnails, optimized versions) public function registerMediaConversions(Media $media = null): void { $this->addMediaConversion('thumb') ->width(300) ->height(300) ->sharpen(10) ->performOnCollections('images'); $this->addMediaConversion('detail') ->width(1200) ->height(800) ->format('webp') ->quality(90) ->performOnCollections('images'); $this->addMediaConversion('preview') ->width(600) ->height(400) ->nonQueued() // Generate immediately ->performOnCollections('images'); } } // Upload files in controller class ProductController extends Controller { public function store(Request $request) { $product = Product::create($request->validated()); // Upload single file if ($request->hasFile('thumbnail')) { $product->addMediaFromRequest('thumbnail') ->toMediaCollection('thumbnail'); } // Upload multiple files if ($request->hasFile('images')) { foreach ($request->file('images') as $image) { $product->addMedia($image) ->withCustomProperties(['order' => $loop->index]) ->toMediaCollection('images'); } } return redirect()->route('products.show', $product); } public function show(Product $product) { // Get first media item $thumbnail = $product->getFirstMediaUrl('thumbnail'); // Get all media items $images = $product->getMedia('images'); // Get specific conversion URL $thumbUrl = $product->getFirstMediaUrl('images', 'thumb'); $detailUrl = $product->getFirstMediaUrl('images', 'detail'); return view('products.show', compact('product', 'thumbnail', 'images')); } }

Advanced Image Manipulation

Apply sophisticated image transformations using Spatie Image and custom manipulations.

use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\Image\Manipulations; class Post extends Model implements HasMedia { use InteractsWithMedia; public function registerMediaConversions(Media $media = null): void { // Basic resizing $this->addMediaConversion('thumb') ->width(200) ->height(200) ->sharpen(10); // Fit methods $this->addMediaConversion('square') ->fit(Manipulations::FIT_CROP, 500, 500) ->background('#ffffff'); $this->addMediaConversion('contain') ->fit(Manipulations::FIT_CONTAIN, 800, 600) ->background('#f5f5f5'); // Format conversion $this->addMediaConversion('webp') ->format('webp') ->quality(85); // Watermark $this->addMediaConversion('watermarked') ->width(1200) ->watermark(public_path('images/watermark.png')) ->watermarkPosition('bottom-right') ->watermarkPadding(10, 10); // Multiple manipulations $this->addMediaConversion('optimized') ->width(1920) ->height(1080) ->fit(Manipulations::FIT_MAX, 1920, 1080) ->format('webp') ->quality(90) ->optimize() ->sharpen(5); // Responsive images $this->addMediaConversion('responsive') ->withResponsiveImages() ->format('webp'); // Custom manipulation using callback $this->addMediaConversion('custom') ->manipulate(function (Image $image) { return $image->greyscale() ->blur(10) ->brightness(50); }); } } // Custom image manipulator use Spatie\Image\Image; use Spatie\Image\Manipulations; class ImageManipulator { public static function createCollage(array $imagePaths, string $outputPath) { $image = Image::load($imagePaths[0]) ->fit(Manipulations::FIT_CROP, 500, 500); // Add more images side by side foreach (array_slice($imagePaths, 1) as $path) { $nextImage = Image::load($path) ->fit(Manipulations::FIT_CROP, 500, 500); // Combine images (requires custom implementation) } $image->save($outputPath); } public static function addTextOverlay(string $imagePath, string $text, string $outputPath) { $image = Image::load($imagePath); // This requires Imagick $image->manipulate(function ($imagick) use ($text) { $draw = new \ImagickDraw(); $draw->setFontSize(50); $draw->setFillColor('white'); $draw->setGravity(\Imagick::GRAVITY_CENTER); $imagick->annotateImage($draw, 0, 0, 0, $text); return $imagick; }); $image->save($outputPath); } }
Tip: Use the withResponsiveImages() method to automatically generate multiple sizes for responsive images. This creates srcset-compatible image sets that browsers can choose from based on viewport size.

Cloud Storage Integration

Store media files on cloud services like S3, Cloudinary, or DigitalOcean Spaces.

// Configure filesystems in config/filesystems.php 'disks' => [ 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), 'visibility' => 'public', ], 'cloudinary' => [ 'driver' => 'cloudinary', 'cloud_name' => env('CLOUDINARY_CLOUD_NAME'), 'api_key' => env('CLOUDINARY_API_KEY'), 'api_secret' => env('CLOUDINARY_API_SECRET'), ], 'spaces' => [ 'driver' => 's3', 'key' => env('DO_SPACES_KEY'), 'secret' => env('DO_SPACES_SECRET'), 'endpoint' => env('DO_SPACES_ENDPOINT'), 'region' => env('DO_SPACES_REGION'), 'bucket' => env('DO_SPACES_BUCKET'), ], ], // Use different disks for different collections class Video extends Model implements HasMedia { use InteractsWithMedia; public function registerMediaCollections(): void { // Store thumbnails locally $this->addMediaCollection('thumbnails') ->useDisk('public'); // Store videos on S3 $this->addMediaCollection('videos') ->useDisk('s3'); // Store transcoded videos on Cloudinary $this->addMediaCollection('transcoded') ->useDisk('cloudinary'); } } // Direct upload to S3 use Illuminate\Support\Facades\Storage; public function uploadToS3(Request $request) { $path = $request->file('video')->store('videos', 's3'); // Get public URL $url = Storage::disk('s3')->url($path); Video::create([ 'title' => $request->title, 'path' => $path, 'url' => $url, ]); }
Warning: Always validate file uploads on the server side. Check file types, sizes, and use virus scanning for user-uploaded files. Never trust client-side validation alone.

Signed URLs for Private Files

Generate temporary, secure URLs for accessing private files without exposing permanent download links.

use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\URL; // Generate temporary signed URL (expires in 30 minutes) $url = Storage::disk('s3')->temporaryUrl( 'private/document.pdf', now()->addMinutes(30) ); // Custom signed URL for protected downloads class DocumentController extends Controller { public function show(Document $document) { // Check permissions if (!auth()->user()->canView($document)) { abort(403); } // Generate signed URL $signedUrl = URL::temporarySignedRoute( 'documents.download', now()->addHours(1), ['document' => $document->id] ); return view('documents.show', [ 'document' => $document, 'downloadUrl' => $signedUrl, ]); } public function download(Request $request, Document $document) { // Verify signed URL if (!$request->hasValidSignature()) { abort(401, 'Invalid or expired download link'); } // Check permissions again if (!auth()->user()->canView($document)) { abort(403); } return Storage::disk('s3')->download($document->path, $document->filename); } } // In routes/web.php Route::get('/documents/{document}/download', [DocumentController::class, 'download']) ->name('documents.download') ->middleware('signed'); // Media library with signed URLs class ProtectedDocument extends Model implements HasMedia { use InteractsWithMedia; public function registerMediaCollections(): void { $this->addMediaCollection('files') ->useDisk('s3-private'); } public function getTemporaryUrl($expiresAt = null) { $expiresAt = $expiresAt ?? now()->addHour(); return $this->getFirstMedia('files') ->getTemporaryUrl($expiresAt); } }

File Streaming and Range Requests

Implement efficient file streaming for large files, videos, and audio with support for range requests.

use Symfony\Component\HttpFoundation\StreamedResponse; class VideoStreamController extends Controller { public function stream(Video $video) { // Check authorization if (!auth()->user()->canWatch($video)) { abort(403); } $path = $video->getFirstMedia('videos')->getPath(); $size = filesize($path); $start = 0; $end = $size - 1; // Handle range request for seeking if (request()->header('Range')) { $range = request()->header('Range'); list(, $range) = explode('=', $range, 2); if (strpos($range, ',') !== false) { return response('Requested Range Not Satisfiable', 416) ->header('Content-Range', "bytes */{$size}"); } if ($range == '-') { $start = $size - substr($range, 1); } else { $range = explode('-', $range); $start = $range[0]; $end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $end; } $start = max($start, 0); $end = min($end, $size - 1); $length = $end - $start + 1; return response()->stream(function () use ($path, $start, $length) { $stream = fopen($path, 'rb'); fseek($stream, $start); echo fread($stream, $length); fclose($stream); }, 206, [ 'Content-Type' => 'video/mp4', 'Content-Length' => $length, 'Content-Range' => "bytes {$start}-{$end}/{$size}", 'Accept-Ranges' => 'bytes', ]); } // Full file stream return response()->stream(function () use ($path) { $stream = fopen($path, 'rb'); fpassthru($stream); fclose($stream); }, 200, [ 'Content-Type' => 'video/mp4', 'Content-Length' => $size, 'Accept-Ranges' => 'bytes', ]); } // Adaptive streaming for HLS public function streamHls(Video $video) { $playlist = $video->getFirstMedia('playlists')->getPath(); return response()->file($playlist, [ 'Content-Type' => 'application/vnd.apple.mpegurl', ]); } }
Exercise 1: Build a product gallery system using Spatie Media Library. Implement drag-and-drop upload, automatic thumbnail generation in multiple sizes (thumb, medium, large), and the ability to reorder images. Include image deletion and a lightbox viewer.
Exercise 2: Create a document management system with access controls. Store files on S3, generate temporary signed URLs that expire after 1 hour, track downloads, and implement virus scanning using ClamAV or a cloud service before allowing downloads.
Exercise 3: Build a video streaming platform with support for multiple quality levels. Implement video upload with automatic transcoding to 360p, 720p, and 1080p. Generate thumbnail previews at multiple timestamps and support range requests for seeking during playback.