okamal / laravel-media-zone
Elegant polymorphic media uploads for Laravel with Inertia.js and zone-based organization
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/okamal/laravel-media-zone
Requires
- php: ^8.1|^8.2|^8.3|^8.4
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
๐ฏ Laravel Media Zone
Elegant polymorphic media management for Laravel + Inertia.js
Organize uploads by zones. One trait. Beautiful Vue 3 component. Zero hassle.
Features โข Installation โข Quick Start โข Documentation
โจ Features
- ๐ฏ Zone-Based Organization - Group media by zones (
avatar,gallery,documents, etc.) - ๐ Polymorphic Many-to-Many - Attach media to any model
- ๐จ Beautiful Vue 3 Component - Built with Bootstrap 5.3, includes drag & drop, real-time progress, and previews
- โก Temporary Uploads - Files stay in temp storage until model is saved
- ๐ก๏ธ Per-Model Validation - Custom rules and messages for each model and zone
- ๐ฆ Flexible File Handling - Single or multiple files per zone
- ๐งน Auto Cleanup - Automatic deletion of orphaned files and old temp files
- ๐ Laravel 10, 11 & 12 - Full support for all modern Laravel versions
- ๐ฑ Responsive - Mobile-friendly upload interface
- โ๏ธ Configurable - Extensive configuration options
๐ Requirements
| Package | Version |
|---|---|
| PHP | ^8.1, ^8.2, ^8.3, ^8.4 |
| Laravel | ^10.0, ^11.0, ^12.0 |
| Inertia.js | ^1.0 or ^2.0 |
| Vue | ^3.3, ^3.4, or ^3.5 |
| Bootstrap | ^5.3 |
Note: This package includes a Vue 3 component styled with Bootstrap 5.3. Make sure your project uses Bootstrap 5.3+.
๐ Installation
Step 1: Install Package
composer require okamal/laravel-media-zone
Step 2: Install Vue Dependency
npm install vue3-uuid
Your project should already have
vue,@inertiajs/vue3, andaxiosinstalled.
Step 3: Publish Assets
Publish configuration, migrations, and the Vue component:
php artisan vendor:publish --provider="OKamal\LaravelMediaZone\LaravelMediaZoneServiceProvider"
Or publish individually:
# Publish config file php artisan vendor:publish --tag=media-zone-config # Publish migrations php artisan vendor:publish --tag=media-zone-migrations # Publish Vue component php artisan vendor:publish --tag=media-zone-components
Step 4: Run Migrations
php artisan migrate
Step 5: Link Storage
If you haven't already:
php artisan storage:link
Step 6: Configure (Optional)
Review and customize config/media-zone.php if needed.
โก Quick Start
1. Add Trait to Model
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use OKamal\LaravelMediaZone\Traits\HasMediaZones; class Post extends Model { use HasMediaZones; protected $fillable = ['title', 'content']; }
2. Use the Vue Component
The component is published to resources/js/Components/MediaZone/MediaZoneUpload.vue.
<template> <form @submit.prevent="submit"> <!-- Title Input --> <div class="mb-3"> <label class="form-label">Title</label> <input v-model="form.title" type="text" class="form-control" /> </div> <!-- Single File Upload --> <div class="mb-3"> <MediaZoneUpload label="Featured Image" model="App\Models\Post" zone="featured_image" accept="image/*" helper-text="Upload a featured image for your post (max 5MB)" :input-error="form.errors.featured_image" v-model="form.featured_image" /> </div> <!-- Multiple Files Upload --> <div class="mb-3"> <MediaZoneUpload label="Gallery" model="App\Models\Post" zone="gallery" :multiple="true" :max-files="10" accept="image/*" helper-text="Upload up to 10 images" v-model="form.gallery" /> </div> <button type="submit" class="btn btn-primary"> Create Post </button> </form> </template> <script setup> import { useForm } from '@inertiajs/vue3'; import MediaZoneUpload from '@/Components/MediaZone/MediaZoneUpload.vue'; const form = useForm({ title: '', featured_image: null, gallery: [] }); const submit = () => { form.transform(data => ({ ...data, featured_image: data.featured_image?.id, gallery: data.gallery?.map(img => img.id) || [] })).post(route('posts.store')); }; </script>
3. Save in Controller
<?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { public function store(Request $request) { $request->validate([ 'title' => 'required|string|max:255', 'featured_image' => 'required|integer|exists:media,id', 'gallery' => 'nullable|array', 'gallery.*' => 'integer|exists:media,id', ]); $post = Post::create($request->only('title')); // Sync media to model $post->syncMedia([ 'featured_image' => [$request->featured_image], 'gallery' => $request->gallery ?? [] ]); return redirect()->route('posts.index') ->with('success', 'Post created successfully!'); } }
4. Display Media
Add accessors to your model for easy access:
use Illuminate\Database\Eloquent\Casts\Attribute; protected function featuredImage(): Attribute { return Attribute::make( get: fn() => $this->getFirstMediaByZone('featured_image'), ); } protected function gallery(): Attribute { return Attribute::make( get: fn() => $this->getMediaByZone('gallery'), ); }
Display in your views:
<!-- Featured Image --> @if($post->featured_image) <img src="{{ $post->featured_image->url }}" alt="{{ $post->title }}"> @endif <!-- Gallery --> @foreach($post->gallery as $image) <img src="{{ $image->url }}" alt="Gallery image"> @endforeach
That's it! ๐
๐ Documentation
Configuration
The configuration file is located at config/media-zone.php:
return [ // Storage disk (must be configured in config/filesystems.php) 'disk' => env('MEDIA_ZONE_DISK', 'public'), // Base path for media files 'base_path' => env('MEDIA_ZONE_BASE_PATH', 'media'), // Temporary upload path 'temp_path' => env('MEDIA_ZONE_TEMP_PATH', 'media/temp'), // Global validation defaults 'validation' => [ 'max_file_size' => 10240, // KB (10MB) 'allowed_mime_types' => [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'video/mp4', 'video/mpeg', ], ], // Auto-delete physical files when media records are deleted 'auto_delete_files' => true, // Cleanup old temporary files 'cleanup_temp_files' => [ 'enabled' => true, 'older_than_hours' => 24, ], // Routes configuration 'routes' => [ 'enabled' => true, 'prefix' => 'api/media-zone', 'middleware' => ['web', 'auth'], 'name' => 'media-zone.', ], // Per-model validation configurations 'models' => [ // Example: // App\Models\Post::class => App\MediaZone\PostMediaConfig::class, ], ];
Component Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
label |
String | '' |
No | Label text above the file input |
model |
String | - | Yes | Full model class name (e.g., App\Models\Post) |
zone |
String | - | Yes | Zone identifier (e.g., avatar, gallery) |
multiple |
Boolean | false |
No | Allow multiple file uploads |
maxFiles |
Number | null |
No | Maximum number of files (only for multiple uploads) |
accept |
String | '*' |
No | File type filter (e.g., image/*, .pdf) |
inputError |
String | '' |
No | Validation error message |
helperText |
String | '' |
No | Helper text below the input |
uploadRoute |
String | null |
No | Custom upload endpoint |
deleteRoute |
String | null |
No | Custom delete endpoint (use :id placeholder) |
Example:
<MediaZoneUpload label="Profile Picture" model="App\Models\User" zone="avatar" accept="image/jpeg,image/png" helper-text="Square image recommended. Max 2MB." :input-error="form.errors.avatar" v-model="form.avatar" />
Trait Methods
Get Media
// Get all media for this model $model->media; // Collection // Get media by zone $model->getMediaByZone('gallery'); // Collection // Get first media in zone $model->getFirstMediaByZone('avatar'); // Media|null // Get media URL (helper) $model->getMediaUrl('avatar'); // string|null
Sync & Attach
// Sync media (recommended for form submissions) $post->syncMedia([ 'featured_image' => [$imageId], 'gallery' => [$id1, $id2, $id3], 'documents' => [$docId] ]); // Add single media to zone $post->addMediaToZone($mediaId, 'avatar'); // Replace all media in a zone $post->replaceMediaInZone([$newId1, $newId2], 'gallery');
Check & Delete
// Check if model has media in a zone if ($post->hasMediaInZone('featured_image')) { // Has featured image } // Clear specific zone $post->clearMediaZone('gallery'); // Delete all media for this model $post->deleteMedia();
Storage Paths
// Get storage directory for a zone $path = $post->mediaStorageDirectory('gallery'); // Example output: "media/posts/galleries"
Media Model Attributes
$media = $post->featured_image; $media->id; // int $media->name; // string - Filename $media->url; // string - Public URL $media->mime_type; // string - MIME type $media->size; // int - Size in bytes $media->human_size; // string - Formatted size (e.g., "2.5 MB") $media->zone; // string - Zone name $media->storage_path; // string - Disk path // Type checks $media->isImage(); // bool $media->isVideo(); // bool $media->isDocument(); // bool
Per-Model Validation
Create custom validation rules for specific models and zones.
Step 1: Create Config Class
<?php namespace App\MediaZone; use OKamal\LaravelMediaZone\Contracts\MediaZoneConfig; class PostMediaConfig implements MediaZoneConfig { public function rules(string $zone): array { return match ($zone) { 'featured_image' => [ 'featured_image' => [ 'required', 'image', 'max:5120', // 5MB 'mimes:jpeg,png,webp', 'dimensions:min_width=800,min_height=600', ], ], 'gallery' => [ 'gallery' => [ 'required', 'image', 'max:2048', // 2MB 'mimes:jpeg,png', ], ], 'attachments' => [ 'attachments' => [ 'required', 'file', 'max:10240', // 10MB 'mimes:pdf,doc,docx', ], ], default => [], }; } public function messages(string $zone): array { return match ($zone) { 'featured_image' => [ 'featured_image.required' => 'Please upload a featured image.', 'featured_image.dimensions' => 'Featured image must be at least 800x600 pixels.', 'featured_image.max' => 'Featured image must not exceed 5MB.', ], 'gallery' => [ 'gallery.max' => 'Each gallery image must not exceed 2MB.', ], default => [], }; } public function zones(): array { return ['featured_image', 'gallery', 'attachments']; } public function isMultiple(string $zone): bool { return in_array($zone, ['gallery', 'attachments']); } }
Step 2: Register in Config
// config/media-zone.php 'models' => [ App\Models\Post::class => App\MediaZone\PostMediaConfig::class, ],
Now all uploads for Post model will use these custom rules! ๐
Maintenance Commands
Cleanup Temporary Files
Files uploaded but not attached to any model are automatically cleaned up:
php artisan media-zone:cleanup
Schedule Automatic Cleanup:
Add to app/Console/Kernel.php:
protected function schedule(Schedule $schedule) { $schedule->command('media-zone:cleanup')->daily(); }
๐ก Usage Examples
Example 1: User Profile
// Model class User extends Authenticatable { use HasMediaZones; }
<!-- Form --> <MediaZoneUpload label="Profile Picture" model="App\Models\User" zone="avatar" accept="image/*" v-model="form.avatar" /> <MediaZoneUpload label="Cover Photo" model="App\Models\User" zone="cover" accept="image/*" v-model="form.cover" />
// Controller public function updateProfile(Request $request) { $user = auth()->user(); $user->syncMedia([ 'avatar' => [$request->avatar], 'cover' => [$request->cover], ]); return back()->with('success', 'Profile updated!'); }
Example 2: Product Gallery
// Model class Product extends Model { use HasMediaZones; }
<!-- Form --> <MediaZoneUpload label="Product Images" model="App\Models\Product" zone="images" :multiple="true" :max-files="10" accept="image/*" v-model="form.images" />
// Accessor protected function images(): Attribute { return Attribute::make( get: fn() => $this->getMediaByZone('images'), ); } // Display foreach($product->images as $image) { echo "<img src='{$image->url}'>"; }
Example 3: Document Uploads
class Contract extends Model { use HasMediaZones; }
<MediaZoneUpload label="Contract PDF" model="App\Models\Contract" zone="contract_pdf" accept=".pdf" v-model="form.contract_pdf" /> <MediaZoneUpload label="Supporting Documents" model="App\Models\Contract" zone="supporting_docs" :multiple="true" accept=".pdf,.doc,.docx" v-model="form.supporting_docs" />
๐จ Styling
The Vue component is styled with Bootstrap 5.3 classes. Make sure your project includes Bootstrap 5.3+.
Using Bootstrap
If you're using Bootstrap via npm:
npm install bootstrap@5.3
// In your app.js import 'bootstrap/dist/css/bootstrap.min.css';
Custom Styling
The component uses slots, so you can customize the markup:
<MediaZoneUpload model="App\Models\Post" zone="image" v-model="form.image" > <template #label> <h3 class="my-custom-label">Upload Your Image</h3> </template> <template #preview> <!-- Your custom preview markup --> </template> </MediaZoneUpload>
๐ง Advanced Usage
Custom Routes
Disable package routes and define your own:
// config/media-zone.php 'routes' => [ 'enabled' => false, ],
// routes/web.php use OKamal\LaravelMediaZone\Http\Controllers\MediaZoneController; Route::post('/custom-upload', [MediaZoneController::class, 'store']) ->name('custom.upload');
<!-- Component --> <MediaZoneUpload upload-route="/custom-upload" delete-route="/custom-delete/:id" ... />
Eager Loading
Prevent N+1 queries:
$posts = Post::with('media')->get(); foreach ($posts as $post) { $post->featured_image; // No additional query }
Custom Storage Paths
Override the storage path method in your model:
public function mediaStorageDirectory(?string $zone = null): string { // Organize by user return "media/users/{$this->user_id}/posts/" . str($zone)->plural(); }
โ FAQ
Q: Do I need Bootstrap?
A: Yes, the Vue component uses Bootstrap 5.3 classes. A Tailwind version may be added in the future.
Q: Can I use this with React or Svelte?
A: Currently, only Vue 3 is supported. React/Svelte components may be added in future versions.
Q: Does this work with Laravel Livewire?
A: No, this package is specifically designed for Inertia.js.
Q: Can media be shared across multiple models?
A: Yes! The package uses a many-to-many polymorphic relationship, so the same media can be attached to multiple models.
๐ Changelog
See CHANGELOG.md for recent changes.
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
๐ Security
If you discover a security vulnerability, please email e.omarkamal@gmail.com. All security vulnerabilities will be promptly addressed.
๐ License
The MIT License (MIT). See LICENSE for details.
๐ Support
If this package saves you time:
- โญ Star this repo on GitHub
- ๐ Report bugs via Issues
- ๐ก Request features via Issues
- ๐ฌ Share it with other Laravel developers
๐ Credits
- Omar Kamal - Creator & Maintainer
- All Contributors - View Contributors
Inspired by the needs of the Laravel + Inertia.js community and the excellent work of packages like Spatie's Media Library.
๐ฌ Connect
- ๐ผ LinkedIn: Omar Kamal
- ๐ง Email: e.omarkamal@gmail.com
- ๐ Website: [Coming Soon]
Need custom Laravel development or want to hire me for your project? Get in touch!
Made with โค๏ธ by Omar Kamal
If this package helps your project, please give it a โญ star!