maccesar / laravel-dropzone-enhanced
Enhanced Dropzone.js component for Laravel with file upload, thumbnails, and photo management
Requires
- php: ^7.4|^8.0|^8.2
- laravel/framework: ^8.0|^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^6.0|^7.0|^8.0
- phpunit/phpunit: ^9.0
README
A powerful and customizable Laravel package that enhances Dropzone.js to provide an elegant and efficient image upload and management solution for your Eloquent models.
Features
- Seamless Integration: Add a complete image management UI to your models with a single trait and two Blade components.
- Standalone & Dependency-Free: Works out-of-the-box with no need for external libraries like Glide.
- Automatic Thumbnail Generation: Natively processes and creates thumbnails for fast-loading galleries.
- Full Management UI: Includes drag & drop reordering, main image selection, lightbox preview, and secure deletion.
- Highly Customizable: Configure everything from image dimensions and quality to storage disks and route middleware.
- Broad Compatibility: Supports Laravel 8, 9, 10, and 11.
Requirements
- PHP 7.4 or higher
- Laravel 8.0 or higher
Installation
1. Install via Composer
composer require maccesar/laravel-dropzone-enhanced
2. Run the Installer This command publishes the config file, migrations, and assets.
php artisan dropzone-enhanced:install
3. Run Migrations
php artisan migrate
4. Link Storage Ensure your public storage disk is linked so images are accessible.
php artisan storage:link
Quickstart: A Practical Example
This guide shows the most common use case: managing photos for an existing model in an edit form.
Step 1: Prepare Your Model
Add the HasPhotos
trait to any Eloquent model you want to associate with images.
// app/Models/Product.php namespace App\Models; use Illuminate\Database\Eloquent\Model; use MacCesar\LaravelDropzoneEnhanced\Traits\HasPhotos; class Product extends Model { use HasPhotos; // ... your other model properties }
Step 2: Implement the View
In your Blade view (e.g., resources/views/products/edit.blade.php
), add the two components. They work together to provide the full experience.
{{-- resources/views/products/edit.blade.php --}} @extends('layouts.app') @section('content') <h1>Edit Product: {{ $product->name }}</h1> <form action="{{ route('products.update', $product) }}" method="POST"> @csrf @method('PUT') {{-- Your other form fields --}} <div> <label for="name">Product Name</label> <input id="name" name="name" type="text" value="{{ $product->name }}"> </div> <hr> {{-- 1. UPLOAD NEW PHOTOS --}} <h3>Add New Photos</h3> <x-dropzone-enhanced::area :max-files="10" :max-filesize="5" :model="$product" directory="products" /> <hr> {{-- 2. MANAGE EXISTING PHOTOS --}} <h3>Manage Existing Photos</h3> <p>Drag to reorder, click the star to set the main photo, or use the trash icon to delete.</p> <x-dropzone-enhanced::photos :lightbox="true" :model="$product" /> <button type="submit">Save Changes</button> </form> @endsection
How It Works
- The
<x-dropzone-enhanced::area />
component provides the Dropzone interface to upload new images, which are automatically associated with the same$product
. - The
<x-dropzone-enhanced::photos />
component displays the gallery of already uploaded images for the given$product
, enabling management actions (reorder, delete, set main).
Component Reference
Uploader: <x-dropzone-enhanced::area />
This component provides the file upload interface.
Parameter | Type | Description | Default |
---|---|---|---|
:model |
Model |
Required. The Eloquent model instance to attach photos to. | |
directory |
string |
Required. The subdirectory within your storage disk to save the images. | |
dimensions |
string |
Max dimensions for resize (e.g., "1920x1080"). | config('dropzone.images.default_dimensions') |
preResize |
bool |
Whether to resize the image in the browser before upload. | config('dropzone.images.pre_resize') |
maxFiles |
int |
Maximum number of files allowed to be uploaded. | config('dropzone.images.max_files') |
maxFilesize |
int |
Maximum file size in MB. | config('dropzone.images.max_filesize') |
reloadOnSuccess |
bool |
If true , the page will automatically reload after all uploads are successfully completed. |
false |
Gallery: <x-dropzone-enhanced::photos />
This component displays and manages existing photos for a model.
Parameter | Type | Description | Default |
---|---|---|---|
:model |
Model |
Required. The Eloquent model instance whose photos you want to display. | |
:lightbox |
bool |
Enables or disables the lightbox preview when clicking an image. | true |
Advanced Usage
Working with the HasPhotos
Trait
The trait adds several useful methods to your model:
// Get all associated photos as a Collection (ordered by sort_order) $product->photos; // Get the main photo model instance $photo = $product->mainPhoto(); // Get the URL of the main photo (original) $url = $product->getMainPhotoUrl(); // Get the thumbnail URL of the main photo (default dimensions from config) $thumbUrl = $product->getMainPhotoThumbnailUrl(); // Get custom processed images (NEW in v2.1) $mainPhoto = $product->mainPhoto(); $customUrl = $mainPhoto?->getUrl('400x400'); // Square 400x400 $webpUrl = $mainPhoto?->getUrl('800x600', 'webp'); // WebP format $qualityUrl = $mainPhoto?->getUrl('400x400', 'jpg', 85); // Custom quality // Set a specific photo as the main one $product->setMainPhoto($photoId); // Check if the model has any photos if ($product->hasPhotos()) { // ... } // Delete all photos associated with the model $product->deleteAllPhotos();
Advanced Customization Examples
Custom Upload Controller
Create a custom controller to extend the package's functionality:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use MacCesar\LaravelDropzoneEnhanced\Http\Controllers\DropzoneController; use MacCesar\LaravelDropzoneEnhanced\Models\Photo; class CustomDropzoneController extends DropzoneController { public function upload(Request $request) { // Add custom validation rules $request->validate([ 'file' => 'required|image|mimes:jpeg,png,webp|dimensions:min_width=800,min_height=600', 'directory' => 'required|string', 'model_id' => 'required|integer', 'model_type' => 'required|string', ]); // Custom processing before upload $file = $request->file('file'); // Add watermark, custom processing, etc. $this->processImageBeforeUpload($file); // Call parent upload method return parent::upload($request); } private function processImageBeforeUpload($file) { // Your custom image processing logic here // Example: Add watermark, EXIF data removal, etc. } protected function userCanDeletePhoto(Request $request, Photo $photo, $model) { // Add custom authorization logic if ($model instanceof \App\Models\Product) { // Check if user owns the product's company if ($model->company_id !== auth()->user()->company_id) { return false; } } // Call parent method for default checks return parent::userCanDeletePhoto($request, $photo, $model); } }
Then register your custom controller in your routes:
// In routes/web.php use App\Http\Controllers\CustomDropzoneController; Route::post('dropzone/upload', [CustomDropzoneController::class, 'upload']); Route::delete('dropzone/photos/{id}', [CustomDropzoneController::class, 'destroy']);
Multiple Upload Areas for Different Photo Types
Handle different image categories for the same model:
{{-- Main product gallery --}} <div class="mb-8"> <h3 class="text-lg font-semibold mb-4">Product Gallery</h3> <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->id }}/gallery" dimensions="1200x800" :maxFiles="10" :preResize="true" /> <x-dropzone-enhanced::photos :model="$product" /> </div> {{-- Technical specifications images --}} <div class="mb-8"> <h3 class="text-lg font-semibold mb-4">Technical Specifications</h3> <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->id }}/specs" dimensions="1920x1080" :maxFiles="5" /> </div> {{-- Thumbnail/avatar images --}} <div class="mb-8"> <h3 class="text-lg font-semibold mb-4">Product Thumbnails</h3> <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->id }}/thumbs" dimensions="400x400" :maxFiles="3" :preResize="true" /> </div>
Working with Photo Data
Access and manipulate photo metadata:
// Get photo information $photo = $product->photos->first(); echo $photo->filename; // UUID filename echo $photo->original_filename; // Original upload name echo $photo->extension; // File extension echo $photo->mime_type; // MIME type echo $photo->size; // File size in bytes echo $photo->width; // Image width echo $photo->height; // Image height echo $photo->sort_order; // Display order echo $photo->is_main; // Boolean main status // Get URLs echo $photo->getUrl(); // Original image URL echo $photo->getThumbnailUrl(); // Default thumbnail (from config) echo $photo->getPath(); // Storage path // Custom image processing (NEW in v2.1) echo $photo->getUrl('400x400'); // Square 400x400 echo $photo->getUrl('800x600', 'webp'); // Rectangular WebP echo $photo->getUrl('400x400', 'jpg', 85); // Custom quality echo $photo->getUrl('300x200', 'png'); // PNG format // Photo operations $photo->deletePhoto(); // Delete photo and files
Custom Photo Filtering and Sorting
Add custom scopes to filter photos:
// Create a custom Photo model extending the package's Photo <?php namespace App\Models; use MacCesar\LaravelDropzoneEnhanced\Models\Photo as BasePhoto; class Photo extends BasePhoto { // Custom scopes public function scopeByDirectory($query, $directory) { return $query->where('directory', 'like', "%{$directory}%"); } public function scopeMainPhotos($query) { return $query->where('is_main', true); } public function scopeLargeImages($query, $minWidth = 1000) { return $query->where('width', '>=', $minWidth); } // Custom accessors public function getFileSizeFormattedAttribute() { $bytes = $this->size; $units = ['B', 'KB', 'MB', 'GB']; for ($i = 0; $bytes > 1024; $i++) { $bytes /= 1024; } return round($bytes, 2) . ' ' . $units[$i]; } public function getAspectRatioAttribute() { return $this->width / $this->height; } }
Use in your models:
// In your Product model, override the photos relationship public function photos() { return $this->morphMany(\App\Models\Photo::class, 'photoable') ->orderBy('sort_order', 'asc'); } // Then use custom scopes $mainPhotos = $product->photos()->mainPhotos()->get(); $galleryPhotos = $product->photos()->byDirectory('gallery')->get(); $largeImages = $product->photos()->largeImages(1200)->get();
Dynamic Configuration Based on User Roles
Configure dropzone behavior based on user permissions:
@php $user = auth()->user(); $maxFiles = $user->isPremium() ? 20 : 5; $maxSize = $user->isPremium() ? 10 : 2; // MB $dimensions = $user->hasRole('photographer') ? '4000x3000' : '1920x1080'; $enablePreResize = !$user->hasRole('professional'); @endphp <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->category }}/{{ $user->id }}" :dimensions="$dimensions" :maxFiles="$maxFiles" :maxFilesize="$maxSize" :preResize="$enablePreResize" />
Custom Event Handling
Add JavaScript event listeners for custom behavior:
<script> document.addEventListener('DOMContentLoaded', function() { // Custom success handler window.addEventListener('dropzone:success', function(event) { const detail = event.detail; console.log('Upload successful:', detail); // Custom notifications showToast('Image uploaded successfully!', 'success'); // Update UI counters updatePhotoCounter(); // Auto-refresh gallery if needed if (detail.isFirstPhoto) { location.reload(); // Refresh to show new main photo } }); // Custom error handler window.addEventListener('dropzone:error', function(event) { const error = event.detail; console.error('Upload failed:', error); // Show detailed error messages if (error.message.includes('validation')) { showToast('Please check your file format and size', 'error'); } else if (error.message.includes('storage')) { showToast('Storage error. Please try again.', 'error'); } else { showToast('Upload failed: ' + error.message, 'error'); } }); // Custom progress handler window.addEventListener('dropzone:progress', function(event) { const progress = event.detail.progress; updateProgressBar(progress); // Show/hide loading overlay if (progress === 100) { hideLoadingOverlay(); } else { showLoadingOverlay(); } }); }); function showToast(message, type) { // Your notification system integration } function updatePhotoCounter() { // Update photo count in UI const count = document.querySelectorAll('.photo-item').length; document.querySelector('#photo-count').textContent = count; } function updateProgressBar(progress) { const progressBar = document.querySelector('#upload-progress'); if (progressBar) { progressBar.style.width = progress + '%'; } } </script>
Custom CSS Styling
Override default styles with custom CSS:
/* Custom dropzone styling */ .dropzone-container .dropzone { border: 2px dashed #4f46e5; border-radius: 12px; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); transition: all 0.3s ease; min-height: 200px; display: flex; align-items: center; justify-content: center; } .dropzone:hover { border-color: #3730a3; background: linear-gradient(135deg, #eef2ff 0%, #ddd6fe 100%); transform: translateY(-2px); box-shadow: 0 8px 25px rgba(79, 70, 229, 0.15); } .dropzone.dz-drag-hover { border-color: #1e40af; background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); transform: scale(1.02); } /* Custom photo gallery */ .photos-container .photos-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; } .photos-container .photo-item { position: relative; aspect-ratio: 1; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; cursor: move; } .photos-container .photo-item:hover { transform: scale(1.05); box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.2); } .photos-container .photo-item.is-main { border: 3px solid #fbbf24; transform: scale(1.05); } .photos-container .photo-item.is-main::before { content: "★"; position: absolute; top: 8px; left: 8px; background: #fbbf24; color: white; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; z-index: 10; }
Breaking Changes in v2.1
Enhanced Image Processing API
BEFORE (v2.0 and earlier):
// This worked but was confusing $product->getMainPhotoThumbnailUrl('400x400', 'webp', 85);
AFTER (v2.1+):
// Simplified - thumbnails use config defaults only $product->getMainPhotoThumbnailUrl(); // Default dimensions from config // Enhanced - getUrl() now handles all custom processing $mainPhoto = $product->mainPhoto(); $customUrl = $mainPhoto?->getUrl('400x400', 'webp', 85);
Benefits of the New API:
- ✅ More intuitive:
getUrl()
for all image processing - ✅ Cleaner separation:
getThumbnailUrl()
for defaults only - ✅ More flexible: Support for WebP, PNG, custom quality
- ✅ Better performance: Dynamic generation only when needed
Migration Guide:
// Replace this: $url = $product->getMainPhotoThumbnailUrl('400x400', 'webp'); // With this: $mainPhoto = $product->mainPhoto(); $url = $mainPhoto?->getUrl('400x400', 'webp');
Configuration
For deep customization, publish the configuration file:
php artisan vendor:publish --tag=dropzone-enhanced-config
You can now edit config/dropzone.php
to change default image sizes, storage disks, route middleware, and more.
Security & Authorization
The package includes a comprehensive and robust authorization system for photo deletion to prevent unauthorized actions. It performs a series of checks for authenticated users (model ownership, isAdmin
methods, Gates) and provides secure options for unauthenticated scenarios (session tokens, access keys).
For full details on customizing authorization logic, please refer to the extensive comments in the config/dropzone.php
file and the source code of the DropzoneController
.
Security Best Practices
File Type Validation
Always validate file types both on the client and server side:
// Server-side validation (automatically handled by the package) // The DropzoneController validates with: 'file' => 'required|file|image|max:' . config('dropzone.images.max_filesize') // For custom validation, extend the controller: class CustomDropzoneController extends DropzoneController { public function upload(Request $request) { $request->validate([ 'file' => 'required|image|mimes:jpeg,png,webp|max:5120', // 5MB max 'directory' => 'required|string', 'model_id' => 'required|integer', 'model_type' => 'required|string', ]); return parent::upload($request); } }
Directory Structure Security
Organize uploads in a secure directory structure to prevent unauthorized access:
{{-- Good: Organized by model type --}} <x-dropzone-enhanced::area :model="$product" directory="products" /> {{-- Better: Include model ID for isolation --}} <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->id }}" /> {{-- Best: Include user context for multi-tenant apps --}} <x-dropzone-enhanced::area :model="$product" directory="users/{{ auth()->id() }}/products/{{ $product->id }}" />
User Authorization
The package provides multiple authorization layers. The userCanDeletePhoto()
method checks:
- Photo ownership:
$photo->user_id === auth()->id()
- Model ownership:
$model->user_id === auth()->id()
- User relationship:
$model->user() && $model->user->id === auth()->id()
- Custom ownership:
$model->isOwnedBy(auth()->user())
- Admin check:
auth()->user()->isAdmin()
- Laravel Gates:
auth()->can('delete-photos')
- Spatie Permissions:
auth()->user()->hasPermissionTo('delete photos')
To customize authorization, extend the controller:
class CustomDropzoneController extends DropzoneController { protected function userCanDeletePhoto(Request $request, Photo $photo, $model) { // Add your custom authorization logic if ($model instanceof Product && $model->company_id !== auth()->user()->company_id) { return false; } // Call parent method for default checks return parent::userCanDeletePhoto($request, $photo, $model); } }
Configuration Security
Review your security settings in config/dropzone.php
:
'security' => [ // IMPORTANT: Keep this false in production 'allow_all_authenticated_users' => false, // Set a strong access key for API requests 'access_key' => env('DROPZONE_ACCESS_KEY', null), ], 'images' => [ // Limit file sizes to prevent abuse 'max_filesize' => 10000, // 10MB in KB 'max_files' => 10, // Resize large images to save storage 'default_dimensions' => '1920x1080', 'pre_resize' => true, ],
Database Security
The package uses polymorphic relationships with user tracking:
// The photos table includes security fields: // - user_id: Who uploaded the photo // - photoable_id/photoable_type: What model it belongs to // Check photo ownership programmatically: $photo = Photo::find($photoId); if ($photo->user_id !== auth()->id()) { abort(403, 'Unauthorized'); } // Check model ownership: $model = $photo->photoable; if (!$model->isOwnedBy(auth()->user())) { abort(403, 'Unauthorized'); }
File Size and Rate Limiting
Implement proper limits to prevent abuse:
<x-dropzone-enhanced::area :model="$product" directory="products" :maxFiles="10" {{-- Limit number of files --}} :maxFilesize="5" {{-- Limit file size (MB) --}} dimensions="1920x1080" {{-- Resize large images --}} :preResize="true" {{-- Resize before upload --}} />
Add rate limiting middleware to your routes:
// In routes/web.php or your RouteServiceProvider Route::middleware(['throttle:uploads'])->group(function () { // Dropzone routes are automatically registered }); // In app/Http/Kernel.php protected $middlewareGroups = [ 'web' => [ // ... other middleware 'throttle:60,1', // 60 requests per minute ], ];
Performance Optimization
Image Optimization
Configure automatic image optimization to reduce file sizes and improve loading times:
{{-- Enable pre-resize for better performance --}} <x-dropzone-enhanced::area :model="$product" directory="products" dimensions="1200x800" {{-- Resize to reasonable dimensions --}} :preResize="true" {{-- Resize in browser before upload --}} />
Configure quality settings in config/dropzone.php
:
'images' => [ 'default_dimensions' => '1920x1080', // Max dimensions 'pre_resize' => true, // Client-side resize 'quality' => 90, // JPEG quality (1-100) 'max_filesize' => 10000, // 10MB max in KB 'thumbnails' => [ 'enabled' => true, 'dimensions' => '288x288', // Thumbnail size ], ],
Thumbnail Generation
The package uses the ImageProcessor
service to generate thumbnails efficiently:
{{-- Use different thumbnail sizes for different contexts --}} <x-dropzone-enhanced::photos :model="$product" thumbnailDimensions="200x200" {{-- Smaller for product lists --}} /> <x-dropzone-enhanced::photos :model="$product" thumbnailDimensions="400x300" {{-- Larger for detail views --}} />
Check thumbnail configuration:
// Get thumbnail URL with custom dimensions $photo = $product->photos->first(); $thumbUrl = $photo->getThumbnailUrl('300x200'); // Default thumbnail from config $defaultThumb = $photo->getThumbnailUrl(); // Uses config('dropzone.images.thumbnails.dimensions')
Database Performance
Optimize queries when working with photos:
// Eager load photos to avoid N+1 queries $products = Product::with('photos')->get(); // Get only main photos $products = Product::with(['photos' => function($query) { $query->where('is_main', true); }])->get(); // Order photos by sort_order (already done by HasPhotos trait) $photos = $product->photos; // Automatically ordered by sort_order ASC // Paginate photos for models with many images $photos = $product->photos()->paginate(20);
Storage Optimization
Optimize storage usage and access patterns:
// Use appropriate storage disk for your needs 'storage' => [ 'disk' => 'public', // For local development // 'disk' => 's3', // For production with CDN 'directory' => 'images', ], // Organize files in date-based directories to avoid too many files per folder <x-dropzone-enhanced::area :model="$product" directory="products/{{ date('Y/m') }}/{{ $product->id }}" />
Memory Management
The ImageProcessor
properly manages memory when generating thumbnails:
// The service automatically: // 1. Creates image resources // 2. Generates thumbnails with proper aspect ratio // 3. Cleans up memory with imagedestroy() // 4. Handles different image formats (JPEG, PNG, GIF, WebP) // For very large images, ensure adequate PHP memory: ini_set('memory_limit', '256M');
Lazy Loading
Implement lazy loading for better page performance:
{{-- The photos component includes lazy loading by default --}} <img src="{{ $photo->getThumbnailUrl() }}" loading="lazy" {{-- Native lazy loading --}} alt="{{ $photo->original_filename }}" class="photo-thumb" />
Caching Strategies
Implement caching for frequently accessed data:
// Cache photo counts public function getPhotoCountAttribute() { return Cache::remember( "product_{$this->id}_photo_count", 3600, // 1 hour fn() => $this->photos()->count() ); } // Cache main photo URL public function getCachedMainPhotoUrl() { return Cache::remember( "product_{$this->id}_main_photo_url", 3600, fn() => $this->getMainPhotoUrl() ); }
CDN Integration
For production environments, consider using a CDN:
// Override the Photo model's getUrl() method for CDN class Photo extends \MacCesar\LaravelDropzoneEnhanced\Models\Photo { public function getUrl() { $cdnUrl = config('app.cdn_url'); if ($cdnUrl) { return $cdnUrl . '/' . $this->getPath(); } return parent::getUrl(); } }
Batch Operations
Handle multiple photos efficiently:
// Delete multiple photos efficiently public function deleteSelectedPhotos(array $photoIds) { $photos = $this->photos()->whereIn('id', $photoIds)->get(); foreach ($photos as $photo) { $photo->deletePhoto(); // Handles file deletion + DB cleanup } } // Reorder multiple photos in one operation public function reorderPhotos(array $photoData) { foreach ($photoData as $item) { Photo::where('id', $item['id']) ->update(['sort_order' => $item['order']]); } } // Bulk update main photo status public function setMainPhoto(int $photoId): bool { // Unset all main photos in one query $this->photos()->update(['is_main' => false]); // Set new main photo return (bool) $this->photos() ->where('id', $photoId) ->update(['is_main' => true]); }
Integration with Other Packages
With Livewire
Integrate the package with Livewire components for reactive interfaces:
<?php namespace App\Http\Livewire; use Livewire\Component; use App\Models\Product; class ProductGallery extends Component { public Product $product; public $photos; public $photoCount = 0; protected $listeners = [ 'photoUploaded' => 'refreshPhotos', 'photoDeleted' => 'refreshPhotos', 'photoReordered' => 'refreshPhotos', ]; public function mount(Product $product) { $this->product = $product; $this->refreshPhotos(); } public function refreshPhotos() { $this->photos = $this->product->photos()->get(); $this->photoCount = $this->photos->count(); } public function deletePhoto($photoId) { $photo = $this->product->photos()->findOrFail($photoId); $photo->deletePhoto(); $this->refreshPhotos(); session()->flash('message', 'Photo deleted successfully'); } public function setMainPhoto($photoId) { $this->product->setMainPhoto($photoId); $this->refreshPhotos(); session()->flash('message', 'Main photo updated'); } public function render() { return view('livewire.product-gallery'); } }
Livewire component view:
{{-- resources/views/livewire/product-gallery.blade.php --}} <div> @if (session()->has('message')) <div class="alert alert-success"> {{ session('message') }} </div> @endif <div class="mb-4"> <h3>Upload New Photos ({{ $photoCount }}/{{ config('dropzone.images.max_files', 10) }})</h3> <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->id }}" :reloadOnSuccess="false" wire:ignore /> </div> <div class="mt-6"> <h3>Manage Photos</h3> <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> @foreach($photos as $photo) <div class="relative group"> <img src="{{ $photo->getThumbnailUrl('200x200') }}" alt="{{ $photo->original_filename }}" class="w-full h-32 object-cover rounded-lg {{ $photo->is_main ? 'ring-4 ring-yellow-400' : '' }}" > <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"> <button wire:click="setMainPhoto({{ $photo->id }})" class="bg-yellow-500 text-white p-1 rounded-full text-xs mr-1" title="Set as main photo" > ★ </button> <button wire:click="deletePhoto({{ $photo->id }})" wire:confirm="Are you sure you want to delete this photo?" class="bg-red-500 text-white p-1 rounded-full text-xs" title="Delete photo" > × </button> </div> @if($photo->is_main) <div class="absolute bottom-2 left-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded"> Main </div> @endif </div> @endforeach </div> </div> </div> <script> // Listen for upload success and refresh Livewire component window.addEventListener('dropzone:success', function(event) { @this.call('refreshPhotos'); }); window.addEventListener('dropzone:error', function(event) { // Handle upload errors in Livewire context console.error('Upload failed:', event.detail); }); </script>
With Spatie MediaLibrary (Alternative Implementation)
If you prefer using Spatie MediaLibrary instead of the built-in Photo model:
// Alternative approach using Spatie MediaLibrary use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\HasMedia; class Product extends Model implements HasMedia { use InteractsWithMedia; public function registerMediaCollections(): void { $this->addMediaCollection('gallery') ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']) ->singleFile(); // For single main image $this->addMediaCollection('thumbnails') ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']); } public function registerMediaConversions(Media $media = null): void { $this->addMediaConversion('thumb') ->width(288) ->height(288) ->sharpen(10); $this->addMediaConversion('large') ->width(1920) ->height(1080) ->quality(90); } // Helper methods to work with both systems public function getMainPhotoUrl() { if ($this->hasPhotos()) { return $this->getMainPhotoUrl(); // Use package method } // Fallback to MediaLibrary return $this->getFirstMediaUrl('gallery', 'large'); } }
With Laravel Sanctum API
Create API endpoints for mobile or SPA applications:
// routes/api.php use App\Http\Controllers\Api\DropzoneApiController; Route::middleware('auth:sanctum')->group(function () { Route::post('photos/upload', [DropzoneApiController::class, 'upload']); Route::delete('photos/{photo}', [DropzoneApiController::class, 'destroy']); Route::post('photos/{photo}/main', [DropzoneApiController::class, 'setMain']); Route::post('photos/reorder', [DropzoneApiController::class, 'reorder']); });
API Controller:
<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use MacCesar\LaravelDropzoneEnhanced\Http\Controllers\DropzoneController; use MacCesar\LaravelDropzoneEnhanced\Models\Photo; class DropzoneApiController extends DropzoneController { public function upload(Request $request) { try { $response = parent::upload($request); $data = $response->getData(); if ($data->success) { return response()->json([ 'success' => true, 'photo' => [ 'id' => $data->photo->id, 'url' => $data->photo->getUrl(), 'thumbnail' => $data->photo->getThumbnailUrl(), 'filename' => $data->photo->original_filename, 'size' => $data->photo->size, 'is_main' => $data->photo->is_main, ] ]); } return $response; } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Upload failed', 'error' => $e->getMessage() ], 422); } } public function destroy(Photo $photo) { try { // Use the package's authorization logic if (!$this->userCanDeletePhoto(request(), $photo, $photo->photoable)) { return response()->json([ 'success' => false, 'message' => 'Unauthorized' ], 403); } $success = $photo->deletePhoto(); return response()->json([ 'success' => $success, 'message' => $success ? 'Photo deleted successfully' : 'Failed to delete photo' ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Delete failed', 'error' => $e->getMessage() ], 500); } } }
With Inertia.js and Vue
Use the package with Inertia.js for Vue.js applications:
<!-- resources/js/Pages/Products/Edit.vue --> <template> <div> <h1>Edit Product: {{ product.name }}</h1> <!-- Upload Area --> <div class="mb-8"> <h3>Upload New Photos</h3> <DropzoneArea :model="product" directory="products" :max-files="10" :max-filesize="5" @upload-success="handleUploadSuccess" @upload-error="handleUploadError" /> </div> <!-- Photo Gallery --> <div class="mb-8"> <h3>Manage Photos ({{ photos.length }})</h3> <PhotoGallery :photos="photos" @photo-deleted="handlePhotoDelete" @main-photo-changed="handleMainPhotoChange" @photos-reordered="handlePhotoReorder" /> </div> </div> </template> <script> import { ref, onMounted } from 'vue' import { Inertia } from '@inertiajs/inertia' import DropzoneArea from '@/Components/DropzoneArea.vue' import PhotoGallery from '@/Components/PhotoGallery.vue' export default { components: { DropzoneArea, PhotoGallery }, props: { product: Object, photos: Array }, setup(props) { const photos = ref(props.photos) const handleUploadSuccess = (photo) => { photos.value.push(photo) // Show success notification this.$toast.success('Photo uploaded successfully') } const handleUploadError = (error) => { this.$toast.error('Upload failed: ' + error.message) } const handlePhotoDelete = (photoId) => { photos.value = photos.value.filter(photo => photo.id !== photoId) this.$toast.success('Photo deleted successfully') } const handleMainPhotoChange = (photoId) => { photos.value.forEach(photo => { photo.is_main = photo.id === photoId }) this.$toast.success('Main photo updated') } const handlePhotoReorder = (reorderedPhotos) => { photos.value = reorderedPhotos } return { photos, handleUploadSuccess, handleUploadError, handlePhotoDelete, handleMainPhotoChange, handlePhotoReorder } } } </script>
With Filament Admin Panel
Integrate with Filament for admin interfaces:
// app/Filament/Resources/ProductResource.php use Filament\Forms\Components\SpatieMediaLibraryFileUpload; use MacCesar\LaravelDropzoneEnhanced\Traits\HasPhotos; class ProductResource extends Resource { public static function form(Form $form): Form { return $form->schema([ // Other form fields... Section::make('Photos') ->schema([ // Custom photo management component ViewField::make('photos') ->view('filament.forms.dropzone-photos') ->viewData(fn ($record) => [ 'product' => $record, 'photos' => $record?->photos ?? collect(), ]), ]), ]); } }
Custom Filament view:
{{-- resources/views/filament/forms/dropzone-photos.blade.php --}} <div class="space-y-4"> @if($product) <!-- Upload Area --> <x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->id }}" :maxFiles="10" :maxFilesize="5" /> <!-- Photos Gallery --> @if($photos->count() > 0) <div class="grid grid-cols-3 gap-4 mt-4"> @foreach($photos as $photo) <div class="relative"> <img src="{{ $photo->getThumbnailUrl('200x200') }}" alt="{{ $photo->original_filename }}" class="w-full h-32 object-cover rounded {{ $photo->is_main ? 'ring-2 ring-primary-500' : '' }}" > @if($photo->is_main) <div class="absolute top-1 left-1 bg-primary-500 text-white text-xs px-2 py-1 rounded"> Main </div> @endif </div> @endforeach </div> @endif @else <p class="text-gray-500">Save the product first to add photos.</p> @endif </div>
Troubleshooting
Common Issues
Files Not Uploading
Problem: Files are not uploading or dropzone area is not responsive.
Solutions:
-
Check that your model has the
HasPhotos
trait:use MacCesar\LaravelDropzoneEnhanced\Traits\HasPhotos; class Product extends Model { use HasPhotos; }
-
Verify the routes are correctly registered:
php artisan route:list | grep dropzone
Should show:
POST dropzone/upload
,DELETE dropzone/photos/{id}
, etc. -
Check browser console for JavaScript errors
-
Ensure CSRF token is present in your page (required for web middleware)
Permission Denied Errors
Problem: Files upload but return 403/permission errors.
Solutions:
-
Check storage directory permissions:
chmod -R 775 storage/app/public/
-
Verify the storage link exists:
php artisan storage:link
-
Check your
.env
file has correctAPP_URL
-
Verify the
disk
configuration inconfig/dropzone.php
matches your storage setup
Images Not Displaying
Problem: Files upload successfully but don't display in gallery.
Solutions:
-
Run storage link command:
php artisan storage:link
-
Clear application cache:
php artisan cache:clear php artisan view:clear
-
Check that
storage/app/public/
directory is writable -
Verify your model relationship is working:
$product = Product::find(1); dd($product->photos); // Should return a collection
-
Check the
getUrl()
method is returning valid URLs:$photo = $product->photos->first(); dd($photo->getUrl()); // Should return a valid public URL
File Size Issues
Problem: Large files fail to upload.
Solutions:
-
Check PHP configuration in
php.ini
:upload_max_filesize = 10M post_max_size = 10M max_execution_time = 300 memory_limit = 256M
-
Update your dropzone configuration:
<x-dropzone-enhanced::area :model="$product" :maxFilesize="10" directory="products" />
-
Check the
max_filesize
setting inconfig/dropzone.php
Thumbnail Generation Issues
Problem: Original images display but thumbnails don't generate.
Solutions:
-
Ensure GD extension is installed:
php -m | grep -i gd
-
Check thumbnail configuration in
config/dropzone.php
:'thumbnails' => [ 'enabled' => true, 'dimensions' => '288x288', ],
-
Verify thumbnail directories are created with proper permissions
-
Check logs for thumbnail generation errors:
tail -f storage/logs/laravel.log
FAQ
Q: Can I upload files other than images? A: The package is designed for images, but you can modify the validation rules in the controller to accept other file types.
Q: How do I limit the number of files per model?
A: Use the :maxFiles
parameter on the dropzone component:
<x-dropzone-enhanced::area :model="$product" :maxFiles="5" directory="products" />
Q: Can I customize the upload directory structure?
A: Yes, the directory
parameter accepts nested paths:
<x-dropzone-enhanced::area :model="$product" directory="products/{{ $product->category }}" />
Q: How do I handle different image sizes for different models?
A: Use different dimensions
parameters for each model:
<x-dropzone-enhanced::area :model="$product" dimensions="1920x1080" directory="products" /> <x-dropzone-enhanced::area :model="$user" dimensions="400x400" directory="avatars" />
Q: How do I customize thumbnail dimensions?
A: Use the thumbnailDimensions
prop on the photos component:
<x-dropzone-enhanced::photos :model="$product" thumbnailDimensions="400x300" />
Q: Can I add custom validation rules?
A: Yes, extend the DropzoneController
and override the upload
method with your custom validation.
Contributing
Please see CONTRIBUTING.md for details.
License
The MIT License (MIT). Please see License File for more information.