Skip to content

Media

The media system manages images, videos, and documents through three independent layers: storages (where files physically live), providers (how callers request a file or an image variant), and source providers (how content is fetched from external URLs). A central MediaEntry data model ties them together.


MediaEntry

MediaEntry is the data model for every managed file. Key fields:

Field constant Description
FIELD_PATH Storage-relative path to the primary file.
FIELD_TYPE Media type — TYPE_IMAGE, TYPE_VIDEO, or TYPE_FILE.
FIELD_EXTENSION File extension (without dot).
FIELD_STORAGE_PROVIDER Name of the storage that owns this entry.
FIELD_SOURCE_PATH Original URL when the entry was imported from an external source.
FIELD_SOURCE_PROVIDER Name of the source provider used to fetch the original content.
FIELD_HASH Checksum of the stored file.
FIELD_SIZE File size in bytes.
FIELD_ORIGINAL_NAME Filename as uploaded or imported.
FIELD_META JSON bag for arbitrary metadata (see META_* constants).
FIELD_VARIANTS JSON array of processed image variants (resized/converted copies).
FIELD_PARENT_TYPE Type of the owning entity (e.g. 'product').
FIELD_PARENT_ID ID of the owning entity.
FIELD_FEATURED Whether this is the primary media for its parent.
FIELD_POSITION Sort position within the parent's gallery.

META_* constants

Constant Key Description
META_WIDTH 'width' Original image width in pixels.
META_HEIGHT 'height' Original image height in pixels.
META_ALT_TEXT 'alt_text' Alt text for images.
META_IMAGE_QUALITY 'image_quality' Quality setting used when the variant was generated.
META_PROVIDER_NAME 'provider_name' Image service that generated the variant ('gd', 'vips').
META_IMMEDIATE_PROCESSING 'immediate_processing' Flag: process synchronously on save.
META_PASSTHROUGH 'passthrough' Flag: copy file without resizing/converting.

Flags

$entry->requireImmediateProcessing(); // process synchronously rather than via worker queue
$entry->requirePassthrough();         // skip image conversion, serve original format

Media URI

MediaEntry::MEDIA_URI ('media://') is an internal URI scheme used in configuration values that reference media entries (e.g. media://path/to/file.jpg).


Storages

A storage is responsible for reading, writing, and deleting the physical file. Implement StorageInterface to add a new storage backend.

StorageInterface

Method Description
static getName(): string Unique machine name for this storage.
static getLabel(): string Human-readable label shown in the backoffice.
fetch(MediaEntry): mixed Return raw file content as a string.
download(MediaEntry): ?Response Return a Symfony streamed HTTP response for download.
create(mixed $content, string $filename, string $subFolder, ?MediaEntry): MediaEntry Store content and populate the entry's metadata fields. Content may be a string or a PHP resource.
delete(MediaEntry): void Delete the file and all its variants.
getFilesystem(): FilesystemOperatorInterface Expose the underlying filesystem operator.

StorageInterface also declares the media type constants:

Constant Value
MEDIA_TYPE_IMAGE 'image'
MEDIA_TYPE_VIDEO 'video'
MEDIA_TYPE_PDF 'pdf'

Built-in storage

Name Class Description
merchant_public_filesystem MerchantPublicFileSystem Writes to the merchant's public media filesystem (via FilesystemPoolInterface::getMediaWriter()). Generates public URLs using the merchant's base media URL. Default storage for all entries.

MerchantPublicFileSystem::create() slugifies the filename, appends a random 32-character hex UID, and writes under {subFolder}/{slug}-{uid}.{extension}. This ensures collision-free paths even for files with identical original names.

StoragePoolInterface

Inject StoragePoolInterface to look up a storage by name or by a MediaEntry:

use Amarant\Framework\Contract\Media\StoragePoolInterface;

public function __construct(
    private readonly StoragePoolInterface $storagePool
) {
}

public function fetchContent(MediaEntry $entry): mixed
{
    $storage = $this->storagePool->match($entry); // resolves by $entry->getStorageProvider()
    return $storage?->fetch($entry);
}
Method Description
match(MediaEntry): ?StorageInterface Resolve by $entry->getStorageProvider().
get(string $name): ?StorageInterface Resolve by name string.
getDefaultStorage(): StorageInterface Get the default storage (throws MediaException if not found).
getDefaultStorageName(): string Returns 'merchant_public_filesystem' by default.

Adding a custom storage

use Amarant\Framework\Contract\Media\StorageInterface;
use Amarant\Framework\Contract\Io\FilesystemOperatorInterface;
use Amarant\Framework\DataModel\MediaEntry;
use Symfony\Component\HttpFoundation\Response;
use Override;

final class CdnStorage implements StorageInterface
{
    #[Override] public static function getName(): string
    {
        return 'vendor_module_cdn';
    }

    #[Override] public static function getLabel(): string
    {
        return 'CDN Storage';
    }

    #[Override] public function fetch(MediaEntry $entry): mixed { /* ... */ }
    #[Override] public function download(MediaEntry $entry): ?Response { /* ... */ }
    #[Override] public function create(mixed $content, string $filename, string $subFolder, ?MediaEntry $mediaEntry = null): MediaEntry { /* ... */ }
    #[Override] public function delete(MediaEntry $entry): void { /* ... */ }
    #[Override] public function getFilesystem(): FilesystemOperatorInterface { /* ... */ }
}

Register via DiEnum::MEDIA_STORAGE:

Vendor/ModuleName/Configuration/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\Media\Storage\CdnStorage;

$container->tag(
    [CdnStorage::class],
    [DiEnum::MEDIA_STORAGE]
);

Image provider

The image provider produces resized and converted image variants from a MediaEntry. Inject ImageProviderInterface directly — it is a singleton bound to DefaultImageProvider.

use Amarant\Framework\Contract\Media\ImageProviderInterface;

public function __construct(
    private readonly ImageProviderInterface $imageProvider
) {
}

public function getThumbnail(MediaEntry $entry): ?string
{
    $response = $this->imageProvider->get($entry, newWidth: 400, newHeight: 300);
    return $response?->getUrl();
}

ImageProviderInterface

Method Description
static getName(): string Provider identifier.
get(MediaEntry, ?int $newWidth, ?int $newHeight, ?int $newQuality): ?ImageProviderResponseInterface Resize and convert the image, write the variant, return its metadata. Returns null if the entry is not a MEDIA_TYPE_IMAGE or the source file is missing.
getPassThrough(MediaEntry): ?ImageProviderResponseInterface Copy the original file without resizing or converting. Returns null if the entry is not an image.
remove(MediaEntry): void Delete the entry's file from storage.

get() always outputs WebP. The variant filename follows the pattern {original-slug}-{uid}-{width}x{height}.webp. When neither width nor height is specified, the image is processed at its original dimensions.

ImageProviderResponseInterface

Method Description
getUrl(): string Public URL of the processed variant.
getPath(): string Storage-relative path of the variant file.
getWidth(): int Actual output width in pixels.
getHeight(): int Actual output height in pixels.
getSize(): int File size in bytes.
getExtension(): string Output extension (always 'webp' for processed variants).
getQuality(): int JPEG/WebP quality used (1–100).
getProviderName(): string Image service that rendered the variant ('gd', 'vips', or 'passthrough').

Image quality

Quality is resolved in the following order:

  1. The $newQuality argument to get().
  2. The AMA_IMAGE_SERVICE_QUALITY environment variable.
  3. ImageServiceRequestBuilder::DEFAULT_QUALITY (75).

Quality must be in the range 1–100 inclusive.


Image service

ImageServiceInterface is the low-level image processing backend. DefaultImageProvider delegates to it. The active implementation is swapped by area:

Implementation Area Backend Notes
GdImageService All areas (default) PHP GD extension Uses imagescale() + imagewebp(). Aspect-ratio-preserving.
VipsImageService CLI only libvips via FFI (php-vips) Uses thumbnail_buffer(). Faster for large images. Requires AMA_IMAGE_SERVICE_VIPS_ENABLED=true.

Both services always output WebP.

ImageServiceRequestBuilder

Build an ImageServiceRequestInterface before passing it to ImageServiceInterface::process():

use Amarant\Framework\Media\Image\ImageServiceRequestBuilder;

$request = $this->requestBuilder
    ->withWidth(800)
    ->withHeight(600)
    ->withQuality(80)
    ->withInputImageWidth($entry->getMeta()[MediaEntry::META_WIDTH] ?? null)
    ->build(
        filename: 'photo.jpg',
        extension: 'jpg',
        content: $rawImageBytes
    );

$response = $this->imageService->process($request);

build() resets the builder state, so the same instance can be reused for multiple requests.

Builder method Default Description
withWidth(int) 0 Target width in pixels. 0 = no width constraint.
withHeight(int) 0 Target height in pixels. 0 = derive from width (aspect-ratio).
withQuality(int) 75 WebP quality (1–100).
withInputImageWidth(?int) null Pass the known source width so Vips can skip an extra decode pass.

ImageServiceResponse

Method Description
getContent(): string Raw WebP bytes.
getWidth(): int Actual output width.
getHeight(): int Actual output height.
getProviderName(): string 'gd' or 'vips'.

File provider

The file provider resolves the public URL and metadata for non-image media entries (documents, PDFs, videos). Inject FileProviderInterface directly — it is a singleton bound to DefaultFileProvider.

use Amarant\Framework\Contract\Media\FileProviderInterface;

public function __construct(
    private readonly FileProviderInterface $fileProvider
) {
}

public function getDocumentUrl(MediaEntry $entry): ?string
{
    return $this->fileProvider->get($entry)?->getUrl();
}

FileProviderInterface::get(MediaEntry): ?FileProviderResponseInterface returns null when no storage is matched or the entry has no path.

FileProviderResponseInterface

Method Description
getUrl(): string Public URL of the file. Uses getPublicUrl() from the storage filesystem when available.
getPath(): string Storage-relative path.
getSize(): int File size in bytes.
getExtension(): string File extension.

Source providers

A source provider fetches raw media content from an external location and delivers it to a caller via callbacks. The primary use case is bulk import workflows that pull images from external URLs.

SourceProviderInterface

Method Description
static getName(): string Unique name string.
stream(iterable $entries, Closure $onSuccess, Closure $onFailure, ?Closure $before): void Fetch a batch of entries concurrently.
single(MediaEntry, Closure $onSuccess, Closure $onFailure, ?Closure $before): void Fetch a single entry synchronously.

Both methods share the same callback signatures: - $onSuccess(MediaEntry $entry, mixed $response, int $index): void - $onFailure(MediaEntry $entry, Throwable $exception, int $index): void - $before(MediaEntry $entry, int $index): mixed|null — called before the actual fetch; return a non-null value to skip the fetch and pass that value directly to $onSuccess.

Built-in source provider

Name Class Description
http_source_provider HttpSourceProvider Fetches via HTTP/HTTPS. stream() uses Guzzle CurlMultiHandler for concurrent requests (connect timeout: 10s, read timeout: 30s). single() is synchronous. Automatically rewrites Dropbox share URLs to direct download URLs.

HttpSourceProvider is the default when a MediaEntry has no sourceProvider set.

SourceProviderPool

SourceProviderPool resolves a provider by name:

Method Description
match(MediaEntry): ?SourceProviderInterface Resolve by $entry->getSourceProvider().
get(string $name): ?SourceProviderInterface Resolve by name.
getDefaultProvider(): SourceProviderInterface Get http_source_provider (throws MediaException if missing).
getProviderNames(): string[] List all registered provider names.

Adding a custom source provider

use Amarant\Framework\Contract\Media\SourceProviderInterface;
use Amarant\Framework\DataModel\MediaEntry;
use Closure;
use Override;

final class S3ImportSourceProvider implements SourceProviderInterface
{
    #[Override] public static function getName(): string
    {
        return 'vendor_module_s3_import';
    }

    #[Override] public function stream(iterable $mediaEntries, Closure $onSuccess, Closure $onFailure, ?Closure $before = null): void
    {
        foreach ($mediaEntries as $index => $entry) {
            $this->single($entry, $onSuccess, $onFailure, $before);
        }
    }

    #[Override] public function single(MediaEntry $entry, Closure $onSuccess, Closure $onFailure, ?Closure $before = null): void
    {
        // fetch from S3, call $onSuccess or $onFailure
    }
}

Register via DiEnum::MEDIA_SOURCE_PROVIDER:

Vendor/ModuleName/Configuration/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\Media\SourceProvider\S3ImportSourceProvider;

$container->tag(
    [S3ImportSourceProvider::class],
    [DiEnum::MEDIA_SOURCE_PROVIDER]
);

Upload facade

MediaGalleryUploadFacade handles browser file uploads to the media gallery. It accepts an array of Symfony UploadedFile objects, creates a MediaEntry for each one (using MediaEntryFactory::createFromUpload()), marks each entry for immediate processing, saves them, and returns a collection.

use Amarant\Framework\Media\MediaGalleryUploadFacade;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/** @var UploadedFile[] $files */
$collection = $this->mediaGalleryUploadFacade->execute($files);

The facade sets MediaGalleryEnum::MEDIA_ENTRY_PARENT_TYPE as both the subFolder and parentType for all gallery uploads.


Configuration

Media\Config reads the store configuration for media-specific settings:

Config method Config key Description
isRecyclingEnabled(): bool media/media_entry/enable_recycling Whether deleted media entries are moved to a recycle bin instead of hard-deleted.
isConvertImages(): bool media/media_entry/convert_images Whether images are automatically converted to the target format on upload.
getConvertedImageFormat(): ?string media/media_entry/converted_image_format Target format string (e.g. 'webp'). null when not set or blank.
getMediaEntryResizeBreakpoints(?int\|string $scope, ?int\|string $scopeCode): int[] media/media_entry/resize_breakpoints Comma-separated list of pixel widths to pre-generate on upload. Returned as an array of integers. Accepts an optional scope for scoped reads.

Environment variables

Storage and URL

Variable Example Description
AMA_MERCHANT_BASE_MEDIA_URL https://cdn.amarant-commerce.com/ Public base URL prepended to all media paths when generating public URLs. Consumed by MerchantContext::getMerchantBaseMediaUrl(). Must end with /.
AMA_MERCHANT_BASE_MEDIA_STORAGE_PATH /mnt/amarantmedia Absolute filesystem path where media files are stored. Consumed by MerchantContext::getMerchantBaseMediaStoragePath(). This is the in-container path in Docker deployments.
AMA_MERCHANT_BASE_MEDIA_STORAGE_MOUNT_PATH /Users/you/Sites/amarantmedia Host-side path mounted into the container at AMA_MERCHANT_BASE_MEDIA_STORAGE_PATH. Docker / docker-compose.yml only — not read by PHP.
AMA_MERCHANT_BASE_MEDIA_HOST merchant-media.amarant2.localhost Hostname for the media file server (Traefik router rule). Docker only.

Image processing

Variable Default Description
AMA_IMAGE_SERVICE_VIPS_ENABLED 0 Set to 1 in the CLI environment to use VipsImageService instead of GdImageService. Requires the php-vips extension (FFI). Only takes effect in the CLI area.
AMA_IMAGE_SERVICE_QUALITY 80 Default WebP quality (1–100) used by DefaultImageProvider when no quality is passed to get().

Messaging

Variable Description
MESSAGING_TRANSPORT_MEDIA_ENTRY Transport identifier for the media entry processing queue (e.g. messaging.transport.media.entry). Used by the framework messaging configuration to route media processing messages.

Config overrides

Store configuration values for the media section can be overridden via environment variables using the pattern AMA_CONFIG_MEDIA_<PATH> where / and . in the config path are replaced with _ and the path is uppercased.

Variable Config key
AMA_CONFIG_MEDIA_MEDIA_ENTRY_CONVERT_IMAGES media/media_entry/convert_images
AMA_CONFIG_MEDIA_MEDIA_ENTRY_ENABLE_RECYCLING media/media_entry/enable_recycling
AMA_CONFIG_MEDIA_MEDIA_ENTRY_RESIZE_BREAKPOINTS media/media_entry/resize_breakpoints