Recently I switched from a project from an usual hoster to Heroku. For the file storage we decided to use Cloudcube (because it includes a free tier we use for review apps). For the file abstraction layer we use flysystem with the flysystem bundle for Symfony integration.

Unfortunately the environment variables we get through Heroku aren't a perfect match for what we need in the flysystem configuration. We get CLOUDCUBE_ACCESS_KEY_ID, CLOUDCUBE_SECRET_ACCESS_KEY and CLOUDCUBE_URL. For the configuration we would also need the prefix which is already part of the Cloudcube url. As it's generated for every review app, we can't just put it in a custom environment variable. So we pull it out through an expression. For this we need the package symfony/expression-language installed through composer.

services:
  Aws\S3\S3Client:
    arguments:
      - version: '2006-03-01'
        region: 'eu-west-1'
        endpoint: '%env(CLOUDCUBE_URL)%'
        credentials:
          key: '%env(CLOUDCUBE_ACCESS_KEY_ID)%'
          secret: '%env(CLOUDCUBE_SECRET_ACCESS_KEY)%'

flysystem:
  storages:
    aws.storage:
      adapter: 'aws'
      options:
        client: 'Aws\S3\S3Client'
        bucket: '' # No bucket as it's already part of the url
        # Pull the prefix (last part after ..com/) out of an url like: https://cloud-cube-eu.s3.amazonaws.com/v14wxz3uwp2n
        # Locally we have set the url to `` which will also return `` as we don't need it locally
        prefix: "@=preg_match('([^\/]+$)', %env(CLOUDCUBE_URL)%)"

This is everything needed to use flysystem to store files on Amazon S3.

But for local development and testing we don't want to store our files on S3, so we use the lazy adapter to switch to a local adapter. This is the final flysystem.yaml:

services:
  Aws\S3\S3Client:
    arguments:
      - version: '2006-03-01'
        region: 'eu-west-1'
        endpoint: '%env(CLOUDCUBE_URL)%'
        credentials:
          key: '%env(CLOUDCUBE_ACCESS_KEY_ID)%'
          secret: '%env(CLOUDCUBE_SECRET_ACCESS_KEY)%'

flysystem:
  storages:
    aws.storage:
      adapter: 'aws'
      options:
        client: 'Aws\S3\S3Client'
        bucket: '' # No bucket as it's already part of the url
        # Pull the prefix (last part after ..com/) out of an url like: https://cloud-cube-eu.s3.amazonaws.com/v14wxz3uwp2n
        # Locally we have set the url to `` which will also return `` as we don't need it locally
        prefix: "@=preg_match('([^\/]+$)', %env(CLOUDCUBE_URL)%)"

    local.storage:
      adapter: 'local'
      options:
        directory: '%kernel.project_dir%/var/storage'

    default.storage:
      adapter: 'lazy'
      options:
        source: '%env(APP_STORAGE)%'

APP_STORAGE can now have two parameters: aws.storage or local.storage. Switching between them is as simple as changing the environment variable.

In a Symfony service you can use dependency injection to inject the configured storage by using FilesystemInterface $defaultStorage like this:

private FilesystemInterface $filesystem;

public function __construct(FilesystemInterface $defaultStorage) 
{
    $this->filesystem = $defaultStorage;
}

This technique is taken from the flysystem bundle documentation.