Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ to interact with your storage.
8. [BunnyCDN](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/8-bunnycdn.md)

* [Security issue disclosure procedure](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/A-security-disclosure-procedure.md)
* [Configuration reference](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/B-configuration-reference.md)

## Security Issues

Expand Down
19 changes: 7 additions & 12 deletions docs/1-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

## Installation

flysystem-bundle requires PHP 7.1+ and Symfony 4.2+.
flysystem-bundle requires PHP 8.0+ and Symfony 5.4+.

You can install the bundle using Symfony Flex:

Expand All @@ -26,8 +26,7 @@ use Flysystem in your application as soon as you install the bundle:
flysystem:
storages:
default.storage:
adapter: 'local'
options:
local:
directory: '%kernel.project_dir%/var/storage/default'
```

Expand Down Expand Up @@ -100,13 +99,11 @@ autowired arguments. For example:
flysystem:
storages:
users.storage:
adapter: 'local'
options:
local:
directory: '%kernel.project_dir%/storage/users'

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

Expand Down Expand Up @@ -151,8 +148,7 @@ Then, you can overwrite your storages in the test environment:
flysystem:
storages:
users.storage:
adapter: 'local'
options:
local:
directory: '%kernel.project_dir%/storage/users'
```

Expand All @@ -162,7 +158,7 @@ flysystem:
flysystem:
storages:
users.storage:
adapter: 'memory'
memory: ~
```

This configuration will swap every reference to the `users.storage` service (or to the
Expand All @@ -186,8 +182,7 @@ And then, you can configure your storage with the `readonly` options.
flysystem:
storages:
users.storage:
adapter: 'local'
options:
local:
directory: '%kernel.project_dir%/storage/users'
readonly: true
```
Expand Down
21 changes: 7 additions & 14 deletions docs/2-cloud-storage-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ composer require league/flysystem-azure-blob-storage
flysystem:
storages:
users.storage:
adapter: 'azure'
options:
azure:
client: 'azure_client_service' # The service ID of the MicrosoftAzure\Storage\Blob\BlobRestProxy instance
container: 'container_name'
prefix: 'optional/path/prefix'
Expand All @@ -51,8 +50,7 @@ composer require league/flysystem-async-aws-s3
flysystem:
storages:
users.storage:
adapter: 'asyncaws'
options:
asyncaws:
client: 'aws_client_service' # The service ID of the AsyncAws\S3\S3Client instance
bucket: 'bucket_name'
prefix: 'optional/path/prefix'
Expand All @@ -74,9 +72,8 @@ composer require league/flysystem-aws-s3-v3
flysystem:
storages:
users.storage:
adapter: 'aws'
# visibility: public # Make the uploaded file publicly accessible in S3
options:
aws:
client: 'aws_client_service' # The service ID of the Aws\S3\S3Client instance
bucket: 'bucket_name'
prefix: 'optional/path/prefix'
Expand All @@ -99,8 +96,7 @@ composer require league/flysystem-google-cloud-storage
flysystem:
storages:
users.storage:
adapter: 'gcloud'
options:
gcloud:
client: 'gcloud_client_service' # The service ID of the Google\Cloud\Storage\StorageClient instance
bucket: 'bucket_name'
prefix: 'optional/path/prefix'
Expand All @@ -126,8 +122,7 @@ services:
flysystem:
storages:
cdn.storage:
adapter: 'asyncaws'
options:
asyncaws:
client: 'digitalocean_spaces_client'
bucket: '%env(DIGITALOCEAN_SPACES_BUCKET)%'
```
Expand All @@ -152,8 +147,7 @@ services:
flysystem:
storages:
cdn.storage:
adapter: 'asyncaws'
options:
asyncaws:
client: 'scaleway_spaces_client'
bucket: '%env(SCALEWAY_SPACES_BUCKET)%'
```
Expand All @@ -178,8 +172,7 @@ services:
flysystem:
storages:
cdn.storage:
adapter: 'asyncaws'
options:
asyncaws:
client: 'cloudflare_r2_client'
bucket: '%env(CLOUDFLARE_R2_BUCKET)%'
```
Expand Down
6 changes: 2 additions & 4 deletions docs/3-interacting-with-ftp-and-sftp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ composer require league/flysystem-ftp
flysystem:
storages:
backup.storage:
adapter: 'ftp'
options:
ftp:
host: 'ftp.example.com'
username: 'username'
password: 'password'
Expand Down Expand Up @@ -50,8 +49,7 @@ composer require league/flysystem-sftp-v3
flysystem:
storages:
backup.storage:
adapter: 'sftp'
options:
sftp:
host: 'example.com'
port: 22
username: 'username'
Expand Down
11 changes: 4 additions & 7 deletions docs/4-using-lazy-adapter-to-switch-at-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,20 @@ services:
flysystem:
storages:
uploads.storage.aws:
adapter: 'aws'
options:
aws:
client: 'Aws\S3\S3Client'
bucket: 'my-bucket'
prefix: '%env(S3_STORAGE_PREFIX)%'

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

uploads.storage.memory:
adapter: 'memory'
memory: ~

uploads.storage:
adapter: 'lazy'
options:
lazy:
source: '%env(APP_UPLOADS_SOURCE)%'
```

Expand Down
200 changes: 198 additions & 2 deletions docs/5-creating-a-custom-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,201 @@ storages:
flysystem:
storages:
users.storage:
adapter: 'App\Flysystem\MyCustomAdapter'
```
service: 'App\Flysystem\MyCustomAdapter'
```

## Creating a custom adapter builder (advanced)

For more complex custom adapters that require configuration validation, IDE auto-completion,
and integration with the bundle's configuration system, you can create a custom adapter builder.

This allows you to define your custom adapter directly in the configuration:

```yaml
# config/packages/flysystem.yaml

flysystem:
storages:
users.storage:
my_custom: # Your custom adapter type
option1: 'value1'
option2: 'value2'
```

### Creating the adapter builder

Create a class implementing `AdapterDefinitionBuilderInterface`:

```php
<?php

namespace App\Flysystem\Builder;

use App\Flysystem\MyCustomAdapter;
use League\FlysystemBundle\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 required packages for your adapter
// Format: ['ClassName' => 'vendor/package-name']
return [];
}
Comment on lines +78 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The required package is not only about the package name, but also the version. Does this syntax allow adding the version constraint after the package name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this syntax doesn't support version constraints. It could be interesting, but the use cases are very limited (if not non-existent).
Currently, this mechanism is very useful for official adapters because their packages are separate and have very specific dependencies for each use case.
For a public builder API, the adapter package should theoretically be self-contained. For example, with azure-oss/storage-blob-flysystem, there are two approaches: either Azure OSS provides a dedicated bundle to manage their adapter, or the adapter package includes the AdapterBuilder and users manually register it via the Kernel.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should receive a ContainerConfigurator. The PHP-DSL for service declaration is very convenient. It's the same as service.php config files.

Example of how it's used in the MicroKernelTrait: https://github.com/symfony/symfony/blob/e48850bdb8161e1aa1338432ea469932857b2df5/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php#L194

Copy link
Collaborator Author

@maxhelias maxhelias Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably very convenient, but we need to dynamically register adapter services based on configuration. Since the conditions aren't always known in advance, the ContainerBuilder gives us direct access to service registration methods, which seems more straightforward for this conditional, programmatic service creation.

{
$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;
}
}
```

### Registering the adapter builder

#### Via Bundle (recommended for reusable bundles)

If you're creating a bundle, register your builder in your bundle class:

```php
<?php

namespace App\MyCustomBundle;

use App\Flysystem\Builder\MyCustomAdapterDefinitionBuilder;
use League\FlysystemBundle\FlysystemBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class MyCustomBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
parent::build($container);

// Register your custom adapter builder
$extension = $container->getExtension('flysystem');
if ($extension instanceof FlysystemExtension) {
$extension->addAdapterDefinitionBuilder(new MyCustomAdapterDefinitionBuilder());
}
}
}
```

#### Via Kernel (for application-specific adapters)

For application-specific adapters, register your builder in your `Kernel`:

```php
<?php

namespace App;

use App\Flysystem\Builder\MyCustomAdapterDefinitionBuilder;
use League\FlysystemBundle\DependencyInjection\FlysystemExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
// ...

protected function build(ContainerBuilder $container): void
{
parent::build($container);

// Register your custom adapter builder
$extension = $container->getExtension('flysystem');
if ($extension instanceof FlysystemExtension) {
$extension->addAdapterDefinitionBuilder(new MyCustomAdapterDefinitionBuilder());
}
}
}
```

Once registered, you can use the `debug:config flysystem` command to see your custom adapter
and all its available options in the configuration tree.

### Testing your custom builder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this section. Tests are too often skipped because people don't know how to write them.


Create a test class extending `AbstractAdapterDefinitionBuilderTest`:

```php
<?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));
}
}
```

This provides comprehensive testing of your builder's configuration and adapter creation logic.
Loading