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:
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:
- The
$newQualityargument toget(). - The
AMA_IMAGE_SERVICE_QUALITYenvironment variable. 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:
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 |