aboleon / metaframework-mediaclass
Mediaclass media management components for MetaFramework
Package info
github.com/aboleon/metaframework-mediaclass
pkg:composer/aboleon/metaframework-mediaclass
Requires
- php: ^8.3
- aboleon/metaframework-inputable: ^1.0
- aboleon/metaframework-support: ^1.0
- cohensive/oembed: ^0.20.1
- css-crush/css-crush: ^v5.0.0
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/filesystem: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/view: ^11.0|^12.0|^13.0
- intervention/image: ^3.11
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
This package is auto-updated.
Last update: 2026-06-18 12:31:00 UTC
README
Media management components for Laravel applications. This package provides upload UI, database persistence, image resizing, optional cropping, and helpers to retrieve and render media for Eloquent models.
Quick Start
// Get image URL $url = $post->img('cover')->url(); // Get img tag {!! $post->img('cover')->class('rounded')->lazy()->img() !!} // In Blade <x-mfw-media :src="$post->img('cover')" class="rounded" lazy />
Installation
composer require aboleon/metaframework-mediaclass
Publish package resources after install or update:
php artisan mediaclass:update --force
The update command replaces the package-owned
public/vendor/mfw-mediaclass directory before publishing. This removes assets
deleted by newer package versions instead of leaving stale files from older
releases. Do not customize files inside that published asset directory.
On first install, publish the config without --force, then set its disk to
the application filesystem disk that owns the media paths:
php artisan mediaclass:update --config
Normal package updates never publish the config, including when --force is
used. This preserves application-owned disk, dimensions, subgroup, and path
settings. Use --config --force only when intentionally replacing the
application config with package defaults.
When a release adds migrations, publish and run them explicitly:
php artisan mediaclass:update --force --migrate
Use --views only when the application needs to customize the package Blade
views. Views are not published by default so package updates can keep improving
the upstream UI.
The stored-media component loads LightGallery 2.8.3 from
https://cdnjs.cloudflare.com, including its bundled CSS and the core, zoom,
thumbnail, and video scripts. The package does not publish a local LightGallery
distribution. Applications with a Content Security Policy must allow this host
in script-src and style-src.
Version Tracks
Mediaclass 0.x is the jQuery uploader line. Applications that want the
existing jQuery UI and do not want the v1 Svelte UI should require the 0.x
track explicitly:
composer require aboleon/metaframework-mediaclass:"0.*"
For the current stable jQuery release line only:
composer require aboleon/metaframework-mediaclass:"^0.16"
Mediaclass 1.x is the Svelte UI line. The Svelte assets are built and shipped
inside the Composer package, so consuming Laravel applications should not need
Node, npm, or a Svelte build step just to use the package. After updating the
Composer dependency, run:
php artisan mediaclass:update --force
The 1.x package does not ship or load Blueimp jQuery File Upload. Its uploader,
video URL form, queue, progress state, and validation UI are Svelte. jQuery is
still required by the current media-management bridge for stored-media sorting,
deletion, descriptions, cropping, subgroups, and LightGallery integration.
During v1 development, applications can test the branch with Composer's dev constraint:
composer require aboleon/metaframework-mediaclass:"1.x-dev"
Frontend Asset Development
The v1 frontend source lives in resources/svelte. Uploadable.svelte owns the
upload UI and media-manager.js owns stored-media interactions. Vite compiles
both into the single shipped
public/vendor/mfw-mediaclass/mediaclass-uploader.js bundle. Package
maintainers must run the frontend checks and rebuild the shipped bundle before
tagging a release:
npm install npm run check npm run build
Uploader styles live in
public/vendor/mfw-mediaclass/css/styles.css and are processed by the existing
CSSCrush integration. Svelte components must not contain component-level style
blocks or inject CSS into the compiled JavaScript bundle.
The Vite build disables publicDir intentionally because the bundle output is
inside the package public tree. Do not re-enable public copying for this build.
Blade Components
The active v1 Blade surface is:
<x-mediaclass::uploadable>for the Svelte mount point and media context.<x-mediaclass::stored>for server-rendered existing media.<x-mediaclass::printer>and<x-mfw-media>for frontend rendering.- Internal crop and confirmation modal components used by
uploadable.
The old Blueimp upload-template component was removed in 1.x; it is not a
supported Blade API.
The small published jcrop directory remains because the active crop editor
still loads Jcrop. It is independent from the removed Blueimp uploader.
Applications installing the package through Composer do not run these npm commands. They receive the precompiled bundle and only need:
composer update aboleon/metaframework-mediaclass php artisan mediaclass:update --force
Laravel Compatibility
The package supports Laravel majors through explicit Illuminate constraints in
composer.json, currently ^11.0|^12.0|^13.0. A future Laravel major does not
automatically require a Mediaclass major release. The package should widen its
Laravel constraints and tag a minor or patch release when the public API and
published assets remain compatible.
Use a new Mediaclass major only when the package drops an older Laravel major, changes the public PHP or Blade contract, changes installation behavior in a breaking way, or replaces an implementation detail that applications can reasonably depend on.
The current v1 direction keeps Mediaclass Laravel-bound with a shipped Svelte bundle. If Mediaclass later needs to live as a framework-neutral uploader, the better split is a Lit + TypeScript web-component package with a Laravel bridge package around routes, persistence, configuration, and Blade helpers. That split is larger than the v1 Svelte migration and should be treated as a separate product boundary.
Model Setup
use Illuminate\Database\Eloquent\Model; use MetaFramework\Mediaclass\Contracts\MediaclassInterface; use MetaFramework\Mediaclass\Concerns\Mediaclass as MediaclassTrait; class Post extends Model implements MediaclassInterface { use MediaclassTrait; public function mediaclassSettings(): array { return [ 'cover' => [ 'label' => 'Cover', 'width' => 1600, 'height' => 900, 'cropable' => true, ], 'gallery' => [ 'label' => 'Gallery', 'width' => 1200, 'height' => 800, ], ]; } }
Two Groups Example (Cover + Gallery)
Use group keys in mediaclassSettings() to define the required dimensions per group:
public function mediaclassSettings(): array { return [ 'cover' => [ 'label' => 'Cover', 'width' => 1600, 'height' => 900, 'cropable' => true, // single crop using the group dimensions ], 'gallery' => [ 'label' => 'Gallery', 'width' => 1200, 'height' => 800, // 'cropable' => ['thumb' => [400, 300]] // optional extra crops ], ]; }
If no group is defined, the package falls back to the default sizes defined in
config/mfw-mediaclass.php under dimensions.
Group-Specific Sizes
You can define multiple sizes for a single group using a sizes array. These
sizes will be used for resizing and for size keys when calling url('key'):
public function mediaclassSettings(): array { return [ 'cover' => [ 'label' => 'Cover', 'sizes' => [ 'xl' => ['width' => 1600, 'height' => 900], 'sm' => ['width' => 1200, 'height' => 500], ], 'cropable' => true, // uses the largest size as the crop target ], ]; }
If sizes is not provided for a group, the package uses the single width /
height pair for that group, or falls back to the global dimensions defaults.
Note: Upload processing relies on Intervention Image. If Intervention Image is not installed, upload tests that hit the upload controller will be skipped.
Displaying Images
Fluent API (Recommended)
The simplest way to display images from your models:
// Get URL $url = $post->img('cover')->url(); $url = $post->img('cover')->url('lg'); // specific size // Get img tag $html = $post->img('cover')->img(); $html = $post->img('cover') ->class('rounded-lg shadow') ->alt('Product photo') ->lazy() ->img(); // Get cropped version $url = $post->img('cover')->crop('banner')->url(); // Check if media exists if ($post->img('cover')->exists()) { // ... } // Multiple images foreach ($post->imgs('gallery') as $img) { echo $img->url(); }
Available Methods
| Method | Description |
|---|---|
->url(?string $size) |
Get URL (default: 'sm') |
->img(?string $size) |
Get <img> tag |
->picture(?array $breakpoints) |
Get <picture> element |
->background() |
Get CSS background-image style |
->urls() |
Get all available URLs as array |
Size Methods:
->size('lg') // Set size ->sm() / ->md() / ->lg() / ->xl() // Shorthand
Crop Methods:
->crop('banner') // Use specific crop ->hasCrop('banner') // Check if crop exists
HTML Attributes:
->class('rounded') // CSS classes ->addClass('shadow') // Add to existing classes ->alt('Description') // Alt text ->id('hero-image') // ID attribute ->lazy() // loading="lazy" ->eager() // loading="eager" ->width(800) // Width attribute ->height(600) // Height attribute ->attr('data-id', 1) // Any attribute ->attrs(['class' => 'rounded', 'id' => 'img']) ->data('gallery', 'main') // data-* attributes
Fallback:
->default('/img/fallback.png') // Custom fallback URL ->noDefault() // No fallback image
Blade Component
{{-- Basic usage --}} <x-mfw-media :src="$post->img('cover')" /> {{-- With attributes --}} <x-mfw-media :src="$post->img('cover')" size="lg" class="rounded-lg" alt="Product image" lazy /> {{-- From model directly --}} <x-mfw-media :model="$post" group="cover" size="lg" /> {{-- As URL only --}} <x-mfw-media :src="$post->img('cover')" type="url" /> {{-- As picture element --}} <x-mfw-media :src="$post->img('cover')" type="picture" /> {{-- With specific crop --}} <x-mfw-media :src="$post->img('cover')" crop="banner" />
Component Attributes:
| Attribute | Type | Description |
|---|---|---|
src |
MediaBuilder | From $model->img('group') |
model |
object | Model instance (with group) |
group |
string | Media group name |
subgroup |
string | Media subgroup filter |
size |
string | Image size (sm, md, lg, xl) |
type |
string | Output: img, url, picture, background |
class |
string | CSS classes |
alt |
string | Alt text |
id |
string | HTML id |
lazy |
bool | Enable lazy loading |
crop |
string | Specific crop key |
default |
string | Fallback URL |
noDefault |
bool | Disable fallback |
data |
array | Data attributes |
breakpoints |
array | For picture element |
Direct Media Model Usage
// If you have a Media model directly $media = Media::find(1); $url = $media->url('lg'); $url = $media->crop('banner'); $html = $media->img('md', ['class' => 'rounded']); // Fluent builder $html = $media->builder() ->class('rounded') ->lazy() ->img();
Uploading Media
Upload Component
<x-mediaclass::uploadable :model="$post" group="cover" :limit="1" />
With options:
<x-mediaclass::uploadable :model="$post" group="gallery" :limit="10" :positions="true" :description="true" maxfilesize="5MB" :cropable="['thumb' => [400, 300]]" />
Stored Media Display (Admin)
<x-mediaclass::stored :model="$post" group="gallery" />
Dynamic Subgroups in the Upload UI
Mediaclass stores an optional subgroup on each media row. You can enable an
admin-side subgroup selector for an uploadable group so editors can assign each
uploaded image to a preset subgroup without creating separate upload slots.
Configure presets globally:
// config/mfw-mediaclass.php 'subgroups' => [ 'count' => 5, 'label' => 'Group', 'empty_label' => 'Normal flow', 'key_prefix' => 'group_', 'groups' => [ 'gallery' => true, ], ],
Or define explicit labels:
'subgroups' => [ 'groups' => [ 'gallery' => [ 'options' => [ 'featured' => 'Featured', 'flow' => 'Flow', ], ], ], ],
You can also define subgroup presets on a model group:
public function mediaclassSettings(): array { return [ 'gallery' => [ 'label' => 'Gallery', 'width' => 1200, 'height' => 800, 'subgroups' => [ 'count' => 5, 'label' => 'Group', ], ], ]; }
When subgroups are enabled, <x-mediaclass::uploadable> injects a select into
each native uploaded image row. The select saves through the package AJAX route:
POST /mediaclass-ajax
action=saveSubgroup
The response triggers a jQuery document event:
$(document).on('mediaclass:subgroup-saved', function (event, result, uploadable, select) { // result.group, result.media_id, result.subgroup, result.uses_subgroups });
Frontend rendering remains application-owned. A common pattern is to render
media with subgroup = null in normal flow, and render media sharing the same
subgroup as a grid.
Processing After Save
$post = Post::create($payload); $post->processMedia();
Configuration
Published to config/mfw-mediaclass.php:
return [ 'disk' => 'public', 'dimensions' => [ 'xl' => ['width' => 1920, 'height' => 1080], 'lg' => ['width' => 1400, 'height' => 788], 'md' => ['width' => 700, 'height' => 394], 'sm' => ['width' => 400, 'height' => 225], ], ];
Cropping
Define cropable settings in your model:
public function mediaclassSettings(): array { return [ 'cover' => [ 'width' => 1600, 'height' => 900, 'cropable' => true, // Single crop using group dimensions ], 'banner' => [ 'width' => 1920, 'height' => 400, 'cropable' => [ 'desktop' => [1920, 400], 'mobile' => [800, 400], ], ], ]; }
Access cropped versions:
// Single crop $url = $post->img('cover')->crop('cover')->url(); // Multiple crops $desktop = $post->img('banner')->crop('desktop')->url(); $mobile = $post->img('banner')->crop('mobile')->url(); // Check if crop exists if ($post->img('cover')->hasCrop('cover')) { // ... }
Ghost Media
For media not attached to a specific model instance:
<x-mediaclass::uploadable :model="$post" group="cover" ghost />
Retrieve ghost media:
use MetaFramework\Mediaclass\Mediaclass; $url = Mediaclass::ghostUrl(Post::class, 'cover', 'sm', '/fallback.png');
External Video Embeds
External video media and supported oEmbed URLs can be rendered through the Mediaclass facade or helper:
use MetaFramework\Mediaclass\Facades\MediaclassFacade; $html = MediaclassFacade::embed($media, ['loading' => 'lazy']); $html = mediaclass_embed('https://www.youtube.com/watch?v=...');
Embeds default to 560 × 315. External video media store their display
dimensions in the media storable data. The uploader UI supports a pixel width
or a responsive 100% width:
$media->storable = [ 'url' => 'https://www.youtube.com/watch?v=...', 'embed_width' => '100%', 'embed_height' => 315, ];
Explicit helper options override the stored dimensions.
The back-office uploader stores the provider thumbnail when available. Existing external videos resolve and cache their oEmbed thumbnail on first display. Video previews open in the CDN-hosted LightGallery viewer and autoplay through its video plugin.
Unsupported URLs and provider failures return an empty HtmlString.
Legacy API
The original Parser/Printer classes are still available for backward compatibility:
use MetaFramework\Mediaclass\Mediaclass; use MetaFramework\Mediaclass\Printer; // Fetch and parse $parser = (new Mediaclass())->forModel($post, 'cover')->first(); $url = $parser->url; // Render with Printer $html = (new Printer($parser)) ->setClass('rounded') ->setLoading('lazy') ->img('md');
Storage Layout
- Regular:
{model}/{id}/{width}_{filename}.{ext} - Ghost:
{model}/{width}_{filename}.{ext} - Crops:
{model}/{id}/cropped_{key}_{filename}.{ext}
Routes
POST /mediaclass-ajax- Upload/delete/crop actionsGET /mediaclass/cropable/{media}- Crop UI
Testing
composer install
composer test
Requirements
- PHP 8.3+
- Laravel 11+
- Intervention Image