The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftAnnotations

Uploadable

Add file upload support to any component with Flysystem adapters and optional Imagine image processing.

#[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

ParameterDefaultDescription
adapterRequired. 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
prefixnullOptional 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's publicUrl() method to return a direct CDN or storage URL. Requires the adapter to implement Flysystem's PublicUrlGenerator interface (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's temporaryUrl() method to return a pre-signed URL with an expiry (default: +3 days). Requires the adapter to implement Flysystem's TemporaryUrlGenerator interface. 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.