dialloibrahima/laravel-model-media

A lightweight trait for managing media files directly on your Eloquent models, without additional tables. Perfect when you need to attach files to models by simply storing the path as an attribute.

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 8

Watchers: 0

Forks: 2

Open Issues: 1

pkg:composer/dialloibrahima/laravel-model-media

v2.0.0 2026-02-06 01:44 UTC

README

Laravel Model Media

Latest Version on Packagist GitHub Tests Action Status Total Downloads PHP Version Laravel Version

A lightweight, zero-boilerplate media management trait for Laravel Eloquent models. Attach files directly to your existing model attributes without adding any extra database tables or complex relationships.

Includes a powerful Glide plugin for on-the-fly image manipulation: resize, crop, convert formats, and generate responsive images.

๐Ÿ“‹ Table of Contents

๐Ÿ˜ฐ The Problem

Most media management packages for Laravel (like Spatie MediaLibrary) are powerful but "heavy". They often require:

  • A new media table in your database.
  • Complex Polymorphic relationships.
  • Manual cleanup of files when models are deleted.
  • Overkill for simple use cases where you just want to store a profile picture or a document path directly on a model.

โœจ The Solution

Laravel Model Media keeps it simple. It uses your existing database columns to store file names.

  • โœ… No Extra Tables: Uses the columns you already have.
  • โœ… Automatic Cleanup: Deletes old files when you re-upload or delete the model.
  • โœ… Smart Filenames: Use model attributes or dynamic Closures for naming.
  • โœ… Zero Config: Just add the trait and register your media.
  • โœ… Image Manipulation: Built-in Glide plugin for resizing, cropping, and format conversion.
  • โœ… Responsive Images: Generate srcsets for <picture> elements automatically.

โš–๏ธ Comparison: Spatie MediaLibrary vs. Laravel Model Media

Feature Spatie MediaLibrary Laravel Model Media
Philosophy "One table for everything" "Keep it on the model"
New Tables media (Polymorphic) None
Complexity High (Conversions, Collections) Low (Simple & Fast)
Performance Extra Join/Query for each model Zero extra queries
Setup Migrations + Trait + Interface Trait only
Image Manipulation Built-in conversions Glide plugin (optional)
Ideal for Complex CMS, Multiple galleries Profile pics, Single documents, Simple uploads

๐Ÿ“ฆ Installation

composer require dialloibrahima/laravel-model-media

For image manipulation, install the optional Glide plugin:

composer require league/glide

โšก Quick Start

1. Prepare your Model

Add the HasMedia trait and register which column should handle media.

namespace App\Models;

use DialloIbrahima\HasMedia\HasMedia;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasMedia;

    protected static function booted()
    {
        self::registerMediaForColumn(
            column: 'profile_photo',    // Your DB column
            directory: 'avatars',       // Storage folder
            fileName: 'id',             // Name file after a model attribute
            disk: 'public'              // Optional: storage disk (default is 'public')
        );
    }
}

2. Handle Uploads

Call attachMedia() in your controller.

public function update(Request $request, User $user)
{
    if ($request->hasFile('photo')) {
        $user->attachMedia($request->file('photo'), 'profile_photo');
        $user->save();
    }

    return back();
}

3. Retrieve URLs

<img src="{{ $user->getMediaUrl('profile_photo') }}">

4. Delete Media

// Remove media and clear the column
$user->detachMedia('profile_photo');

// Media is also automatically deleted when the model is deleted
$user->delete(); // Files are cleaned up automatically

๐Ÿ“š API Reference

HasMedia Trait

The core trait that provides media management capabilities.

registerMediaForColumn()

Register a media mapping for a model column.

protected static function registerMediaForColumn(
    string $column,              // Database column name
    string $directory,           // Storage directory
    string|Closure $fileName,    // Attribute name or Closure for filename
    string $disk = 'public'      // Storage disk
): void

attachMedia()

Attach an uploaded file to a column.

public function attachMedia(UploadedFile $file, string $column): bool

Returns: true if storage was successful, false otherwise.

Behavior:

  • Stores the file to the configured disk and directory
  • Automatically deletes any previously attached file
  • Updates the model attribute with the new filename

detachMedia()

Remove media from a column and delete the file.

public function detachMedia(?string $column): bool

Returns: true when complete.

getMediaUrl()

Get the public URL for a media file.

public function getMediaUrl(string $column): ?string

Returns: The full URL to the file, or null if no file is attached.

getMediaMappings()

Get all registered media mappings for the model.

public function getMediaMappings(): array

HasGlideUrls Trait

Adds Glide image manipulation capabilities. Requires league/glide package.

โš ๏ธ Important: This trait only works with image files. It validates MIME types and throws InvalidMediaTypeException for non-image files.

getGlideUrl()

Generate a URL for a transformed image.

public function getGlideUrl(
    string $column,              // Column name
    array $params = [],          // Glide transformation parameters
    bool $throwOnError = false   // Throw exception on error
): ?string

Returns: URL to the transformed image, or null if unavailable.

// Basic resize
$user->getGlideUrl('avatar', ['w' => 200, 'h' => 200]);

// Crop to square
$user->getGlideUrl('avatar', ['w' => 300, 'h' => 300, 'fit' => 'crop']);

// Convert to WebP
$user->getGlideUrl('avatar', ['w' => 400, 'fm' => 'webp', 'q' => 85]);

// With error handling
try {
    $url = $user->getGlideUrl('document', ['w' => 200], throwOnError: true);
} catch (InvalidMediaTypeException $e) {
    // Handle non-image file
}

getGlidePresetUrl()

Generate a URL using a predefined preset.

public function getGlidePresetUrl(
    string $column,              // Column name
    string $preset,              // Preset name from config
    bool $throwOnError = false   // Throw exception on error
): ?string
// Use thumbnail preset (defined in config)
$user->getGlidePresetUrl('avatar', 'thumbnail');

// Use medium preset
$user->getGlidePresetUrl('cover', 'medium');

getGlideSrcset()

Generate a srcset attribute for responsive images.

public function getGlideSrcset(
    string $column,                               // Column name
    array $widths = [400, 800, 1200, 1600],       // Image widths
    bool $throwOnError = false                    // Throw exception on error
): ?string
// Default widths
$srcset = $user->getGlideSrcset('cover');
// Result: "http://...?w=400&fm=webp 400w, http://...?w=800&fm=webp 800w, ..."

// Custom widths for mobile-first
$srcset = $user->getGlideSrcset('hero', [375, 768, 1024, 1920]);

Usage in Blade:

<img 
    src="{{ $post->getGlideUrl('cover', ['w' => 800]) }}"
    srcset="{{ $post->getGlideSrcset('cover') }}"
    sizes="(max-width: 768px) 100vw, 800px"
    alt="{{ $post->title }}"
>

hasImageMedia()

Check if a column contains a valid image file.

public function hasImageMedia(string $column): bool
@if($user->hasImageMedia('avatar'))
    <img src="{{ $user->getGlideUrl('avatar', ['w' => 200]) }}">
@else
    <img src="{{ asset('images/default-avatar.jpg') }}">
@endif

๐Ÿ”ง Advanced Usage

Dynamic Filenames via Closure

If you need complex naming logic, use a Closure:

use Illuminate\Support\Str;

self::registerMediaForColumn(
    column: 'invoice_pdf',
    directory: 'invoices',
    fileName: fn ($model, $file) => "invoice-{$model->number}-" . Str::random(5)
);
// Result: invoice-INV-001-x7kP2.pdf

The Closure receives:

  • $model - The Eloquent model instance
  • $file - The UploadedFile instance

Multiple Storage Disks

// Store avatars on public disk
self::registerMediaForColumn(
    column: 'avatar',
    directory: 'avatars',
    fileName: 'id',
    disk: 'public'
);

// Store private documents on S3
self::registerMediaForColumn(
    column: 'contract',
    directory: 'contracts',
    fileName: fn ($model) => "contract-{$model->id}",
    disk: 's3'
);

Automatic Cleanup

You don't need to do anything! Laravel Model Media automatically:

  • Deletes the old file when you re-upload a new one to the same column.
  • Deletes all associated files when the model is deleted (via Model Observer).

๐Ÿ–ผ๏ธ Image Manipulation with Glide

The package includes a powerful Glide plugin for on-the-fly image manipulation.

Glide Installation

composer require league/glide

Then add the HasGlideUrls trait to your model:

use DialloIbrahima\HasMedia\HasMedia;
use DialloIbrahima\HasMedia\Plugins\Glide\HasGlideUrls;

class User extends Model
{
    use HasMedia, HasGlideUrls;
    
    // ...
}

Publish the Glide configuration (optional):

php artisan vendor:publish --tag=model-media-glide-config

Glide Basic Usage

// Resize to 200x200
$url = $user->getGlideUrl('avatar', ['w' => 200, 'h' => 200]);

// Crop to fit
$url = $user->getGlideUrl('avatar', ['w' => 300, 'h' => 300, 'fit' => 'crop']);

// Convert to WebP with quality
$url = $user->getGlideUrl('avatar', ['fm' => 'webp', 'q' => 85]);

Transformation Parameters

Parameter Description Example
w Width in pixels ['w' => 200]
h Height in pixels ['h' => 200]
fit Fit mode: contain, max, fill, stretch, crop ['fit' => 'crop']
fm Format: jpg, png, gif, webp ['fm' => 'webp']
q Quality (0-100) ['q' => 85]
blur Blur (0-100) ['blur' => 5]
bri Brightness (-100 to 100) ['bri' => 10]
con Contrast (-100 to 100) ['con' => 10]
gam Gamma (0.1 to 9.99) ['gam' => 1.5]
sharp Sharpen (0-100) ['sharp' => 10]
flip Flip: v (vertical), h (horizontal), both ['flip' => 'h']
or Orientation: auto, 0, 90, 180, 270 ['or' => 'auto']
border Border in format width,color,method ['border' => '5,FFFFFF,overlay']

See the Glide documentation for all available parameters.

Using Presets

Presets allow you to define reusable transformation sets:

// config/model-media-glide.php
'presets' => [
    'thumbnail' => [
        'w' => 200,
        'h' => 200,
        'fit' => 'crop',
        'fm' => 'webp',
        'q' => 90,
    ],
    'medium' => [
        'w' => 800,
        'h' => 600,
        'fit' => 'contain',
        'fm' => 'webp',
        'q' => 85,
    ],
    'og-image' => [
        'w' => 1200,
        'h' => 630,
        'fit' => 'crop',
        'fm' => 'jpg',
        'q' => 85,
    ],
],

Then use them in your code:

$thumbnailUrl = $user->getGlidePresetUrl('avatar', 'thumbnail');
$ogImageUrl = $post->getGlidePresetUrl('cover', 'og-image');

Responsive Images (srcset)

Generate responsive image sets automatically:

<picture>
    <source 
        srcset="{{ $post->getGlideSrcset('cover', [375, 768, 1024, 1920]) }}"
        sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
    >
    <img 
        src="{{ $post->getGlideUrl('cover', ['w' => 800]) }}"
        alt="{{ $post->title }}"
        loading="lazy"
    >
</picture>

Glide Configuration

// config/model-media-glide.php
return [
    // Routes
    'routes_enabled' => true,
    'route_prefix' => 'media',
    'middleware' => ['web'],

    // Security
    'secure' => env('GLIDE_SECURE', false),
    'signature_key' => env('GLIDE_SIGNATURE_KEY'),

    // Server
    'source' => storage_path('app/public'),
    'cache' => storage_path('app/glide-cache'),
    'driver' => 'gd', // or 'imagick'
    'max_image_size' => 2000 * 2000,

    // Presets
    'presets' => [
        'thumbnail' => ['w' => 200, 'h' => 200, 'fit' => 'crop'],
        // ...
    ],
];

Security

Enable URL signing to prevent unauthorized image manipulation (DoS protection):

GLIDE_SECURE=true
GLIDE_SIGNATURE_KEY=your-32-character-random-string

Generate a secure key:

php artisan tinker
>>> Str::random(32)

When secure mode is enabled, all Glide URLs will be cryptographically signed.

Automatic Cache Cleanup

The HasGlideUrls trait automatically registers a GlideCacheObserver that cleans up cached images when:

  • Image is updated: When you attach a new image to a column, the old cached versions are deleted
  • Model is deleted: All cached images for the model are removed

This happens automatically - you don't need to configure anything. The observer:

  1. Detects when a media column changes
  2. Finds cached transformations for the old image
  3. Deletes them from the Glide cache directory

This prevents stale cached images from being served and saves disk space.

Manual Cache Clearing

If you need to manually clear the cache for all images:

# Clear entire Glide cache
rm -rf storage/app/glide-cache/*

๐Ÿ—๏ธ How It Works

graph TD
    subgraph Upload ["๐Ÿ“ค 1. ATTACH (Upload)"]
        A[File Uploaded] --> B[attachMedia Method]
        B --> C[Retrieve Mapping]
        C --> D[Generate Filename]
        D --> E[Store to Disk]
        E --> F[Update Model Attribute]
    end

    subgraph Update ["๐Ÿ”„ 2. UPDATE (Auto-Cleanup)"]
        G[Replace File] --> H[Identify Old File]
        H --> I[Delete Old File from Disk]
        I --> J[Proceed with New Upload]
    end

    subgraph Delete ["๐Ÿ—‘๏ธ 3. DELETE (Full Cleanup)"]
        K[Model Deleted] --> L[MediaObserver Triggers]
        L --> M[Fetch All Mappings]
        M --> N[Delete All Files from Disk]
    end

    subgraph Glide ["๐Ÿ–ผ๏ธ 4. GLIDE (Image Transform)"]
        O[getGlideUrl Called] --> P[Validate Image Type]
        P --> Q[Build Transform URL]
        Q --> R[Glide Server Processes]
        R --> S[Return Cached/Generated Image]
    end

    F -.-> G
    J --> B
    F -.-> O
Loading

๐Ÿงช Testing

The package includes a robust test suite. You can run it via composer:

composer test

We test for:

  • โœ… Correct storage path and filename generation.
  • โœ… Automatic deletion of old media on update.
  • โœ… Garbage collection of files on model deletion.
  • โœ… Glide URL generation and transformation.
  • โœ… Glide preset and srcset generation.
  • โœ… Image type validation (only images allowed for Glide).
  • โœ… Error handling for non-image files.

๐Ÿค Contributing

Please see CONTRIBUTING for details.

๐Ÿ“„ License

The MIT License (MIT). Please see License File for more information.

๐Ÿ‘จโ€๐Ÿ’ป Credits