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

v1.0.0 2025-11-12 22:36 UTC

This package is auto-updated.

Last update: 2026-01-12 23:22:19 UTC


README

๐ŸŽฏ Laravel Media Zone

Latest Version on Packagist Total Downloads License

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, and axios installed.

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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Inspired by the needs of the Laravel + Inertia.js community and the excellent work of packages like Spatie's Media Library.

๐Ÿ“ฌ Connect

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!

โฌ† Back to Top