On April 6th and 7th 2019, I was honored to take part in one of the most interesting open-source experience of my life: the EU FOSSA Symfony Hackathon 2019. During two days, 54 of the most active members of the Symfony community gathered in Brussels with a common goal: designing and shaping the future of Symfony, by talking and coding together.

This incredible event was an initiative from the European Commission aimed at supporting the open-source communities they rely on. They did an amazing job, many thanks to them all!

This hackathon was the occasion for me to work on an idea I had in mind since February 2018: implementing a modern Flysystem integration for Symfony.

Autowiring and interfaces implementations

In February 2018, as I was starting yet another personal side project, I needed to store files uploaded by users in a cloud storage provider to avoid size limits and data corruption.

As I discussed in my article about how to build a reliable test suite with Symfony, I also wanted to be able to swap the uploaded files storage from local to memory during tests in order to increase their resilience and performance.

For these reasons, as I almost always do in my new Symfony projects, I used Flysystem.

Flysystem is a PHP library developed by Frank de Jonge that provides an abstraction for the filesystem based on several adapters. It helps manipulating in a same manner files stored on various storages, including local, FTP, AWS S3, Google Cloud Storage, ...

However, as soon as I started using the library, I stumbled upon the same problem I always encountered with it: I wanted to create multiple filesystem services (one for the users storage, one for the projects storage, ...) and I wanted to leverage autowiring to get my services injected without any service configuration. This was not possible at the time.

This problem and following discussions with Nicolas Grekas lead to a wider analysis about how it was difficult to use autowiring with multiple implementations of the same interface. In my case, I couldn't use only the FilesystemInterface with autowiring, as multiple services implemented it:

class RegistrationManager
{
    // Autowiring will always inject the service precisely named 
    // "League\Flysystem\FilesystemInterface", even if you have
    // multiple services implementing the interface
    public function __construct(FilesystemInterface $storage)
    {
        // ...
    }
}

This discussion took several months between February and August and was the occasion of a lot of various ideas, until Nicolas implemented an elegant fix in Symfony 4.2: named autowiring aliases.

Using named autowiring aliases to resolve which implementation to use

Named autowiring aliases are a way for the autowiring pass to know what service implementation you want based not only on the interface name but also on the variable name of your parameter. This feature helps you express a more specific semantic about the service you need in your class ("I want a filesystem, to store users files"). Don't hesitate to read the official documentation about this feature to learn more.

Named autowiring aliases are a perfect fit to solve my problem with Flysystem: by creating properly named aliases to match the aim of my storages, I can leverage autowiring and have multiple filesystems in my application.

This is why today I'm releasing the league/flysystem-bundle, integrating Flysystem into Symfony 4.2+.

This bundle helps you use the library in Symfony with a few lines of configuration and by relying on named aliases for the actual injection in your services. I recommend you to use it with multiple filesystems: by naming your storages using their intents, you will naturally increase the readability of the code using your storages.

A simple configuration corresponding to my initial needs could be the following:

# config/packages/flysystem.yaml

flysystem:
    storages:
        users.storage:
            adapter: 'local'
            options:
                directory: '%kernel.project_dir%/storage/users'
                
        projects.storage:
            adapter: 'local'
            options:
                directory: '%kernel.project_dir%/storage/projects'

This configuration defines two storages and their associated named aliases automatically, allowing you to use autowiring for injection:

use League\Flysystem\FilesystemInterface;

class MyService
{
    private $usersStorage;
    private $projectsStorage;
    
    // The variable names $usersStorage and $projectsStorage
    // match the filesystems definitions, allowing the autowiring
    // to know which service to inject
    public function __construct(FilesystemInterface $usersStorage, FilesystemInterface $projectsStorage)
    {
        $this->usersStorage = $usersStorage;
        $this->projectsStorage = $projectsStorage;
    }
    
    // ...
}

I am really happy to finally have achieved such a great integration. Many thanks to Frank who always was helping and motivated to do this! Because of him, this bundle is now the official Flysystem Symfony bundle: https://github.com/thephpleague/flysystem-bundle.

Do you want to try it? With Symfony Flex, it's one command-line away:

composer require league/flysystem-bundle
Package operations: 3 installs, 0 updates, 0 removals
  - Installing symfony/options-resolver (v4.2.7): Loading from cache
  - Installing league/flysystem (1.0.51): Loading from cache
  - Installing league/flysystem-bundle (1.0.0): Downloading (100%)         
Writing lock file
Generating autoload files
Symfony operations: 1 recipe (d6fb49b705673a6a06f8351206ac6192)
  - Configuring league/flysystem-bundle (>=1.0)

Some files may have been created or updated to configure your new packages.
Please review, edit and commit them: these files are yours.

                                             
 You just installed league/flysystem-bundle! 
 What's next?                                
                                             

  * Use the default storage immediately:

      public function __construct(FilesystemInterface $defaultStorage)
      {
          ...
      }

  * Configure your own storages in config/packages/flysystem.yaml.

  * Read the documentation at:
    https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md