flysystem-bundle

repository·3.x·Indexed 19 days ago

https://github.com/thephpleague/flysystem-bundle

A Symfony bundle that integrates the Flysystem library to provide a filesystem abstraction layer. It enables developers to swap storage backends such as local, S3, Azure, Google Cloud Storage, FTP, and SFTP based on the environment without changing application code. It supports PHP 8.0+ and Symfony 5.4+, offering features like service autowiring via the FilesystemOperator interface, read-only storage, in-memory storage for testing, and CLI commands for pushing and pulling files.

Tokens
11K
Snippets
36
Records
40
Agent score
62%

What's inside flysystem-bundle

  1. Switch storage backends at runtime using a lazy adapter

    3.x

    While the standard way to switch storage backends is to use environment-specific configuration files (e.g., config/packages/dev/flysystem.yaml), you can use a lazy adapter to choose a storage backend dynamically at runtime.

    A lazy adapter acts as a proxy that delays the instantiation of the actual storage until it is needed. This is particularly useful when you need to switch backends based on a single environment variable without creating multiple service definitions for different environments.

    To implement this, define your actual storage backends as separate services, then define a main storage service using the lazy key and point its source to the desired backend via an environment variable.

    # config/packages/flysystem.yaml
    
    flysystem:
        storages:
            uploads.storage.aws:
                aws:
                    client: 'Aws\S3\S3Client'
                    bucket: 'my-bucket'
                    prefix: '%env(S3_STORAGE_PREFIX)%'
    
            uploads.storage.local:
                local:
                    directory: '%kernel.project_dir%/var/storage/uploads'
    
            uploads.storage.memory:
                memory: ~
    
            uploads.storage:
                lazy:
                    source: '%env(APP_UPLOADS_SOURCE)%'
  2. Create a custom adapter builder

    3.x

    For complex adapters that require configuration validation, IDE auto-completion, and integration with the bundle's configuration system, implement the AdapterDefinitionBuilderInterface.

    This allows you to define a custom adapter type (e.g., my_custom:) directly in your YAML configuration.

    Key methods to implement:

    • getName(): Returns the string used in the YAML config (e.g., my_custom).
    • getRequiredPackages(): Returns an array of ['ClassName' => 'vendor/package-name'] if the adapter depends on external packages.
    • addConfiguration(NodeDefinition $node): Uses Symfony Config components to define the schema, required options, default values, and descriptions for your adapter's configuration.
    • createAdapter(...): Responsible for creating the service definition for the adapter and adding it to the ContainerBuilder. It should return the service ID (typically flysystem.adapter.{storageName}).
    <?php
    
    namespace App\Flysystem\Builder;
    
    use App\Flysystem\MyCustomAdapter;
    use Leaguelysystem-bundle\Adapter\Builder\AdapterDefinitionBuilderInterface;
    use Symfony\Component\Config\Definition\Builder\NodeDefinition;
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\DependencyInjection\Definition;
    
    class MyCustomAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface
    {
        public function getName(): string
        {
            return 'my_custom';
        }
    
        public function getRequiredPackages(): array
        {
            return [];
        }
    
        public function addConfiguration(NodeDefinition $node): void
        {
            $node
                ->children()
                    ->scalarNode('option1')
                        ->isRequired()
                        ->info('Description of option1')
                    ->end()
                    ->scalarNode('option2')
                        ->defaultValue('default_value')
                        ->info('Description of option2')
                    ->end()
                    ->booleanNode('option3')
                        ->defaultFalse()
                        ->info('Description of option3')
                    ->end()
                ->end();
        }
    
        public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): string
        {
            $adapterId = 'flysystem.adapter.' . $storageName;
    
            $definition = new Definition(MyCustomAdapter::class);
            $definition->setPublic(false);
            
            // Configure your adapter with the options
            $definition->setArgument(0, $options['option1']);
            $definition->setArgument(1, $options['option2']);
            $definition->setArgument(2, $options['option3']);
    
            $container->setDefinition($adapterId, $definition);
    
            return $adapterId;
        }
    }
  3. Test a custom adapter builder

    3.x

    To ensure your builder correctly validates configuration and creates the adapter service, extend AbstractAdapterDefinitionBuilderTest.

    Implement:

    • createBuilder(): Returns an instance of your builder.
    • provideValidOptions(): A generator yielding arrays of valid configuration options.
    • assertDefinition(Definition $definition): Asserts that the generated service definition has the correct class and arguments.
    <?php
    
    namespace Tests\App\Flysystem\Builder;
    
    use App\Flysystem\Builder\MyCustomAdapterDefinitionBuilder;
    use App\Flysystem\MyCustomAdapter;
    use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest;
    use Symfony\Component\DependencyInjection\Definition;
    
    class MyCustomAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest
    {
        protected function createBuilder(): MyCustomAdapterDefinitionBuilder
        {
            return new MyCustomAdapterDefinitionBuilder();
        }
    
        public static function provideValidOptions(): \Generator
        {
            yield 'minimal' => [[
                'option1' => 'value1',
            ]];
    
            yield 'full' => [[
                'option1' => 'value1',
                'option2' => 'custom_value',
                'option3' => true,
            ]];
        }
    
        protected function assertDefinition(Definition $definition): void
        {
            $this->assertSame(MyCustomAdapter::class, $definition->getClass());
            $this->assertSame('value1', $definition->getArgument(0));
            $this->assertSame('custom_value', $definition->getArgument(1));
            $this->assertTrue($definition->getArgument(2));
        }
    }
  4. Inject Flysystem storage using manual service registration

    3.x

    If you are not using autowiring, you can manually inject the storage service in config/services.yaml. The bundle creates a service for each storage following the pattern: flysystem.adapter.{storageName}.

    Example for a storage named default.storage:

    # config/services.yaml
    services:
        App\MyService:
            arguments:
                $storage: @flysystem.adapter.default.storage
  5. Upgrade from flysystem-bundle 2.0 to 3.0

    3.x

    Upgrading to version 3.0 involves environment updates rather than direct breaking changes in the bundle itself. The bundle has dropped support for older versions of PHP, Symfony, and Flysystem.

    Key Changes:

    • PHP: Dropped support for PHP 7.x.
    • Symfony: Dropped support for Symfony versions 4.2 through 5.3.
    • Flysystem: Dropped support for Flysystem 2.x (requires Flysystem 3.x).
    • New Support: Added support for Azure Blob Storage via league/flysystem-azure-blob-storage ^3.1.

    Because the bundle relies on Flysystem, you must check the Flysystem 3.x changelog for indirect breaking changes that may affect your filesystem operations.

  6. Configure read-only storage

    3.x

    To prevent write operations on a specific storage, use the read_only option. This will cause any write operation to throw an exception.

    1. Install the read-only package: composer require league/flysystem-read-only.
    2. Set read_only: true in your storage configuration.
    composer require league/flysystem-read-only
    # config/packages/flysystem.yaml
    flysystem:
        storages:
            users.storage:
                local:
                    directory: '%kernel.project_dir%/storage/users'
                read_only: true
  7. Register a custom adapter builder

    3.x

    To make your custom adapter builder available to the Flysystem bundle, you must register it with the FlysystemExtension.

    For Reusable Bundles

    Register the builder within your bundle's build() method:

    public function build(ContainerBuilder $container): void
    {
        parent::build($container);
    
        $extension = $container->getExtension('flysystem');
        if ($extension instanceof FlysystemExtension) {
            $extension->addAdapterDefinitionBuilder(new MyCustomAdapterDefinitionBuilder());
        }
    }

    For Application-Specific Adapters

    Register the builder within your application's Kernel class:

    protected function build(ContainerBuilder $container): void
    {
        parent::build($container);
    
        $extension = $container->getExtension('flysystem');
        if ($extension instanceof FlysystemExtension) {
            $extension->addAdapterDefinitionBuilder(new MyCustomAdapterDefinitionBuilder());
        }
    }

    After registration, you can verify your configuration using the debug:config flysystem command.

    // Example registration in Kernel
    protected function build(ContainerBuilder $container): void
    {
        parent::build($container);
    
        $extension = $container->getExtension('flysystem');
        if ($extension instanceof FlysystemExtension) {
            $extension->addAdapterDefinitionBuilder(new MyCustomAdapterDefinitionBuilder());
        }
    }
  8. Configure WebDAV storage in Flysystem

    3.x

    To set up WebDAV, you need to define a WebDAV client service (typically using Sabre\DAV\Client) and then register it under the flysystem.storages configuration.

    Key configuration options for the webdav adapter:

    • client: The service ID of your Sabre\DAV\Client instance.
    • prefix: An optional path prefix to scope all operations.
    • visibility_handling: Determines how visibility errors are handled. You can use \League\Flysystem\WebDAV\WebDAVAdapter::ON_VISIBILITY_THROW_ERROR to throw an error when visibility cannot be set.
    • manual_copy: Boolean to enable/disable manual copy operations.
    • manual_move: Boolean to enable/disable manual move operations.
    # config/services.yaml
    services:
      webdav_client:
        class: Sabre\DAV\Client
        arguments:
          - { baseUri: 'https://webdav.example.com/', userName: 'your_user', password: 'superSecret1234' }
    
    # config/packages/flysystem.yaml
    flysystem:
        storages:
            webdav.storage:
                webdav:
                    client: 'webdav_client'
                    prefix: 'optional/path/prefix'
                    visibility_handling: !php/const \League\Flysystem\WebDAV\WebDAVAdapter::ON_VISIBILITY_THROW_ERROR
                    manual_copy: false
                    manual_move: false
  9. Use a custom Flysystem adapter via service reference

    3.x

    If you have a custom Flysystem adapter, you can use it in your storage configuration by referencing it as a service.

    1. Create a class that implements League\Flysystem\FilesystemAdapter.
    2. Ensure the class is registered as a service in your Symfony container (autodiscovery handles this by default in most Symfony 4.2+ applications).
    3. Reference the service ID or class name in your flysystem.yaml configuration under the service key.
    # config/packages/flysystem.yaml
    
    flysystem:
        storages:
            users.storage:
                service: 'App\Flysystem\MyCustomAdapter'
  10. Configure BunnyCDN Storage

    3.x

    To configure BunnyCDN, you need to define a service for the PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNClient and then register it under the flysystem.storages configuration.

    1. Define the Client Service: Create a service in config/services.yaml (or your service configuration) that passes the storage_zone_name, api_key, and region to the BunnyCDNClient constructor. The region should use the PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNRegion constants.
    2. Register the Storage: In config/packages/flysystem.yaml, add a new storage entry using the bunnycdn adapter type, referencing your client service. You can optionally provide a pull_zone URL.
    # config/services.yaml
    services:
      bunny_client:
        class: PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNClient
        arguments:
          $storage_zone_name: 'storage-zone'
          $api_key: 'api-key'
          $region: '!php/const:PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNRegion::FALKENSTEIN'
    
    # config/packages/flysystem.yaml
    flysystem:
        storages:
            bunny.storage:
                bunnycdn:
                    client: 'bunny_client'
                    pull_zone: 'https://testing.b-cdn.net/' # optional