Search by

softspring / media-bundle

softspring

A media management bundle

Package info

github.com/softspring/media-bundle

Type:symfony-bundle

pkg:composer/softspring/media-bundle

Statistics

Installs: 13 155

Dependents: 4

Suggesters: 1

Stars: 3

Open Issues: 4

v5.6.0 2026-10-08 13:51 UTC

This package is auto-updated.

Last update: 2026-10-23 11:18:00 UTC


README

Latest Stable Latest Unstable License PHP Version Downloads CI Coverage

Media library, media versioning, media rendering, and admin media management for Symfony applications.

Parallel media migrations

The migration command defaults to a single process. Start with two workers and increase only after measuring CPU and memory usage, especially with animated media and AVIF (the encoders may also use multiple threads).

php bin/console sfs:media:types-migration --workers=2 --batch-size=100
php bin/console sfs:media:types-migration --workers=2 --type=background --dry-run
php bin/console sfs:media:types-migration --workers=4 --type=background --force --dry-run
php bin/console sfs:media:types-migration --workers=4 --type=background --force --no-debug

Workers partition media IDs deterministically. A media and all its versions are processed by one worker, in version order. IDs are paginated and Doctrine's identity map is cleared after each media. Each worker reports timings and counts; the parent returns a nonzero exit code if any worker fails. Dry runs report version changes without modifying media or storage. Existing matching versions are not regenerated; rerunning checks the remaining differences.

Use --force to rebuild generated versions even when their configuration is unchanged, for example after updating image codecs. It preserves _original and configured manual/uploaded versions. Normal migration cleanup of obsolete versions still applies. Combine it with --type to limit its scope and with --dry-run to inspect the plan without writing anything. Without --dry-run, forced regeneration replaces generated versions and uploads their new files; it can be expensive. Force is opt-in and is forwarded to every worker.

Distributed runners can use --worker-index=0 --worker-count=4 (indexes 0–3), instead of --workers. Every task must use the same configuration, filters and worker count, and a unique index. Do not run overlapping migration jobs against the same media concurrently: partitioning is not a cross-job lock. Do not change media type configuration during a run. No Messenger worker or PHP threading extension is required; local workers use Symfony Process and the project's bin/console. The worker count must fit the resources allocated to the container.

Regenerate versions from the admin

The media detail page includes Regenerate versions when the user has the media migration permission and the media has an original file and generated versions. This opens a confirmation page listing the generated version keys. Only a confirmed, CSRF-protected POST starts the synchronous regeneration. Originals and configured manual uploads are preserved; generated versions are rebuilt even when their options already match. Normal cleanup of obsolete versions still applies. On completion the user returns to the media detail page with a success/error notification. Large animations may take time: wait for the result before retrying. The standard and Sylius admin templates are supported.

What It Provides

  • configurable media types for images and videos
  • generated and manual media versions
  • filesystem and Google Cloud Storage drivers
  • Twig rendering helpers for images, pictures, videos, and video sets
  • admin media library screens
  • migration tools when type definitions change

Static image processing driver

Static generated images use Imagick by default. Override the default globally, per media type or per version (version > type > global):

sfs_media:
    imagine:
        driver: imagick # imagick | gd
        allow_upscale: false
        resize_tolerance: 0.02
        preserve_original_encoding: true
    types:
        content:
            imagine_driver: gd
            versions:
                small:
                    type: webp # inherits gd
                    scale_width: 400
                large:
                    type: avif
                    imagine_driver: imagick
                    scale_width: 1200

Install ext-imagick and the ImageMagick codecs used by your sources and outputs. On Alpine, AVIF support requires imagemagick-heic (libheif), in addition to the PHP Imagick extension. Configuration checks the selected driver and its codecs; there is no silent fallback to GD. Set imagine.driver: gd to keep using GD.

The resolved imagine_driver is stored in each static generated version's options, so adding or changing the effective driver is detected by the media migration checker, including existing versions created before this option existed. Run the normal media migration explicitly to regenerate files; changing configuration does not itself run a migration. Type/global driver changes do not affect versions with an explicit override. Animated FFmpeg versions and manual uploads do not inherit an Imagine driver. resampling-filter is passed to the resize operation; Imagick supports configurable filters, while GD keeps its native resampling.

The three resize/encoding options follow the same global → type → version inheritance (use their names directly on a type or version). Their effective values are also persisted and checked for regeneration:

  • allow_upscale: false caps dimensions at the source size.
  • resize_tolerance: 0.02 skips proportional reductions of 2% or less. For example, a 1127px source requested at 1120px stays at 1127px. Set 0 for strict resizing. Tolerance does not apply when both width and height are explicitly requested.
  • preserve_original_encoding: true keeps an independent byte-for-byte copy when dimensions and format remain unchanged, unless flattening or an explicit output resolution is requested. Quality settings do not force recompression in this case; set this option to false to force encoding. Format conversions and larger reductions still encode normally.

Picture width descriptors use the actual generated width and duplicate widths within a source are removed. These options apply only to static generated images, not animated FFmpeg versions or manual uploads. Existing stored files are unchanged until a media migration/regeneration is explicitly run.

Delayed Google Cloud Storage deletion

Set sfs_media.google_cloud_storage.delayed_deletion_days to a positive integer to keep a removed object available at its existing URL until the configured delay has elapsed:

sfs_media:
    google_cloud_storage:
        bucket: media-bucket
        delayed_deletion_days: 90

The driver sets the object's Custom-Time to the delayed deletion date. Configure the bucket with this Google Cloud Storage lifecycle rule, without a matchesPrefix condition:

{
  "rule": [
    {
      "action": {"type": "Delete"},
      "condition": {"daysSinceCustomTime": 0}
    }
  ]
}

For example, save it as lifecycle.json and apply it with:

gcloud storage buckets update gs://media-bucket --lifecycle-file=lifecycle.json

Without that lifecycle rule, marked objects are not deleted automatically.

This rule only affects objects with Custom-Time set. Ensure that no other process using the same bucket sets Custom-Time, or its objects will also be eligible for deletion when that time is reached.

Armonic

This package is part of Armonic.

Documentation

Armonic Documentation

Configured media selections

Media fields restrict both their modal choices and submitted values to media_types. For example, content: { picture: two_columns } exposes only picture#two_columns, not raw image versions or other picture sets. Image, video, picture and video-set choices follow the configured order and must exist on the selected media. The version selector is hidden for zero or one choice. Existing invalid selections remain visible as invalid; migrate saved module data before expecting it to validate under a narrower allow-list. Rebuild/install the bundle's assets when updating.

Picture display dimensions

By default, a rendered <picture> takes its <img> width and height from img.src_version. When that fallback has different dimensions from the responsive sources, use img.dimensions_version to provide the intrinsic display size without changing the fallback URL:

pictures:
    card:
        sources:
            - { srcset: [ { version: card, suffix: '1x' }, { version: card_retina, suffix: '2x' } ] }
        img:
            src_version: fallback
            dimensions_version: card

Explicit image attributes passed to the renderer still take precedence over the configured dimensions version.

Animated image versions

Generated animated versions use ffprobe to validate the source and ffmpeg to resize and encode every frame. Both binaries must be installed in the application runtime. Processing is synchronous; applications should keep duration and frame limits conservative until background generation is available.

sfs_media:
    ffmpeg:
        binary: ffmpeg
        probe_binary: ffprobe
        timeout: 300

    types:
        animation:
            upload_requirements:
                mimeTypes: [image/avif, image/apng, image/gif]
            versions:
                small_avif:
                    type: avif
                    animated: true
                    scale_width: 400
                    avif_quality: 82
                    animation:
                        loop: 0
                        speed: 6
                        max_duration: 3
                        max_frames: 75
                small_webp:
                    type: webp
                    animated: true
                    scale_width: 400
                    webp_quality: 82
                    animation:
                        loop: 0
                        max_duration: 3
                        max_frames: 75

Supported generated targets are AVIF, WebP and APNG. type: keep also preserves animated GIF input. Source timing and frame rate are preserved unless animation.fps is set.

Animated WebP outputs use independent full frames. This is slightly larger than lossy partial-frame animation, but avoids accumulated composition errors that appear as trails or stripes in browsers.

When at least one version uses animated: true, configuration compilation checks that the configured ffmpeg and ffprobe binaries are executable. It fails early with their configured names and installation instructions if either binary is missing. Types without animated versions do not require FFmpeg.

FFmpeg 6.1, currently shipped by Alpine 3.22, can encode animated WebP but cannot reliably demux it as an input. Use AVIF, APNG or GIF uploads with that runtime. Animated WebP can be enabled as an upload source when the deployed FFmpeg build includes its newer animated WebP demuxer.

Animation options:

  • fps: resample the output frame rate, from 1 to 120.
  • loop: output loop count; 0 means infinite.
  • crf: AVIF constant-quality value, from 0 to 63. When omitted, it is derived from avif_quality.
  • speed: AVIF encoding speed (libaom-av1 cpu-used), from 0 to 8.
  • keyframe_interval: encoder GOP size.
  • max_duration: reject longer source animations, in seconds.
  • max_frames: reject sources containing more frames.

The processor only handles generated versions explicitly marked with animated: true. Manual versions keep their uploaded file, and regular generated images use the configured Imagine driver.

Create from an existing original

The media detail screen offers a Create from this type dropdown. It creates a new media from the source's _original, copying its name, description and all alt translations. Original and generated files belong to the new media; deleting or editing either media does not modify the other's files or translations.

Destinations must be public image types other than the source type, satisfy the original's stored dimensions, orientation, ratio, pixels, MIME type and size requirements, and have no independently uploaded versions. The downloaded file is validated again against the destination's upload requirements before creation. Generation uses the existing synchronous processing pipeline. The POST action requires create/read permissions and a source-specific CSRF token.

Package Files

Contributing

Use the standard package commands before sending changes:

composer fix
composer test
composer test-bc

See the contributing guide.

Report issues and send Pull Requests

Security

Please report vulnerabilities privately. See SECURITY.md.

License

This package is free and released under the AGPL-3.0 license.