Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sonata Media Bundle Laravel Package

mediamonks/sonata-media-bundle

A MediaMonks-maintained Symfony bundle based on Sonata Media Bundle for managing uploaded media, image thumbnails, and storage providers. It supports Sonata Admin integration and flexible asset handling for PHP projects that need a familiar media management workflow.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install

composer require mediamonks/sonata-media-bundle

The bundle sits on top of Sonata Admin and a Flysystem filesystem, so you'll also need sonata-project/admin-bundle, sonata-project/doctrine-orm-admin-bundle, league/flysystem-bundle (or a manually defined Flysystem service), and something to serve/resize images on the front end (LiipImagineBundle or Glide are the usual pairings — the bundle stores originals, it does not ship a thumbnail controller).

2. Enable and configure

Register MediaMonks\SonataMediaBundle\MediaMonksSonataMediaBundle in config/bundles.php, then dump the reference before guessing at keys:

bin/console config:dump-reference media_monks_sonata_media

Typical shape:

# config/packages/media_monks_sonata_media.yaml
media_monks_sonata_media:
    filesystem: sonata_media_filesystem   # id of your Flysystem filesystem service
    # image/cache/visibility options — verify against Configuration.php

Define the Flysystem filesystem it points at (local adapter in dev, S3/GCS in prod):

flysystem:
    storages:
        sonata_media_filesystem:
            adapter: 'flysystem.adapter.sonata_media.storage'
            visibility: public

3. Schema

The bundle ships a Media entity mapping but no migrations. Generate them yourself:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

If the entity lives in the bundle namespace, make sure it's registered in doctrine.orm.mappings (the bundle's DependencyInjection extension usually does this, but check doctrine:mapping:info shows Media).

4. Register the admin

# config/packages/sonata_admin.yaml
sonata_admin:
    dashboard:
        groups:
            media:
                label: Media
                items:
                    - sonata.admin.media

The bundle registers sonata.admin.media for you (MediaAdmin), which gives you a Sonata list/form with an upload widget, a provider selector (image/file/video), and a preview. Once you can upload a JPEG through /admin and see it in the list, the plumbing works end to end.

First use case to validate: upload one image via the admin, then render it in Twig. If the Twig function returns a URL that resolves to the stored file, you've validated config, storage, DB, and rendering in one pass — everything else is variations on that loop.

Where to look first in the repo:

  • DependencyInjection/Configuration.php — the authoritative list of options.
  • Entity/Media.php — fields (name, provider, providerReference, width, height, focalPoint, filesize, contentType, …) you'll query on.
  • Provider/ — ImageProvider, FileProvider, and whatever else exists; this is where MIME → handler routing lives.
  • Twig/ — the extension with the render/URL helpers.
  • Admin/MediaAdmin.php + the form types — what the upload widget actually posts.

Implementation Patterns

The core upload → persist → render loop

Every use case reduces to the same three services:

  • MediaManager — orchestrates: creates/persists the Media entity, delegates bytes to storage.
  • MediaStorage — thin wrapper over Flysystem (write/read/delete, path generation).
  • MediaProviderInterface implementations — decide how a given upload is handled and how it's rendered.

An upload arrives as an UploadedFile, a provider is chosen (usually by MIME sniffing or by an explicit form field), the file is written to Flysystem under a generated path, and the entity records the path plus metadata (dimensions, MIME, size). Rendering is just resolving that path back to a URL through the same filesystem.

Attaching media to your own entities

Don't invent a parallel upload path. Store a relation and let Sonata's form handle it:

#[ORM\ManyToOne(targetEntity: Media::class)]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Media $heroImage = null;
$builder->add('heroImage', AdminType::class, [
    'required' => false,
    'label'    => 'Hero image',
]);

This gives editors the standard Sonata media picker with upload, and keeps Media as the single source of truth for the file. Use onDelete: 'SET NULL' (or a soft-delete/deletedAt on Media) rather than CASCADE, so deleting a media record doesn't take your content row with it — and so media deletion can be blocked while still referenced.

Rendering in Twig

Use the bundle's Twig extension rather than hand-building paths:

{# verify exact function name in Twig/ — commonly media_render / media_url / media_path #}
{% if page.heroImage %}
    <img src="{{ media_url(page.heroImage) }}" alt="{{ page.heroImage.name }}">
{% endif %}

Wrap in a null guard or a macro — the helpers generally assume a Media instance. For srcsets, generate derived URLs via LiipImagine and pass the original path in:

<img
    src="{{ media_url(page.heroImage)|imagine_filter('hero_sm') }}"
    srcset="{{ media_url(page.heroImage)|imagine_filter('hero_sm') }} 640w,
            {{ media_url(page.heroImage)|imagine_filter('hero_lg') }} 1280w"
    sizes="100vw" alt="">

Custom providers

If you need a provider for PDFs, audio, or a video CDN, implement the provider interface and tag the service. The interface is small: it declares what it can handle and how to produce a URL/render payload.

final class PodcastProvider implements MediaProviderInterface
{
    public function supports(Media $media): bool { /* MIME check */ }
    public function getUrl(Media $media): string { /* CDN URL */ }
}
services:
    App\Media\PodcastProvider:
        tags: ['media_monks_sonata_media.provider']  # confirm tag name in the extension

Providers are also the right seam for signed URLs, CDN rewrites, or object-key prefixing per tenant.

Storage / CDN patterns

Because everything goes through one Flysystem filesystem, environment-specific behaviour belongs in the adapter, not in application code:

  • Dev: local adapter rooted at public/media.
  • Prod: S3/GCS adapter with a CDN in front. Either point the filesystem at a public bucket and let media_url() return the CDN host, or keep the bucket private and decorate the provider to return presigned URLs.
  • Multi-tenant: register a filesystem per tenant, or prefix paths — path generation is centralized in MediaStorage, so decorating it is cleaner than sprinkling prefixes through controllers.

Front-end delivery pipeline

The bundle stores originals only. The practical stack is: bundle → LiipImagine/Glide → HTTP cache. Configure a cache_resolver keyed on the media path, run liip:imagine:cache:resolve in a warmup step if you know your hot images, and set a long Cache-Control — the underlying file path is content-addressed enough (or can be made so) that cache busting is rarely needed.

Testing

Swap in league/flysystem-memory for the test environment:

# config/packages/test/flysystem.yaml
flysystem:
    storages:
        sonata_media_filesystem:
            adapter: 'flysystem.adapter.memory'

Then functional tests can upload through the admin, assert the entity row, and assert bytes are readable from the in-memory adapter — no filesystem cleanup, no S3 mocks.


Gotchas and Tips

The project is small and thinly maintained. ~10 stars, no dependents, and a low health score. Treat it as a starting point you may end up owning: pin an exact version in composer.json, keep a fork remote configured, and skim the changelog before every composer update. There will be no security advisory feed for this one — audit it yourself against your Sonata/Flysystem versions.

Flysystem version drift is the #1 breakage source. Flysystem 1.x used write()/has(); 2.x/3.x renamed things (write() → write() on FilesystemOperator, adapter methods changed to read/write/delete/listContents). If you're on league/flysystem-bundle 3.x and the bundle was written against 1.x, you'll get "call to undefined method" at runtime, not at container compile time. Check the bundle's composer.json constraints first, and decide whether to pin Flysystem down or patch the storage service.

The filesystem service id is a magic string. Mismatch between media_monks_sonata_media.filesystem and the actual Flysystem service id fails only when an upload happens. Add a smoke test that uploads a file in CI — it catches this, missing bucket permissions, and bad adapter config in one assertion.

No migrations are shipped. doctrine:schema:update will happily create the table in dev and leave you with nothing for prod. Always go through doctrine:migrations:diff, and re-diff after upgrades since the entity can gain columns.

Orphaned files are your problem. Deleting a Media row via raw SQL or a DELETE query bypasses whatever listener cleans storage. Route all deletions through MediaManager (or add a Doctrine postRemove listener on Media), and periodically reconcile: list the Flysystem prefix, diff against providerReference values in the DB, and prune.

Sonata Admin version coupling. The admin class extends Sonata's AbstractAdmin and the form types use AdminType / ModelType. Sonata 4 dropped/changed several of these (AdminType signature, ModelAutocompleteType, route naming, configureRoutes deprecations). If you're on Sonata 4, expect to override the admin service or extend MediaAdmin and fix the form types — check Admin/ for the version it targeted.

Twig helpers assume non-null and may throw. Always guard with {% if entity.media %}. Also confirm the actual function names by reading the extension — this is the most commonly mis-remembered part of small bundles, and a wrong name gives you a Twig\Error\SyntaxError at render, not at cache warmup.

Image dimensions and focal point are metadata, not transformations. The entity records width/height (used to avoid layout shift) and a focal point — but the focal point only matters if your resizer honours it. With LiipImagine, wire a custom filter that crops relative to the stored focal point, otherwise it's dead data.

N+1 queries on media relations. Media is frequently ManyToOne from list-heavy admin pages. Set fetch: 'EAGER' on the relation or join it in the admin query — otherwise every row in a Sonata list triggers a lazy load and the list page crawls at 50 rows.

Extension points, in order of preference:

  1. Override templates — the admin list/form and any front-end fragments; safest, upgrade-friendly.
  2. Decorate MediaStorage or MediaManager — for path schemes, signed URLs, tenant prefixes. #[AsDecorator] keeps the original service intact.
  3. Custom provider — for new MIME families or CDN-specific rendering.
  4. Patch the vendor — last resort; prefer a fork with a vcs repository entry so the change is visible in composer.lock.

Debugging checklist when an upload 500s: bin/console debug:container | grep -i media to confirm services are registered → verify the filesystem id resolves → check adapter credentials/permissions → confirm the entity table exists → then look at the provider's MIME detection (some uploads have application/octet-stream and fall through to no provider).

If the bundle proves too thin for your needs, the realistic alternatives are Sonata Media Bundle (heavier, but maintained and feature-complete), VichUploaderBundle + LiipImagine (no admin UI, more wiring), or a thin in-house Media entity over Flysystem. Budget the swap cost before you build deep integrations on top of the admin form.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource