Media Bundle
The Media Bundle manages stored media files, generated versions, rendering helpers, and an optional admin media library.
It is designed around one idea: media behaviour is not hard-coded in PHP classes. It is defined by media types in configuration, and those types drive:
- what can be uploaded;
- which extra versions exist;
- which versions are generated or uploaded manually;
- how media is rendered in Twig;
- how admin create and update forms behave.
The bundle supports image and video media. Storage is delegated to a storage driver, and image transformations are handled through GD via imagine/imagine.
Documentation Map
Use this page as the full reference and start with the focused guides when you need to implement a specific part:
- Install
- package installation, base configuration, routes, and first required setup
- Getting Started
- the shortest practical path to get uploads, versions, and rendering working
- Concepts
- media entities, versions, processing pipeline, migration, and duplicate detection
- Configure Media Types
- type config, upload rules, generated versions, pictures, and video sets
- Using Medias
- Twig rendering, upload forms, selectors, and day-to-day usage patterns
- Admin Medias
- admin routes, permissions, list/create/update/read/delete, and migrate flows
- Integrations
- Doctrine migrations, CMS translation field caveats, and provider-based type configuration
- Storage Options
- filesystem vs Google Cloud Storage and the implications of each
- Name Generators
- how destination names are generated and how to replace that behavior
- Extending Bundle
- type providers, processors, forms, listeners, templates, and extension points
If you are integrating the bundle for the first time, the usual reading order is: install, getting started, media types, using medias, then admin or extension guides depending on the project.
Installation
Install the bundle:
composer require softspring/media-bundle:^6.0
If your application does not use Symfony Flex, enable it manually:
<?php
return [
// ...
Softspring\MediaBundle\SfsMediaBundle::class => ['all' => true],
];
Requirements
The package requires:
- PHP
8.4or higher - Doctrine ORM
- PHP
gd imagine/imaginegoogle/cloud-storage
google/cloud-storage is a direct package requirement in the current codebase, even if your project only uses filesystem storage.
Image processing support also depends on what your GD build can actually read and write. The configuration validator checks that before the container is built.
Main Concepts
The bundle has three core runtime objects:
MediaInterface- the logical media entry
- stores type, metadata, privacy flag, SHA-1, and all versions
MediaVersionInterface- one stored or generated version of a media
- stores URL, mime type, dimensions, file size, timestamps, and processing options
- media type configuration
- one configuration block under
sfs_media.types - defines the business rules for that family of media
- one configuration block under
The bundle does not treat versions as simple strings. Every version is a Doctrine entity with its own metadata and storage URL.
Data Model
By default, the bundle uses:
Softspring\MediaBundle\Entity\MediaSoftspring\MediaBundle\Entity\MediaVersion
Those concrete entities extend these mapped model classes:
Softspring\MediaBundle\Model\MediaSoftspring\MediaBundle\Model\MediaVersion
Media Entity
The media model stores:
idmediaTypeprivatetypenamedescriptioncreatedAtsha1altTexts- the collection of versions
The type field is the configured media type key such as hero_image or homepage_video.
The mediaType field is the normalized numeric category:
- image
- video
- unknown
Media Version Entity
Each version stores:
idmediaversionurlwidthheightfileSizefileMimeTypeuploadedAtgeneratedAtoptionssha1
_original is a special version name. It always represents the main uploaded file.
Generated versions also keep a non-persisted originalVersion reference while they are being processed.
Replacing The Entity Classes
You can replace the default entity classes:
# config/packages/sfs_media.yaml
sfs_media:
media:
class: App\Entity\Media
version:
class: App\Entity\MediaVersion
The bundle resolves MediaInterface and MediaVersionInterface to those configured classes through a compiler pass.
Storage Drivers
The bundle ships two storage drivers:
filesystemgoogle_cloud_storage
Filesystem Driver
Example:
sfs_media:
driver: filesystem
filesystem:
path: '%kernel.project_dir%/public/media'
url: '/media'
If you do not configure it explicitly, the bundle falls back to those same defaults.
The filesystem driver stores logical URLs with the internal prefix:
sfs-media-filesystem://...
and can translate them into public URLs later.
Google Cloud Storage Driver
Example:
sfs_media:
driver: google_cloud_storage
google_cloud_storage:
bucket: '%env(MEDIA_BUCKET_NAME)%'
The storage driver stores URLs internally as:
gs://bucket/path/to/file
and exposes them publicly through the standard Google Storage HTTPS URL.
Driver Responsibilities
Both drivers implement the same four operations:
store()remove()download()url()
That matters because generated versions may need to download the original file again during regeneration or migration.
Media Types
Everything important in this bundle starts with sfs_media.types.
A media type defines:
- whether the media is an image or a video;
- whether it is private;
- which upload constraints apply;
- which derived versions exist;
- how pictures or video sets are rendered;
- which file name generator to use.
Example:
sfs_media:
types:
article_image:
type: image
name: 'Article image'
description: 'Responsive images used inside article pages'
private: false
upload_requirements:
mimeTypes: ['image/jpeg', 'image/png', 'image/webp']
minWidth: 1200
minHeight: 675
versions:
card:
type: webp
scale_width: 480
webp_quality: 80
article:
type: webp
scale_width: 1280
webp_quality: 85
pictures:
default:
sources:
- srcset:
- { version: card, suffix: '480w' }
- { version: article, suffix: '1280w' }
attrs:
sizes: '100vw'
img:
src_version: article
Type Configuration Fields
The most important fields are:
typeimageorvideo
namedescriptionprivategeneratorupload_requirementsversionspicturesvideo_sets
Private Media
private: true marks the media entity as private at form submit time.
The bundle does not add a signed-URL system or an access-control layer for private assets by itself. It only persists the flag and uses it in some admin search flows.
Name Generators
The generator field points to a name generator service class.
The default generator is Softspring\MediaBundle\Media\DefaultNameGenerator.
It generates a path based on:
- the media id when available;
- the version name;
- the uploaded file extension.
You can plug another generator by implementing NameGeneratorInterface. Services implementing that interface are auto-tagged and collected by NameGeneratorProvider.
Upload Requirements
The bundle validates uploads through a custom Image constraint and ImageValidator.
Supported upload requirements include:
minWidthminHeightmaxWidthmaxHeightminRatiomaxRatiominPixelsmaxPixelsallowSquareallowLandscapeallowPortraitdetectCorruptedmimeTypesmaxSizebinaryFormat
Mime Type Validation
The configuration layer already validates:
- that configured mime types are supported by the current PHP environment;
- that image types only allow image mime types;
- that video types only allow video mime types;
- that version output formats are supported by the current GD build.
This is especially relevant for:
webpavifapng
If your environment cannot encode or decode one of those formats, container compilation fails with a configuration error instead of failing later at runtime.
APNG Support
The bundle has explicit APNG support:
- the config validator accepts
image/apngwhen PNG support exists; - the upload validator removes false-positive mime-type violations for APNG files detected as
image/png; StoreFileProcessorstores APNG uploads withimage/apng;ImagineProcessordoes not attempt to scale APNG files when the target remains APNG.
That is one of the more specialized parts of the bundle and is worth relying on only if your environment actually supports the needed image functions.
Versions
Every media always has an _original version.
Configured versions fall into two categories:
- generated versions
- uploaded versions
Generated Versions
A generated version is a version without its own upload_requirements.
It is derived from another version, usually _original, using processing options such as:
typefromscale_widthscale_heightpng_compression_levelwebp_qualityjpeg_qualityavif_qualityflattenresolution-xresolution-yresolution-unitsresampling-filter
If type is omitted for image versions, the bundle normalizes it to keep.
If resampling-filter or resolution-units are omitted for generated image versions, the bundle fills them with defaults in Configuration::fixConfigTypes().
Uploaded Versions
A version with its own upload_requirements is not generated automatically from another file. It is expected to be uploaded explicitly.
This is how you model things such as:
- alternative manually exported formats;
- video poster images;
- video encodings that come from an external pipeline.
Version Options In Database
When a version file is finally stored, StoreFileProcessor removes some configuration-only keys before persisting the version options:
upload_requirementsfrom
That is why migration compares actual stored version options against normalized type config instead of just comparing the raw config blocks directly.
Processing Pipeline
The real upload and generation flow is driven by Doctrine entity listeners plus tagged processors.
When Version Entities Are Created
MediaListener::preFlush() ensures that:
_originalexists;- every configured version entity exists.
This happens automatically on regular media saves.
The listener skips this behaviour while the media manager is in migration mode, because migration handles versions explicitly.
Processor Order
Processors are auto-tagged and ordered by priority:
UploadedImageSizeProcessor255VersionFileCopyProcessor200ImagineProcessor0StoreFileProcessor-100
That order explains the lifecycle:
- read image dimensions from uploaded files;
- copy or download the original into a temp file for generated versions;
- transform the temp file when the version is generated from an image;
- store the final file and persist the storage URL.
UploadedImageSizeProcessor
This processor runs only for uploaded files.
It extracts width and height for image uploads before storage.
VersionFileCopyProcessor
This processor prepares generated versions.
If a non-original version has an originalVersion:
- and that original still has a local uploaded file, it copies it;
- otherwise, it downloads the original from the storage driver into a temp file.
This is what makes post-persist generation and later migrations possible.
ImagineProcessor
This processor handles generated image versions.
It only supports:
- non-
_originalversions; - versions without
upload_requirements; - versions linked to an original image file;
- target output types in
jpeg,png,gif,webp,avif, orkeep.
It can:
- resize by width;
- resize by height;
- resize to fixed width and height;
- change format;
- set image save options such as quality or compression.
It does not support arbitrary video transcoding. Video versions that need conversion must come from an external pipeline and be uploaded as explicit version files.
StoreFileProcessor
This processor runs for every version that currently has an upload file.
It:
- normalizes stored options;
- detects APNG uploads;
- fills mime type and file size;
- computes the SHA-1;
- asks the selected name generator for the destination name;
- stores the file through the configured storage driver;
- removes the temp file unless
keepTmpFileis true.
This processor is the final step of the pipeline.
Regeneration And Original Replacement
MediaVersionListener::onFlush() contains an important regeneration rule.
When the _original version is updated with a new upload:
- its URL is reset;
- generated versions that already exist and have
generatedAtare also reset; - those generated versions get the new original version as source.
This forces the bundle to regenerate derived versions from the new original file.
This behaviour is one of the main reasons the version model is more than just a static metadata table.
Rendering In Twig
The bundle provides both Twig filters and Twig functions:
sfs_media_render_imagesfs_media_render_picturesfs_media_render_videosfs_media_render_video_setsfs_media_rendersfs_media_image_urlsfs_media_type_config
Basic Rendering Examples
{{ media|sfs_media_render_image('card') }}
{{ media|sfs_media_render_picture('default') }}
{{ media|sfs_media_render_video('mp4', { controls: true }) }}
{{ media|sfs_media_render_video_set('default') }}
{{ media|sfs_media_image_url('article') }}
The generic renderer accepts strings like:
{{ media|sfs_media_render('image#card') }}
{{ media|sfs_media_render('picture#default') }}
{{ media|sfs_media_render('video#mp4') }}
{{ media|sfs_media_render('videoSet#default') }}
Picture Rendering
renderPicture() reads the current media type config and builds:
- one
<picture>tag; - one
<source>tag per configured source block; - one final
<img>tag for the fallback image.
If the picture config name does not exist for the media type, the renderer throws an exception.
Video Set Rendering
renderVideoWithSources() reads the video_sets block and builds:
- one
<video>tag; - one
<source>tag per configured version; - an optional
posterattribute whenposter_versionis configured.
This is useful for video formats such as MP4 and WebM served together.
Alt Text Fallback
For image rendering, the <img> alt attribute is resolved in this order:
- translated
altTexts - media
description - media
name
That fallback chain is worth following in your data model if you want accessible defaults.
Rendering By Id Or Entity
The renderer accepts either:
- a
MediaInterfaceobject; - or a media id string.
If you pass a string id, the renderer loads the media through Doctrine.
Forms
The bundle exposes several forms, but the two most important families are:
- upload forms
- media selection forms
MediaTypeUploadType
MediaTypeUploadType creates a media upload form for one configured media type.
Important options:
media_typerequired_uploadsallow_name_fieldallow_description_fieldallow_alt_text_field
The form automatically adds:
namedescription_original- one field for every version with
upload_requirements altTexts
At submit time, it also sets:
- the media type key;
- the
privateflag from the type config.
MediaVersionUploadType
MediaVersionUploadType is the inner form used for each version upload field.
It applies the custom Image constraint built from the configured upload requirements.
Alt Text Field Caveat
In the current code, MediaTypeUploadType uses Softspring\CmsBundle\Form\Type\TranslationType for altTexts.
If your project does not expose that form type, disable the field:
$builder->add('media', MediaTypeUploadType::class, [
'media_type' => 'article_image',
'allow_alt_text_field' => false,
]);
or replace the form type in your own application.
MediaChoiceType
MediaChoiceType is an EntityType specialized for selecting existing media entries.
It filters by media_types and can preload preview HTML in the choice attributes.
The media_types option is not just a list of allowed type keys. It also defines which rendering mode and version should be used for previews, for example:
[
'article_image' => ['picture' => 'default'],
'teaser_video' => ['video' => 'mp4'],
]
Preview attributes are only generated when the form field has data-media-preview-input.
Modal Selectors
MediaModalType and MediaVersionModalType support the admin-style media picker.
They can:
- restrict the valid media types;
- expose preview configuration to the frontend through data attributes;
- open the admin search route for media selection.
The search route is built around public media only.
Admin UI
The bundle can expose a full media library UI through softspring/crudl-controller.
Enable the built-in admin controller:
sfs_media:
media:
admin_controller: true
Import routes:
_sfs_media_admin:
resource: '@SfsMediaBundle/config/routing/admin_media.yaml'
prefix: /admin/media
Import the role hierarchy when you use the default permission names:
imports:
- { resource: '@SfsMediaBundle/config/security/admin_role_hierarchy.yaml' }
Admin Actions
The admin controller defines these actions:
- list
- create
- update
- delete
- read
- read_ajax
- search_type
- create_ajax
- migrate
This is a richer surface than the current docs page suggested.
Search Type Action
The search_type action is the route used by modal selectors.
MediaSearchTypeListener forces:
private = falsetype__in = <valid types from the route>when not already filtered
So the modal search flow is intentionally limited to public media.
Migrate Action
The migrate admin action runs the same migration logic as the command, but only for one media item.
It is useful when:
- one type definition changed;
- one media entry is missing generated files;
- one media needs repair without migrating the whole library.
Type Migration
When type definitions change, old database rows and stored files do not update automatically.
That is what sfs:media:types-migration is for:
php bin/console sfs:media:types-migration
The command loads all media entities, switches the manager into migration mode, and checks every media against the current type config.
What Migration Compares
TypeChecker::checkMedia() classifies versions into:
oknewchangeddeletemanual
Those categories mean:
ok- the stored version matches the current config
new- the version exists in config but not in database and can be generated automatically
changed- the version exists but its stored options differ from the current config
delete- the version exists in database but no longer exists in config
manual- the version is new in config but needs a manual upload because it has
upload_requirements
- the version is new in config but needs a manual upload because it has
What Migration Actually Does
During migration, the media manager:
- creates missing auto-generated versions;
- recreates changed versions;
- deletes removed versions;
- keeps manual-upload versions marked as manual;
- updates missing SHA-1 values;
- updates missing file sizes.
This distinction between generated and manual versions is one of the most important operational details of the bundle.
Duplicate Detection
MediaManager also contains duplicate helpers based on:
- media type
- SHA-1 of the
_originalversion
Available helpers:
findMediaDuplicates()findDuplicates()getDuplicatesStats()
They are implemented directly in the manager today rather than in a repository, but they are still useful when cleaning a large media library.
Doctrine Migrations Path
The bundle prepends its own migrations path into Doctrine Migrations:
Softspring\MediaBundle\Migrations=>@SfsMediaBundle/src/Migrations
This is part of the runtime setup and matters if your project relies on bundle-provided schema migrations.
Important Implementation Details
These are the parts of the current codebase that are easy to miss and worth knowing before you depend on a behaviour.
Filesystem Public URL Caveat
MediaVersion::getPublicUrl() hardcodes filesystem media URLs as /media/... when the stored URL uses the internal filesystem prefix.
Because MediaRenderer prefers getPublicUrl() over StorageDriverInterface::url(), a custom configured filesystem.url is not always reflected in rendered URLs.
That means the documentation-friendly configuration:
filesystem:
url: '/custom-media'
does not fully match the current rendering behaviour in every code path.
Video Processing Is Storage-Oriented, Not Transcoding-Oriented
The bundle understands video media types and video set rendering, but it does not include a real transcoder.
Video versions with their own uploads are modeled as manually uploaded files, not as FFmpeg-style generated outputs.
Processor Pipeline Depends On Doctrine Lifecycle
The processing pipeline is attached to Doctrine entity listeners.
That means version generation and storage happen when version entities are persisted or updated, not when a standalone helper service is called directly.
Modal Search Excludes Private Media
The modal search flow forces private = false.
If your application needs private media selection in those modals, you need to override that listener or provide a different search flow.
Type Config Is Merged From Providers
The default provider reads sfs_media.types, but the bundle architecture allows extra MediaTypeProviderInterface services.
So type definitions can come from more than one place.
For advanced projects, that is the better extension point than trying to keep all type config in one very large YAML file.