From 34c25a8edb75090114ee928e3a47f5ab8d22e290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20H=C3=A9lias?= Date: Fri, 13 Jun 2025 21:12:18 +0200 Subject: [PATCH 1/5] feat: extract unix persmission in a trait --- .../AbstractAdapterDefinitionBuilder.php | 60 ------------------ .../Builder/FtpAdapterDefinitionBuilder.php | 2 + .../Builder/LocalAdapterDefinitionBuilder.php | 2 + .../Builder/SftpAdapterDefinitionBuilder.php | 2 + src/Adapter/Builder/UnixPermissionTrait.php | 62 +++++++++++++++++++ 5 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 src/Adapter/Builder/UnixPermissionTrait.php diff --git a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php index 3000851..eeeba6b 100644 --- a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php @@ -11,8 +11,6 @@ namespace League\FlysystemBundle\Adapter\Builder; -use League\Flysystem\UnixVisibility\PortableVisibilityConverter; -use League\FlysystemBundle\Exception\MissingPackageException; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -42,62 +40,4 @@ abstract protected function getRequiredPackages(): array; abstract protected function configureOptions(OptionsResolver $resolver); abstract protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories); - - protected function configureUnixOptions(OptionsResolver $resolver): void - { - $method = method_exists($resolver, 'setOptions') ? 'setOptions' : 'setDefault'; - - $resolver->$method('permissions', function (OptionsResolver $subResolver) use ($method) { - $subResolver->$method('file', function (OptionsResolver $permsResolver) { - $permsResolver->setDefault('public', 0644); - $permsResolver->setAllowedTypes('public', 'scalar'); - - $permsResolver->setDefault('private', 0600); - $permsResolver->setAllowedTypes('private', 'scalar'); - }); - - $subResolver->$method('dir', function (OptionsResolver $permsResolver) { - $permsResolver->setDefault('public', 0755); - $permsResolver->setAllowedTypes('public', 'scalar'); - - $permsResolver->setDefault('private', 0700); - $permsResolver->setAllowedTypes('private', 'scalar'); - }); - }); - } - - protected function createUnixDefinition(array $permissions, string $defaultVisibilityForDirectories): Definition - { - return (new Definition(PortableVisibilityConverter::class)) - ->setFactory([PortableVisibilityConverter::class, 'fromArray']) - ->addArgument([ - 'file' => [ - 'public' => (int) $permissions['file']['public'], - 'private' => (int) $permissions['file']['private'], - ], - 'dir' => [ - 'public' => (int) $permissions['dir']['public'], - 'private' => (int) $permissions['dir']['private'], - ], - ]) - ->addArgument($defaultVisibilityForDirectories) - ->setShared(false) - ; - } - - private function ensureRequiredPackagesAvailable(): void - { - $missingPackages = []; - foreach ($this->getRequiredPackages() as $requiredClass => $packageName) { - if (!class_exists($requiredClass)) { - $missingPackages[] = $packageName; - } - } - - if (!$missingPackages) { - return; - } - - throw new MissingPackageException(sprintf("Missing package%s, to use the \"%s\" adapter, run:\n\ncomposer require %s", \count($missingPackages) > 1 ? 's' : '', $this->getName(), implode(' ', $missingPackages))); - } } diff --git a/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php b/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php index b9a7817..f63a055 100644 --- a/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php @@ -25,6 +25,8 @@ */ final class FtpAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder { + use UnixPermissionTrait; + public function getName(): string { return 'ftp'; diff --git a/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php b/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php index 7b66645..3ffe9e7 100644 --- a/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php @@ -23,6 +23,8 @@ */ final class LocalAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder { + use UnixPermissionTrait; + public function getName(): string { return 'local'; diff --git a/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php b/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php index 1d44339..c231caa 100644 --- a/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php @@ -27,6 +27,8 @@ */ final class SftpAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder { + use UnixPermissionTrait; + public function getName(): string { return 'sftp'; diff --git a/src/Adapter/Builder/UnixPermissionTrait.php b/src/Adapter/Builder/UnixPermissionTrait.php new file mode 100644 index 0000000..fdb7f12 --- /dev/null +++ b/src/Adapter/Builder/UnixPermissionTrait.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace League\FlysystemBundle\Adapter\Builder; + +use League\Flysystem\UnixVisibility\PortableVisibilityConverter; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Maxime Hélias + */ +trait UnixPermissionTrait +{ + protected function configureUnixOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('permissions', function (OptionsResolver $subResolver) { + $subResolver->setDefault('file', function (OptionsResolver $permsResolver) { + $permsResolver->setDefault('public', 0644); + $permsResolver->setAllowedTypes('public', 'scalar'); + + $permsResolver->setDefault('private', 0600); + $permsResolver->setAllowedTypes('private', 'scalar'); + }); + + $subResolver->setDefault('dir', function (OptionsResolver $permsResolver) { + $permsResolver->setDefault('public', 0755); + $permsResolver->setAllowedTypes('public', 'scalar'); + + $permsResolver->setDefault('private', 0700); + $permsResolver->setAllowedTypes('private', 'scalar'); + }); + }); + } + + protected function createUnixDefinition(array $permissions, string $defaultVisibilityForDirectories): Definition + { + return (new Definition(PortableVisibilityConverter::class)) + ->setFactory([PortableVisibilityConverter::class, 'fromArray']) + ->addArgument([ + 'file' => [ + 'public' => (int) $permissions['file']['public'], + 'private' => (int) $permissions['file']['private'], + ], + 'dir' => [ + 'public' => (int) $permissions['dir']['public'], + 'private' => (int) $permissions['dir']['private'], + ], + ]) + ->addArgument($defaultVisibilityForDirectories) + ->setShared(false) + ; + } +} From e583a78eae3ec5fd0d846a83b7687e1fc45597a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20H=C3=A9lias?= Date: Fri, 13 Jun 2025 21:14:25 +0200 Subject: [PATCH 2/5] feat: refacto required packages by a builder --- src/Adapter/AdapterDefinitionFactory.php | 25 +++++++++++++++++-- .../AbstractAdapterDefinitionBuilder.php | 4 +-- .../AdapterDefinitionBuilderInterface.php | 2 ++ .../AsyncAwsAdapterDefinitionBuilder.php | 2 +- .../Builder/AwsAdapterDefinitionBuilder.php | 2 +- .../Builder/AzureAdapterDefinitionBuilder.php | 2 +- .../BunnyCDNAdapterDefinitionBuilder.php | 2 +- .../Builder/FtpAdapterDefinitionBuilder.php | 2 +- .../GcloudAdapterDefinitionBuilder.php | 2 +- .../GridFSAdapterDefinitionBuilder.php | 2 +- .../Builder/LocalAdapterDefinitionBuilder.php | 2 +- .../MemoryAdapterDefinitionBuilder.php | 2 +- .../Builder/SftpAdapterDefinitionBuilder.php | 2 +- .../WebDAVAdapterDefinitionBuilder.php | 2 +- 14 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/Adapter/AdapterDefinitionFactory.php b/src/Adapter/AdapterDefinitionFactory.php index fc4e6df..79bd03d 100644 --- a/src/Adapter/AdapterDefinitionFactory.php +++ b/src/Adapter/AdapterDefinitionFactory.php @@ -12,6 +12,7 @@ namespace League\FlysystemBundle\Adapter; use League\FlysystemBundle\Adapter\Builder\AdapterDefinitionBuilderInterface; +use League\FlysystemBundle\Exception\MissingPackageException; use Symfony\Component\DependencyInjection\Definition; /** @@ -46,11 +47,31 @@ public function __construct() public function createDefinition(string $name, array $options, ?string $defaultVisibilityForDirectories = null): ?Definition { foreach ($this->builders as $builder) { - if ($builder->getName() === $name) { - return $builder->createDefinition($options, $defaultVisibilityForDirectories); + if ($builder->getName() !== $name) { + continue; } + + $this->ensureRequiredPackagesBuilderAvailable($builder); + + return $builder->createDefinition($options, $defaultVisibilityForDirectories); } return null; } + + private function ensureRequiredPackagesBuilderAvailable(AdapterDefinitionBuilderInterface $builder): void + { + $missingPackages = []; + foreach ($builder->getRequiredPackages() as $requiredClass => $packageName) { + if (!class_exists($requiredClass)) { + $missingPackages[] = $packageName; + } + } + + if (!$missingPackages) { + return; + } + + throw new MissingPackageException(sprintf("Missing package%s, to use the \"%s\" adapter, run:\n\ncomposer require %s", \count($missingPackages) > 1 ? 's' : '', $this->getName(), implode(' ', $missingPackages))); + } } diff --git a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php index eeeba6b..5fed06f 100644 --- a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php @@ -23,8 +23,6 @@ abstract class AbstractAdapterDefinitionBuilder implements AdapterDefinitionBuil { final public function createDefinition(array $options, ?string $defaultVisibilityForDirectories): Definition { - $this->ensureRequiredPackagesAvailable(); - $resolver = new OptionsResolver(); $this->configureOptions($resolver); @@ -35,7 +33,7 @@ final public function createDefinition(array $options, ?string $defaultVisibilit return $definition; } - abstract protected function getRequiredPackages(): array; + abstract public function getRequiredPackages(): array; abstract protected function configureOptions(OptionsResolver $resolver); diff --git a/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php b/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php index 385e765..7bc2988 100644 --- a/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php +++ b/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php @@ -22,6 +22,8 @@ interface AdapterDefinitionBuilderInterface { public function getName(): string; + public function getRequiredPackages(): array; + /** * Create the definition for this builder's adapter given an array of options. */ diff --git a/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php b/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php index 84130ab..5d08997 100644 --- a/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php @@ -31,7 +31,7 @@ public function getName(): string return 'asyncaws'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ AsyncAwsS3Adapter::class => 'league/flysystem-async-aws-s3', diff --git a/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php b/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php index 8166f6d..30b6e1c 100644 --- a/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php @@ -30,7 +30,7 @@ public function getName(): string return 'aws'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ AwsS3V3Adapter::class => 'league/flysystem-aws-s3-v3', diff --git a/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php b/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php index 0f2d376..b47a4b3 100644 --- a/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php @@ -28,7 +28,7 @@ public function getName(): string return 'azure'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ AzureBlobStorageAdapter::class => 'league/flysystem-azure-blob-storage', diff --git a/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php b/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php index 96f6a96..0b1e0eb 100644 --- a/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php @@ -26,7 +26,7 @@ public function getName(): string return 'bunnycdn'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ BunnyCDNAdapter::class => 'platformcommunity/flysystem-bunnycdn', diff --git a/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php b/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php index f63a055..556d25d 100644 --- a/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php @@ -32,7 +32,7 @@ public function getName(): string return 'ftp'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ FtpAdapter::class => 'league/flysystem-ftp', diff --git a/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php b/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php index cc4fc01..e03e018 100644 --- a/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php @@ -28,7 +28,7 @@ public function getName(): string return 'gcloud'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ GoogleCloudStorageAdapter::class => 'league/flysystem-google-cloud-storage', diff --git a/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php b/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php index cf087ff..7d953de 100644 --- a/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php @@ -32,7 +32,7 @@ public function getName(): string return 'gridfs'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ GridFSAdapter::class => 'league/flysystem-gridfs', diff --git a/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php b/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php index 3ffe9e7..d8d1ea0 100644 --- a/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php @@ -30,7 +30,7 @@ public function getName(): string return 'local'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return []; } diff --git a/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php b/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php index fb31d7b..a69975f 100644 --- a/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php @@ -27,7 +27,7 @@ public function getName(): string return 'memory'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ InMemoryFilesystemAdapter::class => 'league/flysystem-memory', diff --git a/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php b/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php index c231caa..7cf34a8 100644 --- a/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php @@ -34,7 +34,7 @@ public function getName(): string return 'sftp'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { $adapterFqcn = SftpAdapter::class; $packageRequire = 'league/flysystem-sftp-v3'; diff --git a/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php b/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php index ec3fcd8..705d042 100644 --- a/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php @@ -28,7 +28,7 @@ public function getName(): string return 'webdav'; } - protected function getRequiredPackages(): array + public function getRequiredPackages(): array { return [ WebDAVAdapter::class => 'league/flysystem-webdav', From 2a6dd83cdac186ff9d90dff01078c3ce96fadae5 Mon Sep 17 00:00:00 2001 From: jonag Date: Sat, 12 Apr 2025 13:40:21 +0200 Subject: [PATCH 3/5] feat: Allow custom adapter registration This PR implements a solution to the issue discussed in #181, enabling external bundles to register their custom storage adapters with Flysystem Bundle without requiring them to be directly added to the main bundle's codebase. I had to remove the `@internal` annotation from `AdapterDefinitionBuilderInterface` and `AbstractAdapterDefinitionBuilder` to allow them to be referenced by other bundles. ## Usage Example External bundles can now register their adapters as follows: ``` php class AzureBlobStorageAdapterBundle extends Bundle { /** * {@inheritdoc} */ public function build(ContainerBuilder $container): void { parent::build($container); /** @var FlysystemExtension $extension */ $extension = $container->getExtension('flysystem'); $extension->addAdapterDefinitionBuilder(new AzureOssAdapterDefinitionBuilder()); } } ``` --- src/Adapter/AdapterDefinitionFactory.php | 9 ++++++--- .../Builder/AbstractAdapterDefinitionBuilder.php | 2 -- .../Builder/AdapterDefinitionBuilderInterface.php | 2 -- src/DependencyInjection/FlysystemExtension.php | 11 ++++++++++- tests/Adapter/AdapterDefinitionFactoryTest.php | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Adapter/AdapterDefinitionFactory.php b/src/Adapter/AdapterDefinitionFactory.php index 79bd03d..4f4af8b 100644 --- a/src/Adapter/AdapterDefinitionFactory.php +++ b/src/Adapter/AdapterDefinitionFactory.php @@ -27,9 +27,12 @@ final class AdapterDefinitionFactory */ private array $builders; - public function __construct() + /** + * @param list $builders + */ + public function __construct(array $builders) { - $this->builders = [ + $this->builders = array_merge([ new Builder\AsyncAwsAdapterDefinitionBuilder(), new Builder\AwsAdapterDefinitionBuilder(), new Builder\AzureAdapterDefinitionBuilder(), @@ -41,7 +44,7 @@ public function __construct() new Builder\SftpAdapterDefinitionBuilder(), new Builder\WebDAVAdapterDefinitionBuilder(), new Builder\BunnyCDNAdapterDefinitionBuilder(), - ]; + ], $builders); } public function createDefinition(string $name, array $options, ?string $defaultVisibilityForDirectories = null): ?Definition diff --git a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php index 5fed06f..4816475 100644 --- a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php @@ -16,8 +16,6 @@ /** * @author Titouan Galopin - * - * @internal */ abstract class AbstractAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { diff --git a/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php b/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php index 7bc2988..612a58e 100644 --- a/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php +++ b/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php @@ -15,8 +15,6 @@ /** * @author Titouan Galopin - * - * @internal */ interface AdapterDefinitionBuilderInterface { diff --git a/src/DependencyInjection/FlysystemExtension.php b/src/DependencyInjection/FlysystemExtension.php index 1b20a33..144d702 100644 --- a/src/DependencyInjection/FlysystemExtension.php +++ b/src/DependencyInjection/FlysystemExtension.php @@ -17,6 +17,7 @@ use League\Flysystem\FilesystemWriter; use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; use League\FlysystemBundle\Adapter\AdapterDefinitionFactory; +use League\FlysystemBundle\Adapter\Builder\AdapterDefinitionBuilderInterface; use League\FlysystemBundle\Exception\MissingPackageException; use League\FlysystemBundle\Lazy\LazyFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -30,6 +31,9 @@ */ final class FlysystemExtension extends Extension { + /** @var list */ + private array $adapterDefinitionBuilders = []; + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); @@ -43,9 +47,14 @@ public function load(array $configs, ContainerBuilder $container): void $this->createStoragesDefinitions($config, $container); } + public function addAdapterDefinitionBuilder(AdapterDefinitionBuilderInterface $builder): void + { + $this->adapterDefinitionBuilders[] = $builder; + } + private function createStoragesDefinitions(array $config, ContainerBuilder $container): void { - $definitionFactory = new AdapterDefinitionFactory(); + $definitionFactory = new AdapterDefinitionFactory($this->adapterDefinitionBuilders); foreach ($config['storages'] as $storageName => $storageConfig) { // If the storage is a lazy one, it's resolved at runtime diff --git a/tests/Adapter/AdapterDefinitionFactoryTest.php b/tests/Adapter/AdapterDefinitionFactoryTest.php index 7bed4e7..2da2023 100644 --- a/tests/Adapter/AdapterDefinitionFactoryTest.php +++ b/tests/Adapter/AdapterDefinitionFactoryTest.php @@ -117,7 +117,7 @@ public static function provideConfigOptions(): \Generator */ public static function testCreateDefinition($name, $options): void { - $factory = new AdapterDefinitionFactory(); + $factory = new AdapterDefinitionFactory([]); $definition = $factory->createDefinition($name, $options); self::assertInstanceOf(Definition::class, $definition); From 856dbe68fe3169003b5c547c5768da558fb1baad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20H=C3=A9lias?= Date: Mon, 25 Aug 2025 10:32:27 +0200 Subject: [PATCH 4/5] fix: correct rebase --- src/Adapter/Builder/UnixPermissionTrait.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Adapter/Builder/UnixPermissionTrait.php b/src/Adapter/Builder/UnixPermissionTrait.php index fdb7f12..a7b924e 100644 --- a/src/Adapter/Builder/UnixPermissionTrait.php +++ b/src/Adapter/Builder/UnixPermissionTrait.php @@ -16,14 +16,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** + * @internal + * * @author Maxime Hélias */ trait UnixPermissionTrait { protected function configureUnixOptions(OptionsResolver $resolver): void { - $resolver->setDefault('permissions', function (OptionsResolver $subResolver) { - $subResolver->setDefault('file', function (OptionsResolver $permsResolver) { + $method = method_exists($resolver, 'setOptions') ? 'setOptions' : 'setDefault'; + + $resolver->$method('permissions', function (OptionsResolver $subResolver) use ($method) { + $subResolver->$method('file', function (OptionsResolver $permsResolver) { $permsResolver->setDefault('public', 0644); $permsResolver->setAllowedTypes('public', 'scalar'); @@ -31,7 +35,7 @@ protected function configureUnixOptions(OptionsResolver $resolver): void $permsResolver->setAllowedTypes('private', 'scalar'); }); - $subResolver->setDefault('dir', function (OptionsResolver $permsResolver) { + $subResolver->$method('dir', function (OptionsResolver $permsResolver) { $permsResolver->setDefault('public', 0755); $permsResolver->setAllowedTypes('public', 'scalar'); From ef2675fa16fc8704b0a2ab6babb6980c40ad1345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20H=C3=A9lias?= Date: Thu, 28 Aug 2025 23:08:12 +0200 Subject: [PATCH 5/5] feat: make builder options discoverable --- README.md | 1 - docs/1-getting-started.md | 19 +- docs/2-cloud-storage-providers.md | 21 +- ...3-interacting-with-ftp-and-sftp-servers.md | 6 +- ...using-lazy-adapter-to-switch-at-runtime.md | 11 +- docs/5-creating-a-custom-adapter.md | 200 +++++++++++++++++- docs/6-gridfs.md | 9 +- docs/7-webdav.md | 3 +- docs/8-bunnycdn.md | 3 +- docs/B-configuration-reference.md | 90 -------- src/Adapter/AdapterDefinitionFactory.php | 80 ------- .../AbstractAdapterDefinitionBuilder.php | 39 ---- .../AdapterDefinitionBuilderInterface.php | 12 +- .../AsyncAwsAdapterDefinitionBuilder.php | 54 +++-- .../Builder/AwsAdapterDefinitionBuilder.php | 70 ++++-- .../Builder/AzureAdapterDefinitionBuilder.php | 44 +++- .../BunnyCDNAdapterDefinitionBuilder.php | 40 +++- .../Builder/FtpAdapterDefinitionBuilder.php | 122 +++++++++-- .../GcloudAdapterDefinitionBuilder.php | 66 +++++- .../GridFSAdapterDefinitionBuilder.php | 76 +++++-- .../Builder/LazyAdapterDefinitionBuilder.php | 68 ++++++ .../Builder/LocalAdapterDefinitionBuilder.php | 60 +++++- .../MemoryAdapterDefinitionBuilder.php | 28 ++- .../Builder/SftpAdapterDefinitionBuilder.php | 130 +++++++++--- src/Adapter/Builder/UnixPermissionTrait.php | 50 ++++- .../WebDAVAdapterDefinitionBuilder.php | 58 ++++- .../Compiler/GcloudFactoryPass.php | 28 --- src/DependencyInjection/Configuration.php | 156 +++++++++++--- .../FlysystemExtension.php | 141 +++++++++--- src/FlysystemBundle.php | 21 +- .../AbstractAdapterDefinitionBuilderTest.php | 109 ++++++++++ .../Adapter/AdapterDefinitionFactoryTest.php | 125 ----------- .../AsyncAwsAdapterDefinitionBuilderTest.php | 18 +- .../AwsAdapterDefinitionBuilderTest.php | 37 +--- .../AzureAdapterDefinitionBuilderTest.php | 25 +-- .../BunnyCDNAdapterDefinitionBuilderTest.php | 23 +- .../FtpAdapterDefinitionBuilderTest.php | 46 ++-- .../GcloudAdapterDefinitionBuilderTest.php | 42 +--- .../GridFSAdapterDefinitionBuilderTest.php | 34 +-- .../LazyAdapterDefinitionBuilderTest.php | 35 +++ .../LocalAdapterDefinitionBuilderTest.php | 67 +----- .../MemoryAdapterDefinitionBuilderTest.php | 16 +- .../SftpAdapterDefinitionBuilderTest.php | 60 ++---- .../WebDAVAdapterDefinitionBuilderTest.php | 28 +-- .../FlysystemExtensionTest.php | 4 - tests/Kernel/EmptyAppKernel.php | 6 +- tests/Kernel/FlysystemAppKernel.php | 2 +- tests/Kernel/FrameworkAppKernel.php | 3 +- tests/Kernel/config/flysystem.php | 40 ++-- 49 files changed, 1495 insertions(+), 931 deletions(-) delete mode 100644 docs/B-configuration-reference.md delete mode 100644 src/Adapter/AdapterDefinitionFactory.php delete mode 100644 src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php create mode 100644 src/Adapter/Builder/LazyAdapterDefinitionBuilder.php delete mode 100644 src/DependencyInjection/Compiler/GcloudFactoryPass.php create mode 100644 src/Test/AbstractAdapterDefinitionBuilderTest.php delete mode 100644 tests/Adapter/AdapterDefinitionFactoryTest.php create mode 100644 tests/Adapter/Builder/LazyAdapterDefinitionBuilderTest.php diff --git a/README.md b/README.md index 1307347..54c7299 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/1-getting-started.md b/docs/1-getting-started.md index 1a5747c..3a39e1f 100644 --- a/docs/1-getting-started.md +++ b/docs/1-getting-started.md @@ -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: @@ -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' ``` @@ -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' ``` @@ -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' ``` @@ -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 @@ -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 ``` diff --git a/docs/2-cloud-storage-providers.md b/docs/2-cloud-storage-providers.md index 7a4e5d2..0adc440 100644 --- a/docs/2-cloud-storage-providers.md +++ b/docs/2-cloud-storage-providers.md @@ -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' @@ -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' @@ -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' @@ -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' @@ -126,8 +122,7 @@ services: flysystem: storages: cdn.storage: - adapter: 'asyncaws' - options: + asyncaws: client: 'digitalocean_spaces_client' bucket: '%env(DIGITALOCEAN_SPACES_BUCKET)%' ``` @@ -152,8 +147,7 @@ services: flysystem: storages: cdn.storage: - adapter: 'asyncaws' - options: + asyncaws: client: 'scaleway_spaces_client' bucket: '%env(SCALEWAY_SPACES_BUCKET)%' ``` @@ -178,8 +172,7 @@ services: flysystem: storages: cdn.storage: - adapter: 'asyncaws' - options: + asyncaws: client: 'cloudflare_r2_client' bucket: '%env(CLOUDFLARE_R2_BUCKET)%' ``` diff --git a/docs/3-interacting-with-ftp-and-sftp-servers.md b/docs/3-interacting-with-ftp-and-sftp-servers.md index 7b7a7c8..6d596f5 100644 --- a/docs/3-interacting-with-ftp-and-sftp-servers.md +++ b/docs/3-interacting-with-ftp-and-sftp-servers.md @@ -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' @@ -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' diff --git a/docs/4-using-lazy-adapter-to-switch-at-runtime.md b/docs/4-using-lazy-adapter-to-switch-at-runtime.md index afe48a2..ba4455f 100644 --- a/docs/4-using-lazy-adapter-to-switch-at-runtime.md +++ b/docs/4-using-lazy-adapter-to-switch-at-runtime.md @@ -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)%' ``` diff --git a/docs/5-creating-a-custom-adapter.md b/docs/5-creating-a-custom-adapter.md index 7f48681..0bdbe5f 100644 --- a/docs/5-creating-a-custom-adapter.md +++ b/docs/5-creating-a-custom-adapter.md @@ -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 + 'vendor/package-name'] + 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; + } +} +``` + +### Registering the adapter builder + +#### Via Bundle (recommended for reusable bundles) + +If you're creating a bundle, register your builder in your bundle class: + +```php +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 +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 + +Create a test class extending `AbstractAdapterDefinitionBuilderTest`: + +```php + [[ + '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. diff --git a/docs/6-gridfs.md b/docs/6-gridfs.md index 7c75276..99082b4 100644 --- a/docs/6-gridfs.md +++ b/docs/6-gridfs.md @@ -18,8 +18,7 @@ For applications that uses Doctrine MongoDB ODM, set the `doctrine_connection` n flysystem: storages: users.storage: - adapter: 'gridfs' - options: + gridfs: # Name of a Doctrine MongoDB ODM connection doctrine_connection: 'default' # Use the default DB from the Doctrine MongoDB ODM configuration @@ -37,8 +36,7 @@ To initialize the GridFS bucket from configuration, set the `mongodb_uri` and `d flysystem: storages: users.storage: - adapter: 'gridfs' - options: + gridfs: # MongoDB client configuration mongodb_uri: '%env(MONGODB_URI)%' mongodb_uri_options: [] @@ -80,8 +78,7 @@ services: flysystem: storages: users.storage: - adapter: 'gridfs' - options: + gridfs: # Service name bucket: 'mongodb_gridfs_bucket' ``` diff --git a/docs/7-webdav.md b/docs/7-webdav.md index 66dbc7c..67980b6 100644 --- a/docs/7-webdav.md +++ b/docs/7-webdav.md @@ -24,8 +24,7 @@ services: flysystem: storages: webdav.storage: - adapter: 'webdav' - options: + webdav: client: 'webdav_client' prefix: 'optional/path/prefix' visibility_handling: !php/const \League\Flysystem\WebDAV\WebDAVAdapter::ON_VISIBILITY_THROW_ERROR # throw diff --git a/docs/8-bunnycdn.md b/docs/8-bunnycdn.md index 4989d96..df60fac 100644 --- a/docs/8-bunnycdn.md +++ b/docs/8-bunnycdn.md @@ -26,8 +26,7 @@ services: flysystem: storages: bunny.storage: - adapter: 'bunnycdn' - options: + bunnycdn: client: 'bunny_client' pull_zone: 'https://testing.b-cdn.net/' # optional ``` diff --git a/docs/B-configuration-reference.md b/docs/B-configuration-reference.md deleted file mode 100644 index 8abf782..0000000 --- a/docs/B-configuration-reference.md +++ /dev/null @@ -1,90 +0,0 @@ -# Configuration reference - -```yaml -flysystem: - storages: - users1.storage: - adapter: 'aws' - options: - client: 'aws_client_service' - bucket: 'bucket_name' - prefix: 'optional/path/prefix' - - users2.storage: - adapter: 'azure' - options: - client: 'azure_client_service' - container: 'container_name' - prefix: 'optional/path/prefix' - - users3.storage: - adapter: 'ftp' - options: - host: 'ftp.example.com' - username: 'username' - password: 'password' - port: 21 - root: '/path/to/root' - passive: true - ssl: true - timeout: 30 - ignore_passive_address: ~ - - users4.storage: - adapter: 'gcloud' - options: - client: 'gcloud_client_service' - bucket: 'bucket_name' - prefix: 'optional/path/prefix' - - users5.storage: - adapter: 'local' - options: - directory: '%kernel.project_dir%/storage' - lock: 0 - skip_links: false - permissions: - file: - public: 0o744 - private: 0o700 - dir: - public: 0o755 - private: 0o700 - visibility: ~ # default null. Possible values are 'public' or 'private' - directory_visibility: ~ # default null. Possible values are 'public' or 'private' - case_sensitive: true - disable_asserts: false - - users6.storage: - adapter: 'memory' - - users7.storage: - adapter: 'null' - - users8.storage: - adapter: 'sftp' - options: - host: 'example.com' - port: 22 - username: 'username' - password: 'password' - privateKey: 'path/to/or/contents/of/privatekey' - root: '/path/to/root' - timeout: 10 - - users9.storage: - adapter: 'lazy' - options: - source: 'flysystem_storage_service_to_use' - - users10.storage: - adapter: 'custom_adapter' - - users11.storage: - adapter: 'local' - options: - directory: '/tmp/storage' - public_url_generator: 'flysystem_public_url_generator_service_to_use' - temporary_url_generator: 'flysystem_temporary_url_generator_service_to_use' - read_only: true -``` diff --git a/src/Adapter/AdapterDefinitionFactory.php b/src/Adapter/AdapterDefinitionFactory.php deleted file mode 100644 index 4f4af8b..0000000 --- a/src/Adapter/AdapterDefinitionFactory.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace League\FlysystemBundle\Adapter; - -use League\FlysystemBundle\Adapter\Builder\AdapterDefinitionBuilderInterface; -use League\FlysystemBundle\Exception\MissingPackageException; -use Symfony\Component\DependencyInjection\Definition; - -/** - * @author Titouan Galopin - * - * @internal - */ -final class AdapterDefinitionFactory -{ - /** - * @var AdapterDefinitionBuilderInterface[] - */ - private array $builders; - - /** - * @param list $builders - */ - public function __construct(array $builders) - { - $this->builders = array_merge([ - new Builder\AsyncAwsAdapterDefinitionBuilder(), - new Builder\AwsAdapterDefinitionBuilder(), - new Builder\AzureAdapterDefinitionBuilder(), - new Builder\FtpAdapterDefinitionBuilder(), - new Builder\GcloudAdapterDefinitionBuilder(), - new Builder\GridFSAdapterDefinitionBuilder(), - new Builder\LocalAdapterDefinitionBuilder(), - new Builder\MemoryAdapterDefinitionBuilder(), - new Builder\SftpAdapterDefinitionBuilder(), - new Builder\WebDAVAdapterDefinitionBuilder(), - new Builder\BunnyCDNAdapterDefinitionBuilder(), - ], $builders); - } - - public function createDefinition(string $name, array $options, ?string $defaultVisibilityForDirectories = null): ?Definition - { - foreach ($this->builders as $builder) { - if ($builder->getName() !== $name) { - continue; - } - - $this->ensureRequiredPackagesBuilderAvailable($builder); - - return $builder->createDefinition($options, $defaultVisibilityForDirectories); - } - - return null; - } - - private function ensureRequiredPackagesBuilderAvailable(AdapterDefinitionBuilderInterface $builder): void - { - $missingPackages = []; - foreach ($builder->getRequiredPackages() as $requiredClass => $packageName) { - if (!class_exists($requiredClass)) { - $missingPackages[] = $packageName; - } - } - - if (!$missingPackages) { - return; - } - - throw new MissingPackageException(sprintf("Missing package%s, to use the \"%s\" adapter, run:\n\ncomposer require %s", \count($missingPackages) > 1 ? 's' : '', $this->getName(), implode(' ', $missingPackages))); - } -} diff --git a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php b/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php deleted file mode 100644 index 4816475..0000000 --- a/src/Adapter/Builder/AbstractAdapterDefinitionBuilder.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace League\FlysystemBundle\Adapter\Builder; - -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\OptionsResolver\OptionsResolver; - -/** - * @author Titouan Galopin - */ -abstract class AbstractAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface -{ - final public function createDefinition(array $options, ?string $defaultVisibilityForDirectories): Definition - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - - $definition = new Definition(); - $definition->setPublic(false); - $this->configureDefinition($definition, $resolver->resolve($options), $defaultVisibilityForDirectories); - - return $definition; - } - - abstract public function getRequiredPackages(): array; - - abstract protected function configureOptions(OptionsResolver $resolver); - - abstract protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories); -} diff --git a/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php b/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php index 612a58e..c7c4ba0 100644 --- a/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php +++ b/src/Adapter/Builder/AdapterDefinitionBuilderInterface.php @@ -11,7 +11,8 @@ namespace League\FlysystemBundle\Adapter\Builder; -use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @author Titouan Galopin @@ -23,7 +24,12 @@ public function getName(): string; public function getRequiredPackages(): array; /** - * Create the definition for this builder's adapter given an array of options. + * Add the configuration for this adapter to the configuration tree. */ - public function createDefinition(array $options, ?string $defaultVisibilityForDirectories): Definition; + public function addConfiguration(NodeDefinition $node): void; + + /** + * Create the adapter service and return its service ID. + */ + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string; } diff --git a/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php b/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php index 5d08997..b20411c 100644 --- a/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AsyncAwsAdapterDefinitionBuilder.php @@ -14,6 +14,8 @@ use League\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter; use League\Flysystem\AsyncAwsS3\PortableVisibilityConverter; use League\Flysystem\Visibility; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -24,7 +26,7 @@ * * @internal */ -final class AsyncAwsAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class AsyncAwsAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -38,7 +40,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('client'); $resolver->setAllowedTypes('client', 'string'); @@ -50,16 +55,41 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('prefix', 'string'); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(AsyncAwsS3Adapter::class); - $definition->setArgument(0, new Reference($options['client'])); - $definition->setArgument(1, $options['bucket']); - $definition->setArgument(2, $options['prefix']); - $definition->setArgument(3, - (new Definition(PortableVisibilityConverter::class)) - ->setArgument(0, $defaultVisibilityForDirectories ?? Visibility::PUBLIC) - ->setShared(false) - ); + $node + ->children() + ->scalarNode('client') + ->isRequired() + ->info('The AsyncAws S3 client service name') + ->end() + ->scalarNode('bucket') + ->isRequired() + ->info('The name of the AWS S3 bucket') + ->end() + ->scalarNode('prefix') + ->defaultValue('') + ->info('Optional path prefix to prepend to all object keys') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + $container + ->setDefinition($adapterId, new Definition(AsyncAwsS3Adapter::class)) + ->setArgument(0, new Reference($options['client'])) + ->setArgument(1, $options['bucket']) + ->setArgument(2, $options['prefix']) + ->setArgument(3, + (new Definition(PortableVisibilityConverter::class)) + ->setArgument(0, $defaultVisibilityForDirectories ?? Visibility::PUBLIC) + ->setShared(false) + ); + + return $adapterId; } } diff --git a/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php b/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php index 30b6e1c..dff7359 100644 --- a/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AwsAdapterDefinitionBuilder.php @@ -14,6 +14,8 @@ use League\Flysystem\AwsS3V3\AwsS3V3Adapter; use League\Flysystem\AwsS3V3\PortableVisibilityConverter; use League\Flysystem\Visibility; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -23,7 +25,7 @@ * * @internal */ -final class AwsAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class AwsAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -37,7 +39,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('client'); $resolver->setAllowedTypes('client', 'string'); @@ -55,19 +60,54 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('streamReads', 'bool'); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(AwsS3V3Adapter::class); - $definition->setArgument(0, new Reference($options['client'])); - $definition->setArgument(1, $options['bucket']); - $definition->setArgument(2, $options['prefix']); - $definition->setArgument(3, - (new Definition(PortableVisibilityConverter::class)) - ->setArgument(0, $defaultVisibilityForDirectories ?? Visibility::PUBLIC) - ->setShared(false) - ); - $definition->setArgument(4, null); - $definition->setArgument(5, $options['options']); - $definition->setArgument(6, $options['streamReads']); + $node + ->children() + ->scalarNode('client') + ->isRequired() + ->info('The AWS S3 client service name') + ->end() + ->scalarNode('bucket') + ->isRequired() + ->info('The name of the AWS S3 bucket') + ->end() + ->scalarNode('prefix') + ->defaultValue('') + ->info('Optional path prefix to prepend to all object keys') + ->end() + ->arrayNode('options') + ->defaultValue([]) + ->prototype('variable') + ->end() + ->info('Additional AWS S3 request options') + ->end() + ->booleanNode('streamReads') + ->defaultTrue() + ->info('Whether to use streaming for file reads') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + $container + ->setDefinition($adapterId, new Definition(AwsS3V3Adapter::class)) + ->setArgument(0, new Reference($options['client'])) + ->setArgument(1, $options['bucket']) + ->setArgument(2, $options['prefix']) + ->setArgument(3, + (new Definition(PortableVisibilityConverter::class)) + ->setArgument(0, $defaultVisibilityForDirectories ?? Visibility::PUBLIC) + ->setShared(false) + ) + ->setArgument(4, null) + ->setArgument(5, $options['options']) + ->setArgument(6, $options['streamReads']); + + return $adapterId; } } diff --git a/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php b/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php index b47a4b3..69bf2ee 100644 --- a/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/AzureAdapterDefinitionBuilder.php @@ -12,6 +12,8 @@ namespace League\FlysystemBundle\Adapter\Builder; use League\Flysystem\AzureBlobStorage\AzureBlobStorageAdapter; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -21,7 +23,7 @@ * * @internal */ -final class AzureAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class AzureAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -35,7 +37,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('client'); $resolver->setAllowedTypes('client', 'string'); @@ -47,11 +52,36 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('prefix', 'string'); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(AzureBlobStorageAdapter::class); - $definition->setArgument(0, new Reference($options['client'])); - $definition->setArgument(1, $options['container']); - $definition->setArgument(2, $options['prefix']); + $node + ->children() + ->scalarNode('client') + ->isRequired() + ->info('The Azure Blob Storage client service name') + ->end() + ->scalarNode('container') + ->isRequired() + ->info('The name of the Azure Blob Storage container') + ->end() + ->scalarNode('prefix') + ->defaultValue('') + ->info('Optional path prefix to prepend to all blob names') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + $container + ->setDefinition($adapterId, new Definition(AzureBlobStorageAdapter::class)) + ->setArgument(0, new Reference($options['client'])) + ->setArgument(1, $options['container']) + ->setArgument(2, $options['prefix']); + + return $adapterId; } } diff --git a/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php b/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php index 0b1e0eb..1701b79 100644 --- a/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/BunnyCDNAdapterDefinitionBuilder.php @@ -12,6 +12,8 @@ namespace League\FlysystemBundle\Adapter\Builder; use PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNAdapter; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -19,7 +21,7 @@ /** * @internal */ -final class BunnyCDNAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class BunnyCDNAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -33,7 +35,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('client'); $resolver->setAllowedTypes('client', 'string'); @@ -42,12 +47,31 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('pull_zone', 'string'); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(BunnyCDNAdapter::class); - $definition->setArguments([ - new Reference($options['client']), - $options['pull_zone'], - ]); + $node + ->children() + ->scalarNode('client') + ->isRequired() + ->info('The BunnyCDN client service name') + ->end() + ->scalarNode('pull_zone') + ->defaultValue('') + ->info('The BunnyCDN pull zone name') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + $container + ->setDefinition($adapterId, new Definition(BunnyCDNAdapter::class)) + ->setArgument(0, new Reference($options['client'])) + ->setArgument(1, $options['pull_zone']); + + return $adapterId; } } diff --git a/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php b/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php index 556d25d..d33d3a8 100644 --- a/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/FtpAdapterDefinitionBuilder.php @@ -14,6 +14,8 @@ use League\Flysystem\Ftp\FtpAdapter; use League\Flysystem\Ftp\FtpConnectionOptions; use League\Flysystem\Visibility; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -23,7 +25,7 @@ * * @internal */ -final class FtpAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class FtpAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { use UnixPermissionTrait; @@ -39,7 +41,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('host'); $resolver->setAllowedTypes('host', 'string'); @@ -85,19 +90,102 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefault('recurse_manually', true); $resolver->setAllowedTypes('recurse_manually', 'bool'); + $resolver->setDefault('use_raw_list_options', null); + $resolver->setAllowedTypes('use_raw_list_options', ['null', 'bool']); + $resolver->setDefault('connectivityChecker', null); $resolver->setAllowedTypes('connectivityChecker', ['string', 'null']); $this->configureUnixOptions($resolver); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { + $node + ->children() + ->scalarNode('host') + ->isRequired() + ->info('FTP host') + ->end() + ->scalarNode('username') + ->isRequired() + ->info('FTP username') + ->end() + ->scalarNode('password') + ->isRequired() + ->info('FTP password') + ->end() + ->integerNode('port') + ->defaultValue(21) + ->info('FTP port number') + ->end() + ->scalarNode('root') + ->defaultValue('') + ->info('FTP root directory') + ->end() + ->booleanNode('passive') + ->defaultTrue() + ->info('Use passive mode') + ->end() + ->booleanNode('ssl') + ->defaultFalse() + ->info('Use SSL/TLS encryption') + ->end() + ->integerNode('timeout') + ->defaultValue(90) + ->info('Connection timeout in seconds') + ->end() + ->scalarNode('ignore_passive_address') + ->defaultNull() + ->info('Ignore passive address') + ->end() + ->booleanNode('utf8') + ->defaultFalse() + ->info('Enable UTF8 mode') + ->end() + ->scalarNode('transfer_mode') + ->defaultNull() + ->info('Transfer mode (FTP_ASCII or FTP_BINARY constante on ftp extension)') + ->end() + ->enumNode('system_type') + ->values([null, 'windows', 'unix']) + ->defaultNull() + ->info('FTP system type') + ->end() + ->booleanNode('timestamps_on_unix_listings_enabled') + ->defaultFalse() + ->info('Enable timestamps on Unix listings') + ->end() + ->booleanNode('recurse_manually') + ->defaultTrue() + ->info('Recurse directories manually') + ->end() + ->booleanNode('use_raw_list_options') + ->defaultNull() + ->info('Use raw list options') + ->end() + ->scalarNode('connectivityChecker') + ->defaultNull() + ->info('Connectivity checker service name') + ->end() + ->end() + ; + + // Add Unix permissions configuration using the trait + $this->addUnixPermissionsConfiguration($node); + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + // Transform options to match FTP adapter expectations $options['transferMode'] = $options['transfer_mode']; $options['systemType'] = $options['system_type']; - $options['timestampsOnUnixListingsEnabled'] = $options['timestamps_on_unix_listings_enabled']; $options['ignorePassiveAddress'] = $options['ignore_passive_address']; + $options['timestampsOnUnixListingsEnabled'] = $options['timestamps_on_unix_listings_enabled']; $options['recurseManually'] = $options['recurse_manually']; + $options['useRawListOptions'] = $options['use_raw_list_options']; $connectivityChecker = null; if (null !== $options['connectivityChecker']) { @@ -107,21 +195,25 @@ protected function configureDefinition(Definition $definition, array $options, ? unset( $options['transfer_mode'], $options['system_type'], - $options['timestamps_on_unix_listings_enabled'], $options['ignore_passive_address'], + $options['timestamps_on_unix_listings_enabled'], $options['recurse_manually'], + $options['use_raw_list_options'], $options['connectivityChecker'] ); - $definition->setClass(FtpAdapter::class); - $definition->setArgument(0, - (new Definition(FtpConnectionOptions::class)) - ->setFactory([FtpConnectionOptions::class, 'fromArray']) - ->addArgument($options) - ->setShared(false) - ); - $definition->setArgument(1, null); - $definition->setArgument(2, $connectivityChecker); - $definition->setArgument(3, $this->createUnixDefinition($options['permissions'], $defaultVisibilityForDirectories ?? Visibility::PRIVATE)); + $container + ->setDefinition($adapterId, new Definition(FtpAdapter::class)) + ->setArgument(0, + (new Definition(FtpConnectionOptions::class)) + ->setFactory([FtpConnectionOptions::class, 'fromArray']) + ->addArgument($options) + ->setShared(false) + ) + ->setArgument(1, null) + ->setArgument(2, $connectivityChecker) + ->setArgument(3, $this->createUnixDefinition($options['permissions'] ?? [], $defaultVisibilityForDirectories ?? Visibility::PRIVATE)); + + return $adapterId; } } diff --git a/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php b/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php index e03e018..63f720c 100644 --- a/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/GcloudAdapterDefinitionBuilder.php @@ -11,7 +11,12 @@ namespace League\FlysystemBundle\Adapter\Builder; +use Google\Cloud\Storage\StorageClient; use League\Flysystem\GoogleCloudStorage\GoogleCloudStorageAdapter; +use League\Flysystem\GoogleCloudStorage\PortableVisibilityHandler; +use League\Flysystem\GoogleCloudStorage\UniformBucketLevelAccessVisibility; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -21,7 +26,7 @@ * * @internal */ -final class GcloudAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class GcloudAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -35,7 +40,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('client'); $resolver->setAllowedTypes('client', 'string'); @@ -50,20 +58,58 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('visibility_handler', ['string', 'null']); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $bucketDefinition = new Definition(); - $bucketDefinition->setFactory([new Reference($options['client']), 'bucket']); - $bucketDefinition->setArgument(0, $options['bucket']); + $node + ->children() + ->scalarNode('client') + ->isRequired() + ->info('The Google Cloud Storage client service name') + ->end() + ->scalarNode('bucket') + ->isRequired() + ->info('The name of the Google Cloud Storage bucket') + ->end() + ->scalarNode('prefix') + ->defaultValue('') + ->info('Optional path prefix to prepend to all object keys') + ->end() + ->scalarNode('visibility_handler') + ->defaultNull() + ->info('Optional visibility handler service name') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + // Register visibility handlers and their aliases + $container->register(PortableVisibilityHandler::class, PortableVisibilityHandler::class); + $container->setAlias('flysystem.adapter.gcloud.visibility.portable', PortableVisibilityHandler::class); + + $container->register(UniformBucketLevelAccessVisibility::class, UniformBucketLevelAccessVisibility::class); + $container->setAlias('flysystem.adapter.gcloud.visibility.uniform', UniformBucketLevelAccessVisibility::class); $visibilityHandlerReference = null; if (null !== $options['visibility_handler']) { $visibilityHandlerReference = new Reference($options['visibility_handler']); } - $definition->setClass(GoogleCloudStorageAdapter::class); - $definition->setArgument(0, $bucketDefinition); - $definition->setArgument(1, $options['prefix']); - $definition->setArgument(2, $visibilityHandlerReference); + // Create the adapter + $container + ->setDefinition($adapterId, new Definition(GoogleCloudStorageAdapter::class)) + ->setArgument(0, + (new Definition(StorageClient::class)) + ->setFactory([new Reference($options['client']), 'bucket']) + ->setArgument(0, $options['bucket']) + ->setPublic(false) + ) + ->setArgument(1, $options['prefix']) + ->setArgument(2, $visibilityHandlerReference); + + return $adapterId; } } diff --git a/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php b/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php index 7d953de..9adea01 100644 --- a/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php @@ -15,6 +15,8 @@ use League\Flysystem\GridFS\GridFSAdapter; use MongoDB\Client; use MongoDB\GridFS\Bucket; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; @@ -25,7 +27,7 @@ * * @internal */ -final class GridFSAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class GridFSAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -39,7 +41,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->define('bucket')->default(null)->allowedTypes('string', 'null'); $resolver->define('prefix')->default('')->allowedTypes('string'); @@ -50,11 +55,49 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->define('mongodb_driver_options')->default([])->allowedTypes('array'); } - /** - * @param array{bucket:string|null, prefix:string, database:string|null, doctrine_connection?:string, mongodb_uri?:string, mongodb_uri_options:array, mongodb_driver_options:array} $options - */ - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { + $node + ->children() + ->scalarNode('bucket') + ->defaultNull() + ->info('GridFS bucket service name (if using an existing bucket service)') + ->end() + ->scalarNode('prefix') + ->defaultValue('') + ->info('Optional path prefix to prepend to all file names') + ->end() + ->scalarNode('database') + ->defaultNull() + ->info('MongoDB database name') + ->end() + ->scalarNode('doctrine_connection') + ->info('Doctrine MongoDB connection name (mutually exclusive with mongodb_uri)') + ->end() + ->scalarNode('mongodb_uri') + ->info('MongoDB connection URI (mutually exclusive with doctrine_connection)') + ->end() + ->arrayNode('mongodb_uri_options') + ->defaultValue([]) + ->prototype('variable') + ->end() + ->info('MongoDB URI options') + ->end() + ->arrayNode('mongodb_driver_options') + ->defaultValue([]) + ->prototype('variable') + ->end() + ->info('MongoDB driver options') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + // Create bucket service based on configuration if (isset($options['doctrine_connection'])) { if (isset($options['mongodb_uri'])) { throw new InvalidArgumentException('In GridFS configuration, "doctrine_connection" and "mongodb_uri" options cannot be set together.'); @@ -63,28 +106,31 @@ protected function configureDefinition(Definition $definition, array $options, ? $bucket->setFactory([self::class, 'initializeBucketFromDocumentManager']); $bucket->setArguments([ new Reference(sprintf('doctrine_mongodb.odm.%s_document_manager', $options['doctrine_connection'])), - $options['database'], - $options['bucket'], + $options['database'] ?? null, + $options['bucket'] ?? null, ]); } elseif (isset($options['mongodb_uri'])) { $bucket = new Definition(Bucket::class); $bucket->setFactory([self::class, 'initializeBucketFromConfig']); $bucket->setArguments([ $options['mongodb_uri'], - $options['mongodb_uri_options'], - $options['mongodb_driver_options'], + $options['mongodb_uri_options'] ?? [], + $options['mongodb_driver_options'] ?? [], $options['database'] ?? throw new InvalidArgumentException('MongoDB "database" name is required for Flysystem GridFS configuration'), - $options['bucket'], + $options['bucket'] ?? null, ]); - } elseif ($options['bucket']) { + } elseif (isset($options['bucket'])) { $bucket = new Reference($options['bucket']); } else { throw new InvalidArgumentException('Flysystem GridFS configuration requires a "bucket" service name, a "mongodb_uri" or a "doctrine_connection" name'); } - $definition->setClass(GridFSAdapter::class); - $definition->setArgument(0, $bucket); - $definition->setArgument(1, $options['prefix']); + $container + ->setDefinition($adapterId, new Definition(GridFSAdapter::class)) + ->setArgument(0, $bucket) + ->setArgument(1, $options['prefix'] ?? ''); + + return $adapterId; } public static function initializeBucketFromDocumentManager(DocumentManager $documentManager, ?string $dbName, ?string $bucketName): Bucket diff --git a/src/Adapter/Builder/LazyAdapterDefinitionBuilder.php b/src/Adapter/Builder/LazyAdapterDefinitionBuilder.php new file mode 100644 index 0000000..6c6c321 --- /dev/null +++ b/src/Adapter/Builder/LazyAdapterDefinitionBuilder.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace League\FlysystemBundle\Adapter\Builder; + +use League\Flysystem\FilesystemOperator; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Titouan Galopin + * + * @internal + */ +final class LazyAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface +{ + public function getName(): string + { + return 'lazy'; + } + + public function getRequiredPackages(): array + { + return []; + } + + public function addConfiguration(NodeDefinition $node): void + { + $node + ->info('Lazy adapter for runtime storage selection') + ->children() + ->scalarNode('source') + ->info('The service name of the storage to use at runtime') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end(); + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories = null): ?string + { + // For lazy adapter, we don't create a standard adapter + // Instead, we create the storage definition directly here and return null + // to indicate that no adapter service should be created + + $definition = new Definition(FilesystemOperator::class); + $definition->setPublic(false); + $definition->setFactory([new Reference('flysystem.adapter.lazy.factory'), 'createStorage']); + $definition->setArgument(0, $options['source']); + $definition->setArgument(1, $storageName); + $definition->addTag('flysystem.storage', ['storage' => $storageName]); + + $container->setDefinition($storageName, $definition); + + // Return null to indicate this is handled specially + return null; + } +} diff --git a/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php b/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php index d8d1ea0..370d647 100644 --- a/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/LocalAdapterDefinitionBuilder.php @@ -13,6 +13,8 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Visibility; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -21,7 +23,7 @@ * * @internal */ -final class LocalAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class LocalAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { use UnixPermissionTrait; @@ -35,7 +37,10 @@ public function getRequiredPackages(): array return []; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('directory'); $resolver->setAllowedTypes('directory', 'string'); @@ -52,14 +57,49 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('lazy_root_creation', 'scalar'); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(LocalFilesystemAdapter::class); - $definition->setArgument(0, $options['directory']); - $definition->setArgument(1, $this->createUnixDefinition($options['permissions'], $defaultVisibilityForDirectories ?? Visibility::PRIVATE)); - $definition->setArgument(2, $options['lock']); - $definition->setArgument(3, $options['skip_links'] ? LocalFilesystemAdapter::SKIP_LINKS : LocalFilesystemAdapter::DISALLOW_LINKS); - $definition->setArgument(4, null); - $definition->setArgument(5, $options['lazy_root_creation']); + $node + ->children() + ->scalarNode('directory') + ->isRequired() + ->info('Directory path for local storage') + ->end() + + ->integerNode('lock') + ->defaultValue(0) + ->info('Lock flags for file operations') + ->end() + + ->booleanNode('skip_links') + ->defaultFalse() + ->info('Whether to skip symbolic links') + ->end() + + ->booleanNode('lazy_root_creation') + ->defaultFalse() + ->info('Whether to create the root directory lazily') + ->end() + ->end() + ; + + // Add Unix permissions configuration using the trait + $this->addUnixPermissionsConfiguration($node); + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + $container + ->setDefinition($adapterId, new Definition(LocalFilesystemAdapter::class)) + ->setArgument(0, $options['directory']) + ->setArgument(1, $this->createUnixDefinition($options['permissions'] ?? [], $defaultVisibilityForDirectories ?? Visibility::PRIVATE)) + ->setArgument(2, $options['lock'] ?? 0) + ->setArgument(3, ($options['skip_links'] ?? false) ? LocalFilesystemAdapter::SKIP_LINKS : LocalFilesystemAdapter::DISALLOW_LINKS) + ->setArgument(4, null) + ->setArgument(5, $options['lazy_root_creation'] ?? false); + + return $adapterId; } } diff --git a/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php b/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php index a69975f..b974010 100644 --- a/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/MemoryAdapterDefinitionBuilder.php @@ -12,6 +12,8 @@ namespace League\FlysystemBundle\Adapter\Builder; use League\Flysystem\InMemory\InMemoryFilesystemAdapter; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -20,7 +22,7 @@ * * @internal */ -final class MemoryAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class MemoryAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -34,12 +36,30 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver) + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { + // Memory adapter has no configurable options } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(InMemoryFilesystemAdapter::class); + // Memory adapter has no configurable options + $node + ->children() + ->end() + ->info('In-memory adapter for testing (no configuration options)') + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + $container->setDefinition($adapterId, new Definition(InMemoryFilesystemAdapter::class)); + + return $adapterId; } } diff --git a/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php b/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php index 7cf34a8..9cdb1bb 100644 --- a/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/SftpAdapterDefinitionBuilder.php @@ -16,6 +16,8 @@ use League\Flysystem\PhpseclibV3\SftpAdapter; use League\Flysystem\PhpseclibV3\SftpConnectionProvider; use League\Flysystem\Visibility; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -25,7 +27,7 @@ * * @internal */ -final class SftpAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class SftpAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { use UnixPermissionTrait; @@ -52,7 +54,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('host'); $resolver->setAllowedTypes('host', 'string'); @@ -63,45 +68,107 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefault('password', null); $resolver->setAllowedTypes('password', ['string', 'null']); - $resolver->setDefault('port', 22); - $resolver->setAllowedTypes('port', 'scalar'); - - $resolver->setDefault('root', ''); - $resolver->setAllowedTypes('root', 'string'); - $resolver->setDefault('privateKey', null); $resolver->setAllowedTypes('privateKey', ['string', 'null']); $resolver->setDefault('passphrase', null); $resolver->setAllowedTypes('passphrase', ['string', 'null']); - $resolver->setDefault('hostFingerprint', null); - $resolver->setAllowedTypes('hostFingerprint', ['string', 'null']); + $resolver->setDefault('port', 22); + $resolver->setAllowedTypes('port', 'scalar'); $resolver->setDefault('timeout', 90); $resolver->setAllowedTypes('timeout', 'scalar'); + $resolver->setDefault('hostFingerprint', null); + $resolver->setAllowedTypes('hostFingerprint', ['string', 'null']); + + $resolver->setDefault('connectivityChecker', null); + $resolver->setAllowedTypes('connectivityChecker', ['string', 'null']); + + $resolver->setDefault('preferredAlgorithms', []); + $resolver->setAllowedTypes('preferredAlgorithms', 'array'); + + $resolver->setDefault('root', ''); + $resolver->setAllowedTypes('root', 'string'); + $resolver->setDefault('directoryPerm', 0744); $resolver->setAllowedTypes('directoryPerm', 'scalar'); + $resolver->setDeprecated('directoryPerm', 'league/flysystem-bundle', '3.5', 'The "directoryPerm" option is deprecated, use the "permissions" array option instead.'); $resolver->setDefault('permPrivate', 0700); $resolver->setAllowedTypes('permPrivate', 'scalar'); + $resolver->setDeprecated('permPrivate', 'league/flysystem-bundle', '3.5', 'The "permPrivate" option is deprecated, use the "permissions" array option instead.'); $resolver->setDefault('permPublic', 0744); $resolver->setAllowedTypes('permPublic', 'scalar'); - - $resolver->setDefault('connectivityChecker', null); - $resolver->setAllowedTypes('connectivityChecker', ['string', 'null']); - - $resolver->setDefault('preferredAlgorithms', []); - $resolver->setAllowedTypes('preferredAlgorithms', 'array'); + $resolver->setDeprecated('permPublic', 'league/flysystem-bundle', '3.5', 'The "permPublic" option is deprecated, use the "permissions" array option instead.'); $this->configureUnixOptions($resolver); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - // Prevent BC + $node + ->children() + ->scalarNode('host') + ->isRequired() + ->info('SFTP host') + ->end() + ->scalarNode('username') + ->isRequired() + ->info('SFTP username') + ->end() + ->scalarNode('password') + ->defaultNull() + ->info('SFTP password (optional if using private key)') + ->end() + ->scalarNode('privateKey') + ->defaultNull() + ->info('Path to private key file or private key content') + ->end() + ->scalarNode('passphrase') + ->defaultNull() + ->info('Private key passphrase') + ->end() + ->integerNode('port') + ->defaultValue(22) + ->info('SFTP port number') + ->end() + ->integerNode('timeout') + ->defaultValue(90) + ->info('Connection timeout in seconds') + ->end() + ->scalarNode('hostFingerprint') + ->defaultNull() + ->info('Host fingerprint for verification') + ->end() + ->scalarNode('connectivityChecker') + ->defaultNull() + ->info('Connectivity checker service name') + ->end() + ->arrayNode('preferredAlgorithms') + ->defaultValue([]) + ->prototype('variable') + ->end() + ->info('Preferred algorithms for the SSH connection') + ->end() + ->scalarNode('root') + ->defaultValue('') + ->info('SFTP root directory') + ->end() + ->end() + ; + + // Add Unix permissions configuration using the trait + $this->addUnixPermissionsConfiguration($node); + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.'.$storageName; + + // Prevent BC - determine which version to use $adapterFqcn = SftpAdapter::class; $connectionFqcn = SftpConnectionProvider::class; if (class_exists(SftpAdapterLegacy::class)) { @@ -109,18 +176,25 @@ protected function configureDefinition(Definition $definition, array $options, ? $connectionFqcn = SftpConnectionProviderLegacy::class; } - if (null !== $options['connectivityChecker']) { + if (!empty($options['connectivityChecker'])) { $options['connectivityChecker'] = new Reference($options['connectivityChecker']); } - $definition->setClass($adapterFqcn); - $definition->setArgument(0, - (new Definition($connectionFqcn)) - ->setFactory([$connectionFqcn, 'fromArray']) - ->addArgument($options) - ->setShared(false) - ); - $definition->setArgument(1, $options['root']); - $definition->setArgument(2, $this->createUnixDefinition($options['permissions'], $defaultVisibilityForDirectories ?? Visibility::PRIVATE)); + $root = $options['root'] ?? ''; + unset($options['root']); + + // Create main adapter service + $container + ->setDefinition($adapterId, new Definition($adapterFqcn)) + ->setArgument(0, + (new Definition($adapterFqcn)) + ->setFactory([$connectionFqcn, 'fromArray']) + ->addArgument($options) + ->setShared(false) + ) + ->setArgument(1, $root) + ->setArgument(2, $this->createUnixDefinition($options['permissions'] ?? [], $defaultVisibilityForDirectories ?? Visibility::PRIVATE)); + + return $adapterId; } } diff --git a/src/Adapter/Builder/UnixPermissionTrait.php b/src/Adapter/Builder/UnixPermissionTrait.php index a7b924e..643806f 100644 --- a/src/Adapter/Builder/UnixPermissionTrait.php +++ b/src/Adapter/Builder/UnixPermissionTrait.php @@ -12,16 +12,20 @@ namespace League\FlysystemBundle\Adapter\Builder; use League\Flysystem\UnixVisibility\PortableVisibilityConverter; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * @internal - * * @author Maxime Hélias + * + * @internal */ trait UnixPermissionTrait { + /** + * @deprecated since 3.5, use addUnixPermissionsConfiguration() with the new discoverable format instead + */ protected function configureUnixOptions(OptionsResolver $resolver): void { $method = method_exists($resolver, 'setOptions') ? 'setOptions' : 'setDefault'; @@ -45,6 +49,48 @@ protected function configureUnixOptions(OptionsResolver $resolver): void }); } + protected function addUnixPermissionsConfiguration(NodeDefinition $node): void + { + $node + ->children() + ->arrayNode('permissions') + ->addDefaultsIfNotSet() + ->info('Unix permissions configuration for files and directories') + ->children() + ->arrayNode('file') + ->addDefaultsIfNotSet() + ->info('File permissions') + ->children() + ->integerNode('public') + ->defaultValue(0644) + ->info('Public file permissions') + ->end() + ->integerNode('private') + ->defaultValue(0600) + ->info('Private file permissions') + ->end() + ->end() + ->end() + ->arrayNode('dir') + ->addDefaultsIfNotSet() + ->info('Directory permissions') + ->children() + ->integerNode('public') + ->defaultValue(0755) + ->info('Public directory permissions') + ->end() + ->integerNode('private') + ->defaultValue(0700) + ->info('Private directory permissions') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + protected function createUnixDefinition(array $permissions, string $defaultVisibilityForDirectories): Definition { return (new Definition(PortableVisibilityConverter::class)) diff --git a/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php b/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php index 705d042..118f5a0 100644 --- a/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php +++ b/src/Adapter/Builder/WebDAVAdapterDefinitionBuilder.php @@ -12,6 +12,8 @@ namespace League\FlysystemBundle\Adapter\Builder; use League\Flysystem\WebDAV\WebDAVAdapter; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -21,7 +23,7 @@ * * @internal */ -final class WebDAVAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +final class WebDAVAdapterDefinitionBuilder implements AdapterDefinitionBuilderInterface { public function getName(): string { @@ -35,7 +37,10 @@ public function getRequiredPackages(): array ]; } - protected function configureOptions(OptionsResolver $resolver): void + /** + * @deprecated since 3.5, use addConfiguration() with the new config format instead + */ + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('client'); $resolver->setAllowedTypes('client', 'string'); @@ -53,15 +58,46 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('manual_move', 'bool'); } - protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + public function addConfiguration(NodeDefinition $node): void { - $definition->setClass(WebDAVAdapter::class); - $definition->setArguments([ - new Reference($options['client']), - $options['prefix'], - $options['visibility_handling'], - $options['manual_copy'], - $options['manual_move'], - ]); + $node + ->children() + ->scalarNode('client') + ->isRequired() + ->info('The WebDAV client service name') + ->end() + ->scalarNode('prefix') + ->defaultValue('') + ->info('Optional path prefix to prepend to all paths') + ->end() + ->scalarNode('visibility_handling') + ->defaultValue(WebDAVAdapter::ON_VISIBILITY_THROW_ERROR) + ->info('How to handle visibility operations') + ->end() + ->booleanNode('manual_copy') + ->defaultFalse() + ->info('Whether to handle copy operations manually') + ->end() + ->booleanNode('manual_move') + ->defaultFalse() + ->info('Whether to handle move operations manually') + ->end() + ->end() + ; + } + + public function createAdapter(ContainerBuilder $container, string $storageName, array $options, ?string $defaultVisibilityForDirectories): ?string + { + $adapterId = 'flysystem.adapter.webdav.'.$storageName; + + $container + ->setDefinition($adapterId, new Definition(WebDAVAdapter::class)) + ->setArgument(0, new Reference($options['client'])) + ->setArgument(1, $options['prefix']) + ->setArgument(2, $options['visibility_handling']) + ->setArgument(3, $options['manual_copy']) + ->setArgument(4, $options['manual_move']); + + return $adapterId; } } diff --git a/src/DependencyInjection/Compiler/GcloudFactoryPass.php b/src/DependencyInjection/Compiler/GcloudFactoryPass.php deleted file mode 100644 index de5ccf3..0000000 --- a/src/DependencyInjection/Compiler/GcloudFactoryPass.php +++ /dev/null @@ -1,28 +0,0 @@ -register(PortableVisibilityHandler::class, PortableVisibilityHandler::class); - $container->setAlias('flysystem.adapter.gcloud.visibility.portable', PortableVisibilityHandler::class); - - $container->register(UniformBucketLevelAccessVisibility::class, UniformBucketLevelAccessVisibility::class); - $container->setAlias('flysystem.adapter.gcloud.visibility.uniform', UniformBucketLevelAccessVisibility::class); - } -} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 43a7676..dda7879 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -11,6 +11,8 @@ namespace League\FlysystemBundle\DependencyInjection; +use League\FlysystemBundle\Adapter\Builder\AdapterDefinitionBuilderInterface; +use League\FlysystemBundle\Exception\MissingPackageException; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -21,45 +23,149 @@ */ final class Configuration implements ConfigurationInterface { + /** @var AdapterDefinitionBuilderInterface[] */ + private array $adapterBuilders = []; + + public function __construct(array $adapterBuilders = []) + { + $this->adapterBuilders = $adapterBuilders; + } + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('flysystem'); $rootNode = $treeBuilder->getRootNode(); - $rootNode + $storagesNode = $rootNode ->fixXmlConfig('storage') ->children() ->arrayNode('storages') ->useAttributeAsKey('name') ->arrayPrototype() - ->performNoDeepMerging() - ->children() - ->scalarNode('adapter')->isRequired()->end() - ->arrayNode('options') - ->variablePrototype() - ->end() - ->defaultValue([]) - ->end() - ->scalarNode('visibility')->defaultNull()->end() - ->scalarNode('directory_visibility')->defaultNull()->end() - ->booleanNode('case_sensitive')->defaultTrue()->end() - ->booleanNode('disable_asserts')->defaultFalse()->end() - ->arrayNode('public_url') - ->beforeNormalization()->castToArray()->end() - ->defaultValue([]) - ->scalarPrototype()->end() - ->end() - ->scalarNode('path_normalizer')->defaultNull()->end() - ->scalarNode('public_url_generator')->defaultNull()->end() - ->scalarNode('temporary_url_generator')->defaultNull()->end() - ->booleanNode('read_only')->defaultFalse()->end() - ->end() + ->performNoDeepMerging(); + + $storageChildren = $storagesNode->children(); + + // Legacy format (for backward compatibility) + $storageChildren + ->scalarNode('adapter') + ->info('DEPRECATED: Use the new config format instead (e.g. "local:" instead of "adapter: local")') + ->end() + ->arrayNode('options') + ->info('DEPRECATED: Use the new config format instead') + ->variablePrototype()->end() + ->defaultValue([]) + ->end(); + + // Add adapter configurations + foreach ($this->adapterBuilders as $builder) { + $adapterNode = $storageChildren + ->arrayNode($builder->getName()) + ->canBeUnset(); + + $builder->addConfiguration($adapterNode); + } + + // Custom adapter service reference + $storageChildren + ->scalarNode('service') + ->info('Reference to a custom adapter service (alternative to registered adapter types)') + ->end(); + + // General storage options + $storageChildren + ->scalarNode('visibility') + ->defaultNull() + ->info('Default visibility for files') + ->end() + ->scalarNode('directory_visibility') + ->defaultNull() + ->info('Default visibility for directories') + ->end() + ->booleanNode('case_sensitive') + ->defaultTrue() + ->setDeprecated('league/flysystem-bundle', '3.5', 'The "case_sensitive" option is deprecated and will be removed in 4.0.') + ->end() + ->booleanNode('disable_asserts') + ->defaultFalse() + ->setDeprecated('league/flysystem-bundle', '3.5', 'The "disable_asserts" option is deprecated and will be removed in 4.0.') + ->end() + ->arrayNode('public_url') + ->beforeNormalization()->castToArray()->end() + ->defaultValue([]) + ->scalarPrototype()->end() + ->info('For adapter that do not provide public URLs or override adapter capabilities, a base URL can be configured in the main Filesystem configuration') + ->end() + ->scalarNode('path_normalizer') + ->defaultNull() + ->info('Path normalizer service name (should implement League\Flysystem\PathNormalizer)') + ->end() + ->scalarNode('public_url_generator') + ->defaultNull() + ->info('For adapter that do not provide public URLs or override adapter capabilities and public_url option, a public URL generator service name can be configured in the main Filesystem configuration (should implement League\Flysystem\PublicUrlGenerator)') + ->end() + ->scalarNode('temporary_url_generator') + ->defaultNull() + ->info('For adapter that do not provide public URLs or override adapter capabilities, a temporary URL generator service name can be configured in the main Filesystem configuration (should implement League\Flysystem\TemporaryUrlGenerator)') + ->end() + ->booleanNode('read_only') + ->defaultFalse() + ->info('Converts a file system to read-only') + ->end() + ->end(); + + // Validation for exclusive adapter configuration + $storagesNode + ->validate() + ->ifTrue(function ($config) { + return $this->validateAdapterConfiguration($config); + }) + ->thenInvalid('You must configure exactly one adapter per storage using either the legacy format (adapter + options), the new config format, or a custom service reference') ->end() - ->defaultValue([]) ->end() + ->defaultValue([]) ->end() - ; + ->end(); return $treeBuilder; } + + private function validateAdapterConfiguration(array $config): bool + { + $hasLegacyFormat = isset($config['adapter']); + $hasServiceFormat = isset($config['service']); + + $configuredAdapters = []; + foreach ($this->adapterBuilders as $builder) { + if (isset($config[$builder->getName()])) { + $this->ensureRequiredPackagesAvailable($builder); + $configuredAdapters[] = $builder->getName(); + } + } + + $totalConfigurations = ($hasLegacyFormat ? 1 : 0) + ($hasServiceFormat ? 1 : 0) + count($configuredAdapters); + + // Must have exactly one configuration type + if (1 !== $totalConfigurations) { + return true; + } + + return false; + } + + public static function ensureRequiredPackagesAvailable(AdapterDefinitionBuilderInterface $builder): void + { + $missingPackages = []; + foreach ($builder->getRequiredPackages() as $requiredClass => $packageName) { + if (!class_exists($requiredClass)) { + $missingPackages[] = $packageName; + } + } + + if (!$missingPackages) { + return; + } + + throw new MissingPackageException(sprintf("Missing package%s, to use the \"%s\" adapter, run:\n\ncomposer require %s", \count($missingPackages) > 1 ? 's' : '', $builder->getName(), implode(' ', $missingPackages))); + } } diff --git a/src/DependencyInjection/FlysystemExtension.php b/src/DependencyInjection/FlysystemExtension.php index 144d702..e6f8e6a 100644 --- a/src/DependencyInjection/FlysystemExtension.php +++ b/src/DependencyInjection/FlysystemExtension.php @@ -16,10 +16,10 @@ use League\Flysystem\FilesystemReader; use League\Flysystem\FilesystemWriter; use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; -use League\FlysystemBundle\Adapter\AdapterDefinitionFactory; use League\FlysystemBundle\Adapter\Builder\AdapterDefinitionBuilderInterface; use League\FlysystemBundle\Exception\MissingPackageException; use League\FlysystemBundle\Lazy\LazyFactory; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -28,16 +28,25 @@ /** * @author Titouan Galopin + * + * @internal */ final class FlysystemExtension extends Extension { /** @var list */ private array $adapterDefinitionBuilders = []; + /** @var array|null */ + private ?array $adapterDefinitionBuildersCache = null; + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return new Configuration($this->getAdapterDefinitionBuilders()); + } + public function load(array $configs, ContainerBuilder $container): void { - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); + $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); $container ->setDefinition('flysystem.adapter.lazy.factory', new Definition(LazyFactory::class)) @@ -50,18 +59,24 @@ public function load(array $configs, ContainerBuilder $container): void public function addAdapterDefinitionBuilder(AdapterDefinitionBuilderInterface $builder): void { $this->adapterDefinitionBuilders[] = $builder; + // Invalidate cache when adding new builder + $this->adapterDefinitionBuildersCache = null; } private function createStoragesDefinitions(array $config, ContainerBuilder $container): void { - $definitionFactory = new AdapterDefinitionFactory($this->adapterDefinitionBuilders); - foreach ($config['storages'] as $storageName => $storageConfig) { - // If the storage is a lazy one, it's resolved at runtime - if ('lazy' === $storageConfig['adapter']) { - $container->setDefinition($storageName, $this->createLazyStorageDefinition($storageName, $storageConfig['options'])); + // Resolve adapter type and options from either legacy or new format + $adapterType = $this->resolveAdapterType($storageConfig); + $adapterOptions = $this->resolveAdapterOptions($storageConfig, $adapterType); - // Register named autowiring alias + // Create adapter definition + $adapterId = $this->createAdapterDefinition($container, $adapterType, $storageName, $adapterOptions, $storageConfig['directory_visibility'] ?? null, isset($storageConfig['adapter'])); + + // Special case for lazy adapter: it handles storage creation internally + if (null === $adapterId && 'lazy' === $adapterType) { + // The LazyAdapterDefinitionBuilder has already created the storage definition + // We register all autowiring aliases here for consistency $container->registerAliasForArgument($storageName, FilesystemOperator::class, $storageName)->setPublic(false); $container->registerAliasForArgument($storageName, FilesystemReader::class, $storageName)->setPublic(false); $container->registerAliasForArgument($storageName, FilesystemWriter::class, $storageName)->setPublic(false); @@ -69,13 +84,9 @@ private function createStoragesDefinitions(array $config, ContainerBuilder $cont continue; } - // Create adapter definition - if ($adapter = $definitionFactory->createDefinition($storageConfig['adapter'], $storageConfig['options'], $storageConfig['directory_visibility'] ?? null)) { - // Native adapter - $container->setDefinition($id = 'flysystem.adapter.'.$storageName, $adapter)->setPublic(false); - } else { + if (null === $adapterId) { // Custom adapter - $container->setAlias($id = 'flysystem.adapter.'.$storageName, $storageConfig['adapter'])->setPublic(false); + $container->setAlias($adapterId = 'flysystem.adapter.'.$storageName, $adapterType)->setPublic(false); } // Create ReadOnly adapter @@ -84,9 +95,9 @@ private function createStoragesDefinitions(array $config, ContainerBuilder $cont throw new MissingPackageException("Missing package, to use the readonly option, run:\n\ncomposer require league/flysystem-read-only"); } - $originalAdapterId = $id; + $originalAdapterId = $adapterId; $container->setDefinition( - $id = $id.'.read_only', + $adapterId = $adapterId.'.read_only', $this->createReadOnlyAdapterDefinition(new Reference($originalAdapterId)) ); } @@ -94,7 +105,7 @@ private function createStoragesDefinitions(array $config, ContainerBuilder $cont // Create storage definition $container->setDefinition( $storageName, - $this->createStorageDefinition($storageName, new Reference($id), $storageConfig) + $this->createStorageDefinition($storageName, new Reference($adapterId), $storageConfig) ); // Register named autowiring alias @@ -104,22 +115,6 @@ private function createStoragesDefinitions(array $config, ContainerBuilder $cont } } - private function createLazyStorageDefinition(string $storageName, array $options): Definition - { - $resolver = new OptionsResolver(); - $resolver->setRequired('source'); - $resolver->setAllowedTypes('source', 'string'); - - $definition = new Definition(FilesystemOperator::class); - $definition->setPublic(false); - $definition->setFactory([new Reference('flysystem.adapter.lazy.factory'), 'createStorage']); - $definition->setArgument(0, $resolver->resolve($options)['source']); - $definition->setArgument(1, $storageName); - $definition->addTag('flysystem.storage', ['storage' => $storageName]); - - return $definition; - } - private function createStorageDefinition(string $storageName, Reference $adapter, array $config): Definition { $publicUrl = null; @@ -153,4 +148,82 @@ private function createReadOnlyAdapterDefinition(Reference $adapter): Definition return $definition; } + + /** + * Get adapter builders indexed by name with caching. + * + * @return array + */ + private function getAdapterDefinitionBuilders(): array + { + if (null === $this->adapterDefinitionBuildersCache) { + $this->adapterDefinitionBuildersCache = []; + foreach ($this->adapterDefinitionBuilders as $builder) { + $this->adapterDefinitionBuildersCache[$builder->getName()] = $builder; + } + } + + return $this->adapterDefinitionBuildersCache; + } + + private function createAdapterDefinition(ContainerBuilder $container, string $type, string $storageName, array $options, ?string $defaultVisibilityForDirectories = null, bool $isLegacyFormat = false): ?string + { + $builders = $this->getAdapterDefinitionBuilders(); + + $builder = $builders[$type] ?? null; + if (null === $builder) { + return null; + } + + // For legacy format + if ($isLegacyFormat && method_exists($builder, 'configureOptions')) { + Configuration::ensureRequiredPackagesAvailable($builder); + + $resolver = new OptionsResolver(); + $builder->configureOptions($resolver); + $options = $resolver->resolve($options); + } + + return $builder->createAdapter($container, $storageName, $options, $defaultVisibilityForDirectories); + } + + private function resolveAdapterType(array $config): string + { + // Legacy format + if (isset($config['adapter'])) { + trigger_deprecation( + 'league/flysystem-bundle', + '3.5', + 'Using the legacy format with "adapter" and "options" keys is deprecated. Use the new discoverable format instead. See the migration guide for details.' + ); + + return $config['adapter']; + } + + // New discoverable format - check for registered builders first + $builders = $this->getAdapterDefinitionBuilders(); + foreach ($builders as $name => $builder) { + if (isset($config[$name])) { + return $name; + } + } + + // If no registered builder found, check for 'service' key (custom adapter) + if (isset($config['service'])) { + return $config['service']; + } + + throw new \InvalidArgumentException('No adapter configured. Use either a registered adapter type or specify a "service" key for custom adapters.'); + } + + private function resolveAdapterOptions(array $config, string $adapterType): array + { + // Legacy format + if (!empty($config['options'])) { + return $config['options']; + } + + // New discoverable format + return $config[$adapterType] ?? []; + } } diff --git a/src/FlysystemBundle.php b/src/FlysystemBundle.php index 1dd5ce4..1f3b1d5 100644 --- a/src/FlysystemBundle.php +++ b/src/FlysystemBundle.php @@ -11,13 +11,16 @@ namespace League\FlysystemBundle; -use League\FlysystemBundle\DependencyInjection\Compiler\GcloudFactoryPass; +use League\FlysystemBundle\Adapter\Builder; use League\FlysystemBundle\DependencyInjection\Compiler\LazyFactoryPass; +use League\FlysystemBundle\DependencyInjection\FlysystemExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; /** * @author Titouan Galopin + * + * @internal */ final class FlysystemBundle extends Bundle { @@ -25,7 +28,21 @@ public function build(ContainerBuilder $container): void { parent::build($container); + /** @var FlysystemExtension $extension */ + $extension = $container->getExtension('flysystem'); + $extension->addAdapterDefinitionBuilder(new Builder\AsyncAwsAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\AwsAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\AzureAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\FtpAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\GcloudAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\GridFSAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\LazyAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\LocalAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\MemoryAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\SftpAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\WebDAVAdapterDefinitionBuilder()); + $extension->addAdapterDefinitionBuilder(new Builder\BunnyCDNAdapterDefinitionBuilder()); + $container->addCompilerPass(new LazyFactoryPass()); - $container->addCompilerPass(new GcloudFactoryPass()); } } diff --git a/src/Test/AbstractAdapterDefinitionBuilderTest.php b/src/Test/AbstractAdapterDefinitionBuilderTest.php new file mode 100644 index 0000000..3643f6e --- /dev/null +++ b/src/Test/AbstractAdapterDefinitionBuilderTest.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace League\FlysystemBundle\Test; + +use League\FlysystemBundle\Adapter\Builder\AdapterDefinitionBuilderInterface; +use League\FlysystemBundle\Adapter\Builder\LazyAdapterDefinitionBuilder; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +abstract class AbstractAdapterDefinitionBuilderTest extends TestCase +{ + private ContainerBuilder $container; + + protected function setUp(): void + { + $this->container = new ContainerBuilder(); + } + + protected function getContainer(): ContainerBuilder + { + return $this->container; + } + + abstract protected function createBuilder(): AdapterDefinitionBuilderInterface; + + abstract protected function assertDefinition(Definition $definition): void; + + /** + * Default data provider - can be overridden in concrete classes. + */ + public static function provideValidOptions(): \Generator + { + yield 'empty' => [[]]; + } + + public function testGetName(): void + { + $builder = $this->createBuilder(); + $this->assertIsString($builder->getName()); + $this->assertNotEmpty($builder->getName()); + } + + public function testGetRequiredPackages(): void + { + $builder = $this->createBuilder(); + $packages = $builder->getRequiredPackages(); + $this->assertIsArray($packages); + + foreach ($packages as $class => $packageName) { + $this->assertIsString($class); + $this->assertIsString($packageName); + } + } + + /** + * @dataProvider provideValidOptions + */ + public function testConfigurationAndAdapter(array $options, ?string $storageName = null): void + { + $builder = $this->createBuilder(); + + // Use the data provider key as storage name if available, fallback to 'test_storage' + $storageName = $storageName ?? $this->dataName() ?? 'test_storage'; + + // Test that configuration accepts the options + $node = new ArrayNodeDefinition('test'); + $builder->addConfiguration($node); + $tree = $node->getNode(); + + try { + $normalizedOptions = $tree->finalize($options); + $this->assertIsArray($normalizedOptions); + } catch (\Exception $e) { + $this->fail('Configuration should accept valid options: '.$e->getMessage()); + } + + // Test that adapter can be created with the same options + $adapterId = $builder->createAdapter($this->container, $storageName, $normalizedOptions, null); + + if ($builder instanceof LazyAdapterDefinitionBuilder) { + // Lazy adapter does not create an adapter service + $this->assertNull($adapterId); + $this->assertTrue($this->container->hasDefinition($storageName)); + $definition = $this->container->getDefinition($storageName); + $this->assertSame('flysystem.adapter.lazy.factory', (string) $definition->getFactory()[0]); + $this->assertSame($storageName, $definition->getArgument(1)); + + return; + } + + $this->assertIsString($adapterId); + $this->assertTrue($this->container->hasDefinition($adapterId)); + + if ('full' === $storageName) { + $this->assertDefinition($this->container->getDefinition($adapterId)); + } + } +} diff --git a/tests/Adapter/AdapterDefinitionFactoryTest.php b/tests/Adapter/AdapterDefinitionFactoryTest.php deleted file mode 100644 index 2da2023..0000000 --- a/tests/Adapter/AdapterDefinitionFactoryTest.php +++ /dev/null @@ -1,125 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\League\FlysystemBundle\Adapter; - -use League\FlysystemBundle\Adapter\AdapterDefinitionFactory; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Definition; - -class AdapterDefinitionFactoryTest extends TestCase -{ - public static function provideConfigOptions(): \Generator - { - yield 'fs_async_aws' => [ - 'adapter' => 'asyncaws', - 'options' => [ - 'client' => 'aws_client_service', - 'bucket' => 'bucket_name', - 'prefix' => 'optional/path/prefix', - ], - ]; - - yield 'fs_aws' => [ - 'adapter' => 'aws', - 'options' => [ - 'client' => 'aws_client_service', - 'bucket' => 'bucket_name', - 'prefix' => 'optional/path/prefix', - ], - ]; - - yield 'fs_azure' => [ - 'adapter' => 'azure', - 'options' => [ - 'client' => 'azure_client_service', - 'container' => 'container_name', - 'prefix' => 'optional/path/prefix', - ], - ]; - - yield 'fs_ftp' => [ - 'adapter' => 'ftp', - 'options' => [ - 'host' => 'ftp.example.com', - 'username' => 'username', - 'password' => 'password', - 'port' => 21, - 'root' => '/path/to/root', - 'passive' => true, - 'ssl' => true, - 'timeout' => 30, - 'ignore_passive_address' => true, - ], - ]; - - yield 'fs_gcloud' => [ - 'adapter' => 'gcloud', - 'options' => [ - 'client' => 'gcloud_client_service', - 'bucket' => 'bucket_name', - 'prefix' => 'optional/path/prefix', - ], - ]; - - yield 'fs_local' => [ - 'adapter' => 'local', - 'options' => [ - 'directory' => '%kernel.project_dir%/storage', - 'lock' => 0, - 'skip_links' => false, - 'lazy_root_creation' => false, - 'permissions' => [ - 'file' => [ - 'public' => '0744', - 'private' => '0700', - ], - 'dir' => [ - 'public' => '0755', - 'private' => '0700', - ], - ], - ], - ]; - - yield 'fs_memory' => [ - 'adapter' => 'memory', - 'options' => [], - ]; - - yield 'fs_sftp' => [ - 'adapter' => 'sftp', - 'options' => [ - 'host' => 'example.com', - 'port' => 22, - 'username' => 'username', - 'password' => 'password', - 'privateKey' => 'path/to/or/contents/of/privatekey', - 'root' => '/path/to/root', - 'timeout' => 10, - 'preferredAlgorithms' => [ - 'hostkey' => ['rsa-sha2-256', 'ssh-rsa'], - ], - ], - ]; - } - - /** - * @dataProvider provideConfigOptions - */ - public static function testCreateDefinition($name, $options): void - { - $factory = new AdapterDefinitionFactory([]); - - $definition = $factory->createDefinition($name, $options); - self::assertInstanceOf(Definition::class, $definition); - } -} diff --git a/tests/Adapter/Builder/AsyncAwsAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/AsyncAwsAdapterDefinitionBuilderTest.php index e3ad952..da99727 100644 --- a/tests/Adapter/Builder/AsyncAwsAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/AsyncAwsAdapterDefinitionBuilderTest.php @@ -14,12 +14,13 @@ use League\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter; use League\Flysystem\Visibility; use League\FlysystemBundle\Adapter\Builder\AsyncAwsAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class AsyncAwsAdapterDefinitionBuilderTest extends TestCase +class AsyncAwsAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): AsyncAwsAdapterDefinitionBuilder + protected function createBuilder(): AsyncAwsAdapterDefinitionBuilder { return new AsyncAwsAdapterDefinitionBuilder(); } @@ -31,23 +32,20 @@ public static function provideValidOptions(): \Generator 'bucket' => 'bucket', ]]; - yield 'prefix' => [[ + yield 'full' => [[ 'client' => 'my_client', 'bucket' => 'bucket', 'prefix' => 'prefix/path', ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void + protected function assertDefinition(Definition $definition): void { - $definition = $this->createBuilder()->createDefinition($options, Visibility::PRIVATE); $this->assertSame(AsyncAwsS3Adapter::class, $definition->getClass()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('my_client', (string) $definition->getArgument(0)); $this->assertSame('bucket', $definition->getArgument(1)); - $this->assertSame(Visibility::PRIVATE, $definition->getArgument(3)->getArgument(0)); + $this->assertSame('prefix/path', $definition->getArgument(2)); + $this->assertSame(Visibility::PUBLIC, $definition->getArgument(3)->getArgument(0)); } } diff --git a/tests/Adapter/Builder/AwsAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/AwsAdapterDefinitionBuilderTest.php index af92bba..8288675 100644 --- a/tests/Adapter/Builder/AwsAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/AwsAdapterDefinitionBuilderTest.php @@ -14,12 +14,13 @@ use League\Flysystem\AwsS3V3\AwsS3V3Adapter; use League\Flysystem\Visibility; use League\FlysystemBundle\Adapter\Builder\AwsAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class AwsAdapterDefinitionBuilderTest extends TestCase +class AwsAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): AwsAdapterDefinitionBuilder + protected function createBuilder(): AwsAdapterDefinitionBuilder { return new AwsAdapterDefinitionBuilder(); } @@ -31,48 +32,26 @@ public static function provideValidOptions(): \Generator 'bucket' => 'bucket', ]]; - yield 'prefix' => [[ + yield 'full' => [[ 'client' => 'my_client', 'bucket' => 'bucket', 'prefix' => 'prefix/path', - ]]; - - yield 'options' => [[ - 'client' => 'my_client', - 'bucket' => 'bucket', 'options' => [ 'ServerSideEncryption' => 'AES256', ], + 'streamReads' => false, ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void - { - $this->assertSame(AwsS3V3Adapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void + protected function assertDefinition(Definition $definition): void { - $definition = $this->createBuilder()->createDefinition([ - 'client' => 'my_client', - 'bucket' => 'bucket', - 'prefix' => 'prefix/path', - 'options' => [ - 'ServerSideEncryption' => 'AES256', - ], - 'streamReads' => false, - ], Visibility::PRIVATE); - $this->assertSame(AwsS3V3Adapter::class, $definition->getClass()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('my_client', (string) $definition->getArgument(0)); $this->assertSame('bucket', $definition->getArgument(1)); $this->assertSame('prefix/path', $definition->getArgument(2)); + $this->assertSame(Visibility::PUBLIC, $definition->getArgument(3)->getArgument(0)); $this->assertSame(['ServerSideEncryption' => 'AES256'], $definition->getArgument(5)); $this->assertFalse($definition->getArgument(6)); - $this->assertSame(Visibility::PRIVATE, $definition->getArgument(3)->getArgument(0)); } } diff --git a/tests/Adapter/Builder/AzureAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/AzureAdapterDefinitionBuilderTest.php index 2e3f4af..644b24c 100644 --- a/tests/Adapter/Builder/AzureAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/AzureAdapterDefinitionBuilderTest.php @@ -13,12 +13,13 @@ use League\Flysystem\AzureBlobStorage\AzureBlobStorageAdapter; use League\FlysystemBundle\Adapter\Builder\AzureAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class AzureAdapterDefinitionBuilderTest extends TestCase +class AzureAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): AzureAdapterDefinitionBuilder + protected function createBuilder(): AzureAdapterDefinitionBuilder { return new AzureAdapterDefinitionBuilder(); } @@ -30,29 +31,15 @@ public static function provideValidOptions(): \Generator 'container' => 'container_name', ]]; - yield 'prefix' => [[ + yield 'full' => [[ 'client' => 'my_client', 'container' => 'container_name', 'prefix' => 'prefix/path', ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void + protected function assertDefinition(Definition $definition): void { - $this->assertSame(AzureBlobStorageAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void - { - $definition = $this->createBuilder()->createDefinition([ - 'client' => 'my_client', - 'container' => 'container_name', - 'prefix' => 'prefix/path', - ], null); - $this->assertSame(AzureBlobStorageAdapter::class, $definition->getClass()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('my_client', (string) $definition->getArgument(0)); diff --git a/tests/Adapter/Builder/BunnyCDNAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/BunnyCDNAdapterDefinitionBuilderTest.php index 3277805..4b32aa6 100644 --- a/tests/Adapter/Builder/BunnyCDNAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/BunnyCDNAdapterDefinitionBuilderTest.php @@ -11,14 +11,14 @@ namespace Tests\League\FlysystemBundle\Adapter\Builder; -use League\Flysystem\Visibility; use League\FlysystemBundle\Adapter\Builder\BunnyCDNAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; use PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNAdapter; +use Symfony\Component\DependencyInjection\Definition; -class BunnyCDNAdapterDefinitionBuilderTest extends TestCase +class BunnyCDNAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): BunnyCDNAdapterDefinitionBuilder + protected function createBuilder(): BunnyCDNAdapterDefinitionBuilder { return new BunnyCDNAdapterDefinitionBuilder(); } @@ -35,21 +35,8 @@ public static function provideValidOptions(): \Generator ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition(array $options): void + protected function assertDefinition(Definition $definition): void { - $this->assertSame(BunnyCDNAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void - { - $definition = $this->createBuilder()->createDefinition([ - 'client' => 'bunny_client', - 'pull_zone' => 'z1', - ], Visibility::PUBLIC); - $this->assertSame(BunnyCDNAdapter::class, $definition->getClass()); $this->assertSame('bunny_client', (string) $definition->getArgument(0)); $this->assertSame('z1', $definition->getArgument(1)); diff --git a/tests/Adapter/Builder/FtpAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/FtpAdapterDefinitionBuilderTest.php index 8e6be6b..2cf64b0 100644 --- a/tests/Adapter/Builder/FtpAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/FtpAdapterDefinitionBuilderTest.php @@ -14,11 +14,12 @@ use League\Flysystem\Ftp\FtpAdapter; use League\Flysystem\Visibility; use League\FlysystemBundle\Adapter\Builder\FtpAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; -class FtpAdapterDefinitionBuilderTest extends TestCase +class FtpAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): FtpAdapterDefinitionBuilder + protected function createBuilder(): FtpAdapterDefinitionBuilder { return new FtpAdapterDefinitionBuilder(); } @@ -42,33 +43,19 @@ public static function provideValidOptions(): \Generator 'timeout' => 30, 'ignore_passive_address' => true, 'utf8' => false, + 'system_type' => 'unix', + 'recurse_manually' => false, + 'use_raw_list_options' => true, + 'connectivityChecker' => 'my_checker', ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void + protected function assertDefinition(Definition $definition): void { - $this->assertSame(FtpAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void - { - $definition = $this->createBuilder()->createDefinition([ + $expected = [ 'host' => 'ftp.example.com', 'username' => 'username', 'password' => 'password', - 'port' => 21, - 'root' => '/path/to/root', - 'passive' => true, - 'ssl' => true, - 'timeout' => 30, - 'ignore_passive_address' => true, - 'utf8' => false, - ], Visibility::PUBLIC); - - $expected = [ 'port' => 21, 'root' => '/path/to/root', 'passive' => true, @@ -85,18 +72,17 @@ public function testOptionsBehavior(): void 'private' => 0700, ], ], - 'host' => 'ftp.example.com', - 'username' => 'username', - 'password' => 'password', 'transferMode' => null, - 'systemType' => null, - 'timestampsOnUnixListingsEnabled' => false, + 'systemType' => 'unix', 'ignorePassiveAddress' => true, - 'recurseManually' => true, + 'timestampsOnUnixListingsEnabled' => false, + 'recurseManually' => false, + 'useRawListOptions' => true, ]; $this->assertSame(FtpAdapter::class, $definition->getClass()); $this->assertSame($expected, $definition->getArgument(0)->getArgument(0)); - $this->assertSame(Visibility::PUBLIC, $definition->getArgument(3)->getArgument(1)); + $this->assertSame('my_checker', (string) $definition->getArgument(2)); + $this->assertSame(Visibility::PRIVATE, $definition->getArgument(3)->getArgument(1)); } } diff --git a/tests/Adapter/Builder/GcloudAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/GcloudAdapterDefinitionBuilderTest.php index 4a06a41..9bb6b4d 100644 --- a/tests/Adapter/Builder/GcloudAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/GcloudAdapterDefinitionBuilderTest.php @@ -11,17 +11,15 @@ namespace Tests\League\FlysystemBundle\Adapter\Builder; -use League\Flysystem\GoogleCloudStorage\GoogleCloudStorageAdapter; -use League\Flysystem\GoogleCloudStorage\PortableVisibilityHandler; use League\Flysystem\GoogleCloudStorage\UniformBucketLevelAccessVisibility; use League\FlysystemBundle\Adapter\Builder\GcloudAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class GcloudAdapterDefinitionBuilderTest extends TestCase +class GcloudAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): GcloudAdapterDefinitionBuilder + protected function createBuilder(): GcloudAdapterDefinitionBuilder { return new GcloudAdapterDefinitionBuilder(); } @@ -37,44 +35,15 @@ public static function provideValidOptions(): \Generator 'client' => 'my_client', 'bucket' => 'bucket', 'prefix' => 'prefix/path', - ]]; - - yield 'portable visibility handler' => [[ - 'client' => 'my_client', - 'bucket' => 'bucket', - 'visibility_handler' => PortableVisibilityHandler::class, - ]]; - - yield 'uniform visibility handler' => [[ - 'client' => 'my_client', - 'bucket' => 'bucket', 'visibility_handler' => UniformBucketLevelAccessVisibility::class, ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void + protected function assertDefinition(Definition $definition): void { - $this->assertSame(GoogleCloudStorageAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void - { - $definition = $this->createBuilder()->createDefinition([ - 'client' => 'my_client', - 'bucket' => 'bucket_name', - 'prefix' => 'prefix/path', - 'visibility_handler' => UniformBucketLevelAccessVisibility::class, - ], null); - - $this->assertSame(GoogleCloudStorageAdapter::class, $definition->getClass()); - - /** @var Definition $bucketDefinition */ $bucketDefinition = $definition->getArgument(0); $this->assertInstanceOf(Definition::class, $bucketDefinition); - $this->assertSame('bucket_name', $bucketDefinition->getArgument(0)); + $this->assertSame('bucket', $bucketDefinition->getArgument(0)); $this->assertInstanceOf(Reference::class, $bucketDefinition->getFactory()[0]); $this->assertSame('my_client', (string) $bucketDefinition->getFactory()[0]); $this->assertSame('bucket', $bucketDefinition->getFactory()[1]); @@ -84,5 +53,6 @@ public function testOptionsBehavior(): void /** @var Reference $visibilityHandlerReference */ $visibilityHandlerReference = $definition->getArgument(2); $this->assertInstanceOf(Reference::class, $visibilityHandlerReference); + $this->assertSame(UniformBucketLevelAccessVisibility::class, (string) $visibilityHandlerReference); } } diff --git a/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php index 8da4646..b3c245b 100644 --- a/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php @@ -15,14 +15,14 @@ use Doctrine\ODM\MongoDB\DocumentManager; use League\Flysystem\GridFS\GridFSAdapter; use League\FlysystemBundle\Adapter\Builder\GridFSAdapterDefinitionBuilder; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; use MongoDB\Client; -use MongoDB\GridFS\Bucket; -use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -class GridFSAdapterDefinitionBuilderTest extends TestCase +class GridFSAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): GridFSAdapterDefinitionBuilder + protected function createBuilder(): GridFSAdapterDefinitionBuilder { return new GridFSAdapterDefinitionBuilder(); } @@ -39,17 +39,18 @@ public static function provideValidOptions(): \Generator 'bucket' => 'avatars', ]]; - yield 'config_minimal' => [[ + yield 'minimal' => [[ 'mongodb_uri' => 'mongodb://localhost:27017/', 'database' => 'testing', ]]; - yield 'config_full' => [[ + yield 'full' => [[ 'mongodb_uri' => 'mongodb://server1:27017,server2:27017/', 'mongodb_uri_options' => ['appname' => 'flysystem'], 'mongodb_driver_options' => ['disableClientPersistence' => false], 'database' => 'testing', 'bucket' => 'avatars', + 'prefix' => 'prefix/path', ]]; yield 'service' => [[ @@ -57,12 +58,19 @@ public static function provideValidOptions(): \Generator ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void + protected function assertDefinition(Definition $definition): void { - $this->assertSame(GridFSAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); + $this->assertSame(GridFSAdapter::class, $definition->getClass()); + + $bucketDefinition = $definition->getArgument(0); + $this->assertInstanceOf(Definition::class, $bucketDefinition); + $this->assertSame('mongodb://server1:27017,server2:27017/', $bucketDefinition->getArgument(0)); + $this->assertSame(['appname' => 'flysystem'], $bucketDefinition->getArgument(1)); + $this->assertSame(['disableClientPersistence' => false], $bucketDefinition->getArgument(2)); + $this->assertSame('testing', $bucketDefinition->getArgument(3)); + $this->assertSame('avatars', $bucketDefinition->getArgument(4)); + + $this->assertSame('prefix/path', $definition->getArgument(1)); } public static function provideInvalidOptions(): \Generator @@ -93,7 +101,7 @@ public function testInvalidOptions(array $options, string $message): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($message); - $builder->createDefinition($options, null); + $builder->createAdapter($this->getContainer(), 'test_storage', $options, null); } public function testInitializeBucketFromDocumentManager(): void @@ -111,7 +119,6 @@ public function testInitializeBucketFromDocumentManager(): void $bucket = GridFSAdapterDefinitionBuilder::initializeBucketFromDocumentManager($dm, null, 'avatars'); - $this->assertInstanceOf(Bucket::class, $bucket); $this->assertSame('testing', $bucket->getDatabaseName()); $this->assertSame('avatars', $bucket->getBucketName()); } @@ -126,7 +133,6 @@ public function testInitializeBucketFromConfig(): void 'avatars' ); - $this->assertInstanceOf(Bucket::class, $bucket); $this->assertSame('testing', $bucket->getDatabaseName()); $this->assertSame('avatars', $bucket->getBucketName()); } diff --git a/tests/Adapter/Builder/LazyAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/LazyAdapterDefinitionBuilderTest.php new file mode 100644 index 0000000..357cfbd --- /dev/null +++ b/tests/Adapter/Builder/LazyAdapterDefinitionBuilderTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\League\FlysystemBundle\Adapter\Builder; + +use League\FlysystemBundle\Adapter\Builder\LazyAdapterDefinitionBuilder; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; + +class LazyAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest +{ + protected function createBuilder(): LazyAdapterDefinitionBuilder + { + return new LazyAdapterDefinitionBuilder(); + } + + public static function provideValidOptions(): \Generator + { + yield 'minimal' => [[ + 'source' => 'fs_service', + ]]; + } + + protected function assertDefinition(Definition $definition): void + { + } +} diff --git a/tests/Adapter/Builder/LocalAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/LocalAdapterDefinitionBuilderTest.php index 7d36c92..efaf4c5 100644 --- a/tests/Adapter/Builder/LocalAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/LocalAdapterDefinitionBuilderTest.php @@ -14,11 +14,12 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Visibility; use League\FlysystemBundle\Adapter\Builder\LocalAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; -class LocalAdapterDefinitionBuilderTest extends TestCase +class LocalAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): LocalAdapterDefinitionBuilder + protected function createBuilder(): LocalAdapterDefinitionBuilder { return new LocalAdapterDefinitionBuilder(); } @@ -29,47 +30,9 @@ public static function provideValidOptions(): \Generator 'directory' => __DIR__, ]]; - yield 'lock' => [[ - 'directory' => __DIR__, - 'lock' => 0, - ]]; - - yield 'skip_links' => [[ - 'directory' => __DIR__, - 'skip_links' => true, - ]]; - - yield 'permissions_file_public' => [[ - 'directory' => __DIR__, - 'permissions' => [ - 'file' => [ - 'public' => 0700, - ], - ], - ]]; - - yield 'permissions_dir_private' => [[ - 'directory' => __DIR__, - 'permissions' => [ - 'dir' => [ - 'private' => 0755, - ], - ], - ]]; - - yield 'lazy_root_creation_enabled' => [[ - 'directory' => __DIR__, - 'lazy_root_creation' => true, - ]]; - - yield 'lazy_root_creation_disabled' => [[ - 'directory' => __DIR__, - 'lazy_root_creation' => false, - ]]; - yield 'full' => [[ 'directory' => __DIR__, - 'lock' => 0, + 'lock' => LOCK_EX, 'skip_links' => true, 'permissions' => [ 'file' => [ @@ -85,15 +48,7 @@ public static function provideValidOptions(): \Generator ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void - { - $this->assertSame(LocalFilesystemAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void + protected function assertDefinition(Definition $definition): void { $permissions = [ 'file' => [ @@ -106,20 +61,12 @@ public function testOptionsBehavior(): void ], ]; - $definition = $this->createBuilder()->createDefinition([ - 'directory' => __DIR__, - 'lock' => LOCK_EX, - 'skip_links' => true, - 'permissions' => $permissions, - 'lazy_root_creation' => true, - ], Visibility::PUBLIC); - $this->assertSame(LocalFilesystemAdapter::class, $definition->getClass()); $this->assertSame(__DIR__, $definition->getArgument(0)); $this->assertSame($permissions, $definition->getArgument(1)->getArgument(0)); + $this->assertSame(Visibility::PRIVATE, $definition->getArgument(1)->getArgument(1)); $this->assertSame(LOCK_EX, $definition->getArgument(2)); $this->assertSame(LocalFilesystemAdapter::SKIP_LINKS, $definition->getArgument(3)); $this->assertSame(true, $definition->getArgument(5)); - $this->assertSame(Visibility::PUBLIC, $definition->getArgument(1)->getArgument(1)); } } diff --git a/tests/Adapter/Builder/MemoryAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/MemoryAdapterDefinitionBuilderTest.php index 1ac9633..d2fbdb4 100644 --- a/tests/Adapter/Builder/MemoryAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/MemoryAdapterDefinitionBuilderTest.php @@ -13,17 +13,23 @@ use League\Flysystem\InMemory\InMemoryFilesystemAdapter; use League\FlysystemBundle\Adapter\Builder\MemoryAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; -class MemoryAdapterDefinitionBuilderTest extends TestCase +class MemoryAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): MemoryAdapterDefinitionBuilder + protected function createBuilder(): MemoryAdapterDefinitionBuilder { return new MemoryAdapterDefinitionBuilder(); } - public function testOptionsBehavior(): void + public static function provideValidOptions(): \Generator { - $this->assertSame(InMemoryFilesystemAdapter::class, $this->createBuilder()->createDefinition([], null)->getClass()); + yield 'full' => [[]]; + } + + protected function assertDefinition(Definition $definition): void + { + $this->assertSame(InMemoryFilesystemAdapter::class, $definition->getClass()); } } diff --git a/tests/Adapter/Builder/SftpAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/SftpAdapterDefinitionBuilderTest.php index 5142c87..038c3bc 100644 --- a/tests/Adapter/Builder/SftpAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/SftpAdapterDefinitionBuilderTest.php @@ -14,11 +14,12 @@ use League\Flysystem\PhpseclibV3\SftpAdapter; use League\Flysystem\Visibility; use League\FlysystemBundle\Adapter\Builder\SftpAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; -class SftpAdapterDefinitionBuilderTest extends TestCase +class SftpAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): SftpAdapterDefinitionBuilder + protected function createBuilder(): SftpAdapterDefinitionBuilder { return new SftpAdapterDefinitionBuilder(); } @@ -34,56 +35,33 @@ public static function provideValidOptions(): \Generator 'host' => 'ftp.example.com', 'username' => 'username', 'password' => 'password', - 'port' => 22, - 'root' => '/path/to/root', 'privateKey' => '/path/to/or/contents/of/privatekey', 'passphrase' => null, - 'hostFingerprint' => null, + 'port' => 22, 'timeout' => 30, + 'hostFingerprint' => null, + 'connectivityChecker' => 'my_service_check', 'preferredAlgorithms' => [ 'hostkey' => ['rsa-sha2-256', 'ssh-rsa'], ], + 'root' => '/path/to/root', ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition($options): void - { - $this->assertSame(SftpAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void + protected function assertDefinition(Definition $definition): void { - $definition = $this->createBuilder()->createDefinition([ + $expected = [ 'host' => 'ftp.example.com', 'username' => 'username', 'password' => 'password', - 'port' => 22, - 'root' => '/path/to/root', 'privateKey' => '/path/to/or/contents/of/privatekey', 'passphrase' => null, - 'hostFingerprint' => null, - 'timeout' => 30, - 'directoryPerm' => 0755, - 'permPrivate' => 0700, - 'permPublic' => 0744, - ], Visibility::PUBLIC); - - $expected = [ - 'password' => 'password', 'port' => 22, - 'root' => '/path/to/root', - 'privateKey' => '/path/to/or/contents/of/privatekey', - 'passphrase' => null, - 'hostFingerprint' => null, 'timeout' => 30, - 'directoryPerm' => 0755, - 'permPrivate' => 0700, - 'permPublic' => 0744, - 'connectivityChecker' => null, - 'preferredAlgorithms' => [], + 'hostFingerprint' => null, + 'preferredAlgorithms' => [ + 'hostkey' => ['rsa-sha2-256', 'ssh-rsa'], + ], 'permissions' => [ 'file' => [ 'public' => 0644, @@ -94,13 +72,13 @@ public function testOptionsBehavior(): void 'private' => 0700, ], ], - 'host' => 'ftp.example.com', - 'username' => 'username', ]; $this->assertSame(SftpAdapter::class, $definition->getClass()); - $this->assertSame($expected, $definition->getArgument(0)->getArgument(0)); - $this->assertSame($expected['root'], $definition->getArgument(1)); - $this->assertSame(Visibility::PUBLIC, $definition->getArgument(2)->getArgument(1)); + $connectionProviderOptions = $definition->getArgument(0)->getArgument(0); + unset($connectionProviderOptions['connectivityChecker']); + $this->assertSame($expected, $connectionProviderOptions); + $this->assertSame('/path/to/root', $definition->getArgument(1)); + $this->assertSame(Visibility::PRIVATE, $definition->getArgument(2)->getArgument(1)); } } diff --git a/tests/Adapter/Builder/WebDAVAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/WebDAVAdapterDefinitionBuilderTest.php index 082ae05..9bc4dcc 100644 --- a/tests/Adapter/Builder/WebDAVAdapterDefinitionBuilderTest.php +++ b/tests/Adapter/Builder/WebDAVAdapterDefinitionBuilderTest.php @@ -11,17 +11,17 @@ namespace Tests\League\FlysystemBundle\Adapter\Builder; -use League\Flysystem\Visibility; use League\Flysystem\WebDAV\WebDAVAdapter; use League\FlysystemBundle\Adapter\Builder\WebDAVAdapterDefinitionBuilder; -use PHPUnit\Framework\TestCase; +use League\FlysystemBundle\Test\AbstractAdapterDefinitionBuilderTest; +use Symfony\Component\DependencyInjection\Definition; /** * @author Kévin Dunglas */ -class WebDAVAdapterDefinitionBuilderTest extends TestCase +class WebDAVAdapterDefinitionBuilderTest extends AbstractAdapterDefinitionBuilderTest { - public function createBuilder(): WebDAVAdapterDefinitionBuilder + protected function createBuilder(): WebDAVAdapterDefinitionBuilder { return new WebDAVAdapterDefinitionBuilder(); } @@ -35,30 +35,14 @@ public static function provideValidOptions(): \Generator yield 'full' => [[ 'client' => 'webdav_client', 'prefix' => 'optional/path/prefix', - 'visibility_handling' => WebDAVAdapter::ON_VISIBILITY_THROW_ERROR, + 'visibility_handling' => WebDAVAdapter::ON_VISIBILITY_IGNORE, 'manual_copy' => false, 'manual_move' => false, ]]; } - /** - * @dataProvider provideValidOptions - */ - public function testCreateDefinition(array $options): void + protected function assertDefinition(Definition $definition): void { - $this->assertSame(WebDAVAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); - } - - public function testOptionsBehavior(): void - { - $definition = $this->createBuilder()->createDefinition([ - 'client' => 'webdav_client', - 'prefix' => 'optional/path/prefix', - 'visibility_handling' => WebDAVAdapter::ON_VISIBILITY_IGNORE, - 'manual_copy' => false, - 'manual_move' => false, - ], Visibility::PUBLIC); - $this->assertSame(WebDAVAdapter::class, $definition->getClass()); $this->assertSame('webdav_client', (string) $definition->getArgument(0)); $this->assertSame('optional/path/prefix', $definition->getArgument(1)); diff --git a/tests/DependencyInjection/FlysystemExtensionTest.php b/tests/DependencyInjection/FlysystemExtensionTest.php index 171edb4..dd72767 100644 --- a/tests/DependencyInjection/FlysystemExtensionTest.php +++ b/tests/DependencyInjection/FlysystemExtensionTest.php @@ -63,10 +63,6 @@ public function testTaggedCollection(string $fsName): void $kernel = $this->createFlysystemKernel(); $container = $kernel->getContainer()->get('test.service_container'); - if (!$container->has('storages_tagged_collection')) { - $this->markTestSkipped('Symfony 4.3+ is required to use indexed tagged service collections'); - } - $storages = iterator_to_array($container->get('storages_tagged_collection')->locator); $this->assertInstanceOf(FilesystemOperator::class, $storages[$fsName]); diff --git a/tests/Kernel/EmptyAppKernel.php b/tests/Kernel/EmptyAppKernel.php index 05c2010..f8a9411 100644 --- a/tests/Kernel/EmptyAppKernel.php +++ b/tests/Kernel/EmptyAppKernel.php @@ -29,7 +29,11 @@ public function registerContainerConfiguration(LoaderInterface $loader): void { $loader->load(function (ContainerBuilder $container) { $container->loadFromExtension('flysystem', [ - 'storages' => ['uploads.storage' => ['adapter' => 'local', 'options' => ['directory' => __DIR__]]], + 'storages' => [ + 'uploads.storage' => [ + 'local' => ['directory' => __DIR__], + ], + ], ]); }); } diff --git a/tests/Kernel/FlysystemAppKernel.php b/tests/Kernel/FlysystemAppKernel.php index 6fbfad1..44f5043 100644 --- a/tests/Kernel/FlysystemAppKernel.php +++ b/tests/Kernel/FlysystemAppKernel.php @@ -35,7 +35,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $loader->load(function (ContainerBuilder $container) use ($adapterClients) { foreach ($adapterClients as $service => $mock) { - $container->setDefinition($service, new Definition())->setSynthetic(true); + $container->setDefinition($service, new Definition(\stdClass::class))->setSynthetic(true); } }); diff --git a/tests/Kernel/FrameworkAppKernel.php b/tests/Kernel/FrameworkAppKernel.php index 042f75d..21d96b2 100644 --- a/tests/Kernel/FrameworkAppKernel.php +++ b/tests/Kernel/FrameworkAppKernel.php @@ -33,8 +33,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $container->loadFromExtension('flysystem', [ 'storages' => [ 'uploads.storage' => [ - 'adapter' => 'local', - 'options' => ['directory' => __DIR__], + 'local' => ['directory' => __DIR__], ], ], ]); diff --git a/tests/Kernel/config/flysystem.php b/tests/Kernel/config/flysystem.php index 1405e66..a23e4be 100644 --- a/tests/Kernel/config/flysystem.php +++ b/tests/Kernel/config/flysystem.php @@ -6,35 +6,31 @@ $container->extension('flysystem', [ 'storages' => [ 'fs_asyncaws' => [ - 'adapter' => 'asyncaws', - 'options' => [ + 'asyncaws' => [ 'client' => 'asyncaws_client_service', 'bucket' => '%env(AWS_BUCKET)%', 'prefix' => 'optional/path/prefix', ], ], 'fs_aws' => [ - 'adapter' => 'aws', - 'options' => [ + 'aws' => [ 'client' => 'aws_client_service', 'bucket' => '%env(AWS_BUCKET)%', 'prefix' => 'optional/path/prefix', ], ], 'fs_azure' => [ - 'adapter' => 'azure', - 'options' => [ + 'azure' => [ 'client' => 'azure_client_service', 'container' => 'container_name', 'prefix' => 'optional/path/prefix', ], ], 'fs_custom' => [ - 'adapter' => 'custom_adapter', + 'service' => 'custom_adapter', ], 'fs_ftp' => [ - 'adapter' => 'ftp', - 'options' => [ + 'ftp' => [ 'host' => 'ftp.example.com', 'username' => 'username', 'password' => 'password', @@ -47,22 +43,19 @@ ], ], 'fs_gcloud' => [ - 'adapter' => 'gcloud', - 'options' => [ + 'gcloud' => [ 'client' => 'gcloud_client_service', 'bucket' => 'bucket_name', 'prefix' => 'optional/path/prefix', ], ], 'fs_lazy' => [ - 'adapter' => 'lazy', - 'options' => [ + 'lazy' => [ 'source' => '%env(LAZY_SOURCE)%', ], ], 'fs_local' => [ - 'adapter' => 'local', - 'options' => [ + 'local' => [ 'directory' => '/tmp/storage', 'lock' => 0, 'skip_links' => false, @@ -79,11 +72,10 @@ ], ], 'fs_memory' => [ - 'adapter' => 'memory', + 'memory' => null, ], 'fs_sftp' => [ - 'adapter' => 'sftp', - 'options' => [ + 'sftp' => [ 'host' => 'example.com', 'port' => 22, 'username' => 'username', @@ -94,15 +86,13 @@ ], ], 'fs_public_url' => [ - 'adapter' => 'local', - 'options' => [ + 'local' => [ 'directory' => '/tmp/storage', ], 'public_url' => 'https://example.org/assets/', ], 'fs_public_urls' => [ - 'adapter' => 'local', - 'options' => [ + 'local' => [ 'directory' => '/tmp/storage', ], 'public_url' => [ @@ -112,16 +102,14 @@ ], ], 'fs_url_generator' => [ - 'adapter' => 'local', - 'options' => [ + 'local' => [ 'directory' => '/tmp/storage', ], 'public_url_generator' => 'flysystem.test.public_url_generator', 'temporary_url_generator' => 'flysystem.test.temporary_url_generator', ], 'fs_read_only' => [ - 'adapter' => 'local', - 'options' => [ + 'local' => [ 'directory' => '/tmp/storage', ], 'read_only' => true,