PHPackages                             webcimes/laravel-mediaforge - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. webcimes/laravel-mediaforge

ActiveLibrary[File &amp; Storage](/categories/file-storage)

webcimes/laravel-mediaforge
===========================

Laravel file &amp; image upload service with multi-format processing (resize, convert, watermark, text overlay).

0.6.6(1mo ago)145↓90%MITPHPPHP ^8.3

Since Mar 24Pushed 1mo agoCompare

[ Source](https://github.com/WebCimes/laravel-mediaforge)[ Packagist](https://packagist.org/packages/webcimes/laravel-mediaforge)[ RSS](/packages/webcimes-laravel-mediaforge/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (11)Versions (23)Used By (0)

webcimes/laravel-mediaforge
===========================

[](#webcimeslaravel-mediaforge)

Store images and files directly in your existing model columns — no separate media library table required.

Powered by [Intervention Image](https://image.intervention.io/v3), this Laravel package lets you upload a file once and automatically generate multiple formats (thumbnail, WebP, watermark, etc.), each saved as a structured entry you can store in any JSON/text column of your choosing.

Table of contents
-----------------

[](#table-of-contents)

- [Why this package?](#why-this-package)
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [ImageFormat reference](#imageformat-reference)
- [Responsive images (srcset)](#responsive-images-srcset)
- [Handle files (upload + delete + reorder in one call)](#handle-files-upload--delete--reorder-in-one-call)
- [Regenerate a format](#regenerate-a-format)
- [Delete files](#delete-files)
- [Auto-delete on model deletion](#auto-delete-on-model-deletion)
- [Custom base name](#custom-base-name)
- [Filament integration](#filament-integration)
- [Configuration](#configuration)

Why this package?
-----------------

[](#why-this-package)

Most media libraries (like Spatie Media Library) introduce a dedicated `media` table that links files to models through a polymorphic relationship. This works great for complex scenarios, but adds overhead when you just want to attach one or a few images to a model.

**With this package:**

- Image data (path, dimensions, format config) is stored directly in whichever column you choose — a JSON column, a `text` column, even inside a JSON API response.
- No extra table, no polymorphic join, no extra migration.
- The upload result is a plain PHP array — store it anywhere, serialize it however you like.
- Regenerate derivative formats at any time from the stored original.

Features
--------

[](#features)

- Fluent `ImageFormat` builder — chain transforms in a readable, IDE-friendly way
- Multi-format processing in one upload (default + thumb + WebP, all at once)
- Responsive image variants via `->srcset()` — base format + per-width variants with resize type inheritance
- All formats of the same upload grouped in a single folder for easy management
- ULID-based unique naming (collision-proof, chronologically sortable, URL-safe)
- Plain PHP array output — no model binding required
- Regenerate derivative formats from the stored original at any time
- Works with any Laravel filesystem disk (local, S3, SFTP, …)

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11, 12, or 13
- Intervention Image 3.x — requires either the **GD** or **Imagick** PHP extension

Installation
------------

[](#installation)

```
composer require webcimes/laravel-mediaforge
```

Publish the config file:

```
php artisan vendor:publish --tag="mediaforge-config"
```

Basic usage
-----------

[](#basic-usage)

```
use Webcimes\LaravelMediaforge\ImageFormat;
use Webcimes\LaravelMediaforge\Facades\MediaForge;

// Upload and generate two formats in one call:
$imageData = MediaForge::upload(
    $request->file('cover'),
    'public', // disk
    'products', // base path inside the disk
    [
        ImageFormat::make('default')->scaleDown(1920, 1080)->quality(80)->extension('webp'),
        ImageFormat::make('thumb')->cover(400, 300)->quality(65)->extension('webp'),
    ],
);

// Store $imageData directly in your model column (cast it to 'array' or 'json'):
$product->update(['cover' => $imageData]);
```

`$imageData` is a plain array — store it in any JSON column:

```
// $imageData:
[
    'default' => [
        'disk'   => 'public',
        'path'   => 'products/my-cover_01jq8z.../default.webp',
        'width'  => 1920,
        'height' => 1080,
        'alt'    => 'my-cover',
    ],
    'thumb' => [
        'disk'             => 'public',
        'path'             => 'products/my-cover_01jq8z.../thumb.webp',
        'width'            => 400,
        'height'           => 300,
        'alt'              => 'my-cover',
        // 'customAttributes' only present when defined via ->customAttributes([...])
        'customAttributes' => ['role' => 'thumbnail'],
    ],
]
```

ImageFormat reference
---------------------

[](#imageformat-reference)

MethodDescription`->disk('s3')`Override the storage disk for this format`->path('media/thumbs')`Override the storage directory`->extension('webp')`Convert to a specific format`->quality(75)`Encoding quality 1–100 (JPEG, WebP, AVIF, HEIC, TIFF)`->filename('hero')`Override the file name (without extension)`->suffix('_2x')`Append a suffix before the extension`->resize(w, h)`Exact dimensions — no ratio preservation`->resizeDown(w, h)`Same — only shrinks`->scale(w, h?)`Proportional fit — preserves ratio`->scaleDown(w, h?)`Same — only shrinks`->cover(w, h)`Center-crop to exact dimensions`->coverDown(w, h)`Same — only shrinks`->text('Draft', [...])`Text overlay (requires a TTF font — see config)`->watermark('/path/logo.png', [...])`Image watermark overlay`->srcset()`Responsive image variants using config default widths — see [Responsive images](#responsive-images-srcset)`->srcset([1920, 1080, 720])`Responsive image variants with explicit widths — see [Responsive images](#responsive-images-srcset)`->alt('My image')`Override the alt text for this format (defaults to filename stem)`->customAttributes([...])`Custom metadata stored alongside this format entryResponsive images (srcset)
--------------------------

[](#responsive-images-srcset)

`->srcset()` generates a **base format entry** plus **width variant entries**, all nested under the format key via a `srcset` array. This is the recommended way to produce responsive images for use with the HTML `srcset` attribute.

Call `->srcset()` **without arguments** to use the widths defined in `config('mediaforge.srcset.widths')` (default: `[1920, 1440, 1280, 1024, 768, 480]`). Pass an explicit array to override the config for that format.

```
$imageData = MediaForge::upload(
    $request->file('hero'),
    'public',
    'uploads',
    [
        ImageFormat::make('hero')
            ->srcset([1920, 1080, 720, 480])
            ->extension('webp')
            ->quality(80),
    ],
);
```

This produces the following structure (base format + nested `srcset` array + auto-injected `default`):

```
[
    'default' => ['disk' => 'public', 'path' => 'uploads/hero_xxx/default.jpg', 'width' => 2000, 'height' => 1000, 'alt' => 'hero'],
    'hero' => [
        'disk' => 'public', 'path' => 'uploads/hero_xxx/hero.webp', 'width' => 2000, 'height' => 1000, 'alt' => 'hero',
        'srcset' => [
            ['disk' => 'public', 'path' => 'uploads/hero_xxx/hero_1920w.webp', 'width' => 1920, 'height' => 960, 'alt' => 'hero'],
            ['disk' => 'public', 'path' => 'uploads/hero_xxx/hero_1080w.webp', 'width' => 1080, 'height' => 540, 'alt' => 'hero'],
            ['disk' => 'public', 'path' => 'uploads/hero_xxx/hero_720w.webp',  'width' => 720,  'height' => 360, 'alt' => 'hero'],
            ['disk' => 'public', 'path' => 'uploads/hero_xxx/hero_480w.webp',  'width' => 480,  'height' => 240, 'alt' => 'hero'],
        ],
    ],
]
```

**Resize type inheritance** — each variant inherits the resize method configured on the format:

Configured on formatVariant behaviour`cover(1000, 500)``cover(width, proportional_height)` — e.g. at 400w → `cover(400, 200)``scaleDown(1920)``scaleDown(width)` — height auto-computed by Intervention*(none)*falls back to `scaleDown(width)` — aspect ratio preserved, never upscalesThe **base format** (`hero`) always uses the dimensions you configured explicitly and is never filtered by `skipLarger`.

Example — square thumbnails at multiple sizes:

```
ImageFormat::make('thumb')
    ->cover(1000, 1000)
    ->srcset([200, 400, 800])
    ->extension('webp')
    ->quality(75),
// Generates: thumb (1000×1000) with srcset: [200×200, 400×400, 800×800]
```

Example — proportional banner (keep aspect ratio, only shrink):

```
ImageFormat::make('banner')
    ->scaleDown(1920, 600)   // base: max 1920×600, ratio preserved
    ->srcset([960, 480])
    ->extension('webp'),
// Generates: banner (≤1920×600) with srcset: [≤960×300, ≤480×150]
```

Example — free resize without ratio preservation:

```
ImageFormat::make('og')
    ->resize(1200, 630)      // exact 1200×630 for Open Graph, no ratio preserved
    ->srcset([600])
    ->extension('jpg'),
// Generates: og (1200×630) with srcset: [600×315]
```

**`skipLarger`** (default `true`) — variants whose target width exceeds the source image width are skipped (no file written, no entry created). The base format is never filtered.

```
// Default — source 600px wide: srcset contains only hero_480w
ImageFormat::make('hero')->srcset([1920, 1080, 720, 480])->extension('webp');

// skipLarger: false — all four variants are written (scaleDown caps them at source width)
ImageFormat::make('hero')->srcset([1920, 1080, 720, 480], skipLarger: false)->extension('webp');
```

**All other options (`extension()`, `quality()`, `watermark()`, `text()`, `alt()`, `customAttributes()`) are inherited by the base format and all variants.**

**Building an `` attribute (in your Blade view):**

```
$entry = $product->cover; // the stored array

// Build the srcset string from the nested array
$srcset = collect($entry['hero']['srcset'])
    ->map(fn($v) => Storage::disk($v['disk'])->url($v['path']) . ' ' . $v['width'] . 'w')
    ->implode(', ');

// Use the base format (e.g. 'hero') as src — it has the right extension/quality applied.
// Do NOT use 'default' in src: it is the raw original (no conversion, no quality), kept only for regeneration.
```

```

```

`handleFiles()` is designed to process the payload emitted by a file input component in a single call: upload new files, delete removed ones, and apply a global ordering across both existing and new items.

```
$validated['images'] = MediaForge::handleFiles(
    diskName: 'public',
    path: 'products',
    uploadedFiles: $validated['images']['files'] ?? null,     // new UploadedFile[]
    filesToDeleteIndex: $validated['images']['deleted'] ?? null, // int[] — indexes into $existingFiles
    globalOrder: $validated['images']['globalOrder'] ?? null, // full ordered list of all items
    existingFiles: $product->images,                          // current DB value
    imageFormats: $this->imageFormats,                        // optional, same as upload()
);

$product->update(['images' => $validated['images']]);
```

The method returns the updated flat array ready to be stored, or `null` if the result is empty.

**`globalOrder`** is an array of ordering directives, one per surviving file:

```
// Scenario: 3 existing files in DB (indexes 0, 1, 2), index 1 deleted, 2 new files uploaded.
// filesToDeleteIndex: [1]
// uploadedFiles: [$new0, $new1]
// Desired order: new1, existing-0, existing-2, new0

[
    ['type' => 'new',      'index' => 1], // new1      → position 0
    ['type' => 'existing', 'index' => 0], // existing0 → position 1
    ['type' => 'existing', 'index' => 2], // existing2 → position 2 (existing1 was deleted)
    ['type' => 'new',      'index' => 0], // new0      → position 3
]
```

- `type`: `'existing'` (already in DB) or `'new'` (just uploaded)
- `index` for `existing`: position in the `$existingFiles` array passed to the method
- `index` for `new`: position in the `$uploadedFiles` array (0-based, matches `DataTransfer` order in the browser)
- The **array order** defines the final position — the first item ends up at position 0, and so on.

When `globalOrder` is provided, any uploaded file whose index is **not** referenced is automatically deleted from disk to prevent orphaned files.

When `globalOrder` is omitted, existing files come first (in their original order), followed by newly uploaded files.

Regenerate a format
-------------------

[](#regenerate-a-format)

Re-process derivatives from the stored original at any time — useful when you change the design:

```
$updated = MediaForge::regenerate($product->cover, [
    ImageFormat::make('thumb')->cover(200, 200)->extension('avif'),
]);

$product->update(['cover' => $updated]);
```

Delete files
------------

[](#delete-files)

```
// Deletes all files referenced in the stored entry:
MediaForge::delete($product->cover, 'public');
```

Auto-delete on model deletion
-----------------------------

[](#auto-delete-on-model-deletion)

Add the `HasMediaForge` trait to your model and declare which columns (or nested paths) hold MediaForge data. When the model is permanently deleted, all referenced files are automatically removed from disk:

```
use Webcimes\LaravelMediaforge\Traits\HasMediaForge;

class Product extends Model
{
    use HasMediaForge;

    protected array $mediaForgeColumns = [
        // Direct column
        'cover',

        // Multiple-upload column (array of format maps)
        'images',

        // Nested inside a JSON column (e.g. content = ['hero' => ['image' => [formatMap]]])
        'content.hero.image',

        // Repeater with wildcard — deletes the image from every item in the array
        // (e.g. content = ['slides' => [['image' => [formatMap]], ['image' => [formatMap]]]])
        'content.slides.*.image',
    ];
}
```

**Soft-delete aware** — if your model uses `SoftDeletes`, files are preserved on a regular `->delete()` (so a restored record still has its files) and only deleted on `->forceDelete()`.

Custom base name
----------------

[](#custom-base-name)

The upload folder name is `{slug}_{ulid}` by default. Override it:

```
// Auto (default): slug + ULID  → my-photo_01jq8z...
MediaForge::upload($file, 'public', 'uploads', $formats);

// ULID only (no slug)          → 01jq8z...
MediaForge::upload($file, 'public', 'uploads', $formats, '');

// Custom prefix + ULID         → product-hero_01jq8z...
MediaForge::upload($file, 'public', 'uploads', $formats, 'product-hero');
```

Filament integration
--------------------

[](#filament-integration)

`MediaForgeFileUpload` is a drop-in replacement for Filament's `FileUpload` component. It transparently delegates storage to `MediaForge` and encodes the resulting format map as JSON in the database column.

All native `FileUpload` methods (`->multiple()`, `->reorderable()`, `->disk()`, `->directory()`, `->panelLayout()`, etc.) work exactly as in standard Filament. The MediaForge-specific addition is `->imageFormats()`:

```
use Webcimes\LaravelMediaforge\Filament\Forms\Components\MediaForgeFileUpload;
use Webcimes\LaravelMediaforge\ImageFormat;

MediaForgeFileUpload::make('cover')
    ->label('Cover image')
    ->imageFormats([
        ImageFormat::make('default')
            ->scaleDown(1920, 1080)
            ->quality(75)
            ->extension('webp'),
        ImageFormat::make('thumb')
            ->cover(400, 300)
            ->quality(60)
            ->extension('webp'),
    ])
    ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
    ->multiple()
    ->reorderable()
    ->appendFiles()
    ->openable()
    ->disk('public')
    ->directory('uploads')
    ->panelLayout('grid'),
```

The column value stored in the database is a plain array (cast it to `array` or `json` on the model):

```
// Single upload (no ->multiple()):
[
    'default' => ['disk' => 'public', 'path' => 'uploads/hero_01jq8z.../default.webp', 'width' => 1920, 'height' => 1080, 'alt' => 'hero'],
    'thumb'   => ['disk' => 'public', 'path' => 'uploads/hero_01jq8z.../thumb.webp',   'width' => 400,  'height' => 300,  'alt' => 'hero'],
]

// Multiple uploads (->multiple()):
[
    [
        'default' => ['disk' => 'public', 'path' => 'uploads/img-a_01jq8z.../default.webp', ...],
        'thumb'   => ['disk' => 'public', 'path' => 'uploads/img-a_01jq8z.../thumb.webp',   ...],
    ],
    [
        'default' => ['disk' => 'public', 'path' => 'uploads/img-b_01jq8z.../default.webp', ...],
        'thumb'   => ['disk' => 'public', 'path' => 'uploads/img-b_01jq8z.../thumb.webp',   ...],
    ],
]
```

Configuration
-------------

[](#configuration)

After publishing (`php artisan vendor:publish --tag="mediaforge-config"`):

```
return [
    // Image processing driver:
    //   'gd'      — Built into PHP, works on every host. Default.
    //   'imagick' — Requires ext-imagick. Better quality, native AVIF/HEIC. Recommended if available.
    //   'vips'    — Requires: composer require intervention/image-driver-vips + libvips.
    //               Fastest and lowest memory, but rarely available on shared hosting.
    'driver' => 'gd', // switch to 'imagick' if available on your server

    // Text overlay defaults. Any key passed to ImageFormat::text([...]) overrides these.
    // The default font is Montserrat Regular (TTF) bundled in the package — no setup needed.
    // To use a custom font, set an absolute path to a TTF or OTF file:
    'text' => [
        'font' => null, // defaults to Montserrat TTF bundled in vendor; override: resource_path('fonts/my-font.ttf')
        'size' => 48,
        'color' => 'rgba(255, 255, 255, .75)',
        'align' => 'center',
        'valign' => 'middle',
        'angle' => 0,
        'wrap' => 0, // max line width in px before wrapping; 0 = no wrap
    ],

    // Watermark overlay defaults.
    'watermark' => [
        'position' => 'center',
        'x' => 0,
        'y' => 0,
        'opacity' => 0.75,
    ],

    // Srcset defaults. Used when calling ->srcset() with no explicit widths.
    // These are the most common CSS breakpoints; adjust to match your design system.
    // Passing an explicit array to ->srcset([...]) always overrides this config.
    'srcset' => [
        'widths' => [1920, 1440, 1280, 1024, 768, 480],
    ],
];
```

License
-------

[](#license)

MIT

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance92

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Recently: every ~12 days

Total

22

Last Release

38d ago

PHP version history (2 changes)0.1.0PHP ^8.2

0.6.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/9f81f12ea92734e031e9f3d03fd7918771e210301fc3cfa12c6be4a96a3fa8b2?d=identicon)[contact@webcimes.com](/maintainers/contact@webcimes.com)

---

Top Contributors

[![WebCimes](https://avatars.githubusercontent.com/u/43851969?v=4)](https://github.com/WebCimes "WebCimes (27 commits)")

---

Tags

laravelimageresizefileuploadintervention

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/webcimes-laravel-mediaforge/health.svg)

```
[![Health](https://phpackages.com/badges/webcimes-laravel-mediaforge/health.svg)](https://phpackages.com/packages/webcimes-laravel-mediaforge)
```

###  Alternatives

[unisharp/laravel-filemanager

A file upload/editor intended for use with Laravel 5 to 10 and CKEditor / TinyMCE

2.1k3.4M81](/packages/unisharp-laravel-filemanager)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[sopamo/laravel-filepond

Laravel backend module for filepond uploads

216293.6k3](/packages/sopamo-laravel-filepond)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
