Uploadable
#[Silverback\Uploadable] with #[Silverback\UploadableField] turns a component into a file host. Files are stored via Flysystem, served via a public URL, and optionally processed through LiipImagineBundle to generate multiple image variants (thumbnail, hero, square, etc.).
Flysystem Setup
First, install a Flysystem adapter:
# Local filesystem
composer require league/flysystem-local
# Google Cloud Storage
composer require league/flysystem-google-cloud-storage
# S3-compatible
composer require league/flysystem-aws-s3-v3
Register the adapter as a service with the CWA tag:
# config/services.yaml
services:
League\Flysystem\Local\LocalFilesystemAdapter:
arguments:
- '%kernel.project_dir%/var/storage/default'
tags:
- { name: silverback.api_components.filesystem_provider, alias: 'local' }
The alias becomes the adapter name you use in #[UploadableField].
Add to Your Entity
use Silverback\ApiComponentsBundle\Annotation as Silverback;
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
use Silverback\ApiComponentsBundle\Entity\Utility\UploadableTrait;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
#[Silverback\Publishable]
#[Silverback\Uploadable]
#[ORM\Entity]
#[ApiResource(mercure: true)]
class Image extends AbstractComponent
{
use PublishableTrait;
use UploadableTrait;
#[Silverback\UploadableField(adapter: 'local', urlGenerator: 'public', imagineFilters: ['thumbnail'])]
#[Assert\File(maxSize: '5M')]
public ?File $file = null;
}
UploadableField Parameters
| Parameter | Default | Description |
|---|---|---|
adapter | — | Required. The Flysystem adapter alias |
urlGenerator | 'api' | How to generate the file URL — see below |
property | 'filename' | The property name on the media object that stores the filename |
prefix | null | Optional path prefix inside the storage (e.g. 'images/') |
imagineFilters | [] | LiipImagine filter names to apply at upload time |
urlGenerator values
'api'(default) — the file URL is served through a Symfony download route (/_/media_objects/{id}/download). Works with any adapter regardless of whether it supports direct URL generation.'public'— calls Flysystem'spublicUrl()method to return a direct CDN or storage URL. Requires the adapter to implement Flysystem'sPublicUrlGeneratorinterface (e.g.league/flysystem-aws-s3-v3,league/flysystem-google-cloud-storage). Falls back to'api'if the adapter does not support it.'temporary'— calls Flysystem'stemporaryUrl()method to return a pre-signed URL with an expiry (default:+3 days). Requires the adapter to implement Flysystem'sTemporaryUrlGeneratorinterface. Falls back to'api'if not supported. The primary use case is S3 pre-signed URLs for private/access-controlled files.
// S3 pre-signed URL — valid for 3 days, falls back to API route if adapter doesn't support it
#[Silverback\UploadableField(adapter: 's3', urlGenerator: 'temporary')]
public ?File $file = null;
// CDN public URL — direct S3/GCS link
#[Silverback\UploadableField(adapter: 'gcs', urlGenerator: 'public')]
public ?File $image = null;
Uploading a File
Option 1 — Base64 in the JSON body (preferred for REST clients):
POST /component/images
Content-Type: application/json
{
"file": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
}
Option 2 — Multipart form upload:
POST /component/images/{id}/upload
Content-Type: multipart/form-data
file=<binary>
The _metadata Response
After upload, the resource includes a _metadata.mediaObjects map:
{
"@id": "/component/images/018e-...",
"_metadata": {
"mediaObjects": {
"file": {
"contentUrl": "https://cdn.example.com/images/018e-....jpg",
"fileSize": 245120,
"mimeType": "image/jpeg",
"width": 1920,
"height": 1080,
"thumbnail": {
"contentUrl": "https://cdn.example.com/images/018e-...thumbnail.jpg",
"width": 300,
"height": 200
}
}
}
}
}
Each entry under the field name (file) contains the original upload plus any Imagine variants as nested objects.
LiipImagineBundle Integration
LiipImagine generates image variants automatically at upload time.
Install the bundle and configure it to use CWA's Flysystem data loader:
composer require liip/imagine-bundle
# config/packages/liip_imagine.yaml
liip_imagine:
data_loader: silverback.api_components.liip_imagine.binary.loader
filter_sets:
thumbnail:
quality: 80
filters:
thumbnail: { size: [300, 300], mode: outbound }
hero:
quality: 90
filters:
thumbnail: { size: [1200, 630], mode: outbound }
Configure the cache resolver to write back through Flysystem:
services:
app.imagine.cache.resolver.local:
class: Silverback\ApiComponentsBundle\Imagine\FlysystemCacheResolver
arguments:
$filesystem: '@api_components.filesystem.local'
$rootUrl: 'https://cdn.example.com'
tags:
- { name: 'liip_imagine.cache.resolver', resolver: local }
liip_imagine:
cache: local
The service api_components.filesystem.{alias} is created automatically for each registered adapter.
Dynamic Filter Selection
For cases where the required filters depend on runtime data (request context, entity state), implement ImagineFiltersInterface:
use Silverback\ApiComponentsBundle\Imagine\ImagineFiltersInterface;
use Symfony\Component\HttpFoundation\Request;
class Image extends AbstractComponent implements ImagineFiltersInterface
{
public function getImagineFilters(string $property, ?Request $request): array
{
return ['thumbnail', 'hero', 'square'];
}
}
$request is null when called during upload processing outside a web request (e.g. in a fixture).
Deleting a File
Send null for the field property in a PATCH:
PATCH /component/images/{id}
Content-Type: application/merge-patch+json
{ "file": null }
The bundle removes the file from storage and clears the media object data.
On the Front-End
Use useCwaImageResource in your Vue component instead of useCwaResource. It adds contentUrl, displayMedia, loaded, and handleLoad — see Images & Media for the full reference.