From 3831bb9b5efd596565fea316f04f16e13dbfeb09 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sat, 9 May 2026 22:42:11 +0200 Subject: [PATCH] refactor: accessing services through service container in symfony bundles --- phpstan.neon | 1 + rector.src.php | 2 +- .../Compiler/BuildFstabsPass.php | 86 ++- .../Factory/AsyncAwsS3FilesystemFactory.php | 53 +- .../Factory/AzureBlobFilesystemFactory.php | 45 +- .../Resources/config/services.php | 2 - .../Tests/Double/StubBlobService.php | 76 ++ .../AwsS3PrivateClientServiceTest.php | 51 ++ .../AzureBlobPrivateClientServiceTest.php | 46 ++ .../Tests/Unit/ConfigurationTest.php | 27 +- .../AsyncAwsS3FilesystemFactoryTest.php | 101 +-- .../AzureBlobFilesystemFactoryTest.php | 98 ++- .../Command/CreateDatabaseCommand.php | 4 +- .../Command/CurrentCommand.php | 4 +- .../PostgreSqlBundle/Command/DiffCommand.php | 4 +- .../Command/DropDatabaseCommand.php | 4 +- .../Command/ExecuteCommand.php | 4 +- .../Command/GenerateCommand.php | 4 +- .../Command/LatestCommand.php | 4 +- .../PostgreSqlBundle/Command/ListCommand.php | 4 +- .../Command/MigrateCommand.php | 4 +- .../Command/RunSqlCommand.php | 4 +- .../Command/StatusCommand.php | 4 +- .../Command/UpToDateCommand.php | 4 +- .../Compiler/CommandLocatorPass.php | 57 ++ .../DependencyInjection/Configuration.php | 197 ----- .../FlowPostgreSqlExtension.php | 505 ------------- .../PostgreSqlBundle/FlowPostgreSqlBundle.php | 675 +++++++++++++++++- .../Resources/config/database.php | 6 +- .../Resources/config/migrations.php | 18 +- .../Tests/Context/ConfigurationContext.php | 35 + .../FlowPostgreSqlExtensionTest.php | 4 +- .../DependencyInjection/ConfigurationTest.php | 126 ++-- 33 files changed, 1284 insertions(+), 975 deletions(-) create mode 100644 src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Double/StubBlobService.php create mode 100644 src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Integration/AwsS3PrivateClientServiceTest.php create mode 100644 src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Integration/AzureBlobPrivateClientServiceTest.php create mode 100644 src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Compiler/CommandLocatorPass.php delete mode 100644 src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Configuration.php delete mode 100644 src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/FlowPostgreSqlExtension.php create mode 100644 src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Context/ConfigurationContext.php diff --git a/phpstan.neon b/phpstan.neon index a948ac95b..106ee71ee 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -113,6 +113,7 @@ parameters: - src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Resources/config/* - src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/* - src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/Command/* + - src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Resources/config/instrumentation/* - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Messenger/TracingMiddleware.php diff --git a/rector.src.php b/rector.src.php index ec17acb33..0511174ef 100644 --- a/rector.src.php +++ b/rector.src.php @@ -34,7 +34,7 @@ ArrayToFirstClassCallableRector::class => [ __DIR__ . '/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php', __DIR__ . '/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/DependencyInjection/Compiler/BuildFstabsPass.php', - __DIR__ . '/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/FlowPostgreSqlExtension.php', + __DIR__ . '/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php', ], ]) ->withCache(__DIR__ . '/var/rector/src') diff --git a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/DependencyInjection/Compiler/BuildFstabsPass.php b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/DependencyInjection/Compiler/BuildFstabsPass.php index 65f25540b..62595ba16 100644 --- a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/DependencyInjection/Compiler/BuildFstabsPass.php +++ b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/DependencyInjection/Compiler/BuildFstabsPass.php @@ -42,6 +42,8 @@ public function process(ContainerBuilder $container) : void $availableTypes = $this->collectAvailableTypes($container); foreach ($fstabs as $fstabName => $fstabConfig) { + $resolvedFilesystems = []; + foreach ($fstabConfig['filesystems'] as $mountName => $entry) { if (!\array_key_exists($entry['type'], $availableTypes)) { throw new LogicException(\sprintf( @@ -52,6 +54,8 @@ public function process(ContainerBuilder $container) : void \implode(', ', \array_keys($availableTypes)), )); } + + $resolvedFilesystems[$mountName] = $this->resolveServiceReferences($entry); } $telemetryReference = $this->buildTelemetryConfigReference($container, $fstabName, $fstabConfig['telemetry'] ?? []); @@ -61,7 +65,7 @@ public function process(ContainerBuilder $container) : void $definition->setArguments([ new Reference(RegisterFilesystemFactoriesPass::REGISTRY_SERVICE_ID), $fstabName, - $fstabConfig['filesystems'], + $resolvedFilesystems, $telemetryReference, ]); $definition->setPublic(false); @@ -143,4 +147,84 @@ private function collectAvailableTypes(ContainerBuilder $container) : array return $types; } + + /** + * @param array&array{type: string} $entry + * + * @return array&array{type: string} + */ + private function resolveAwsS3References(array $entry) : array + { + if (\array_key_exists('client_service_id', $entry) && \is_string($entry['client_service_id']) && $entry['client_service_id'] !== '') { + $entry['client'] = new Reference($entry['client_service_id']); + unset($entry['client_service_id']); + } + + if (\array_key_exists('client', $entry) && \is_array($entry['client'])) { + $client = $entry['client']; + + if (\array_key_exists('http_client_service_id', $client) && \is_string($client['http_client_service_id']) && $client['http_client_service_id'] !== '') { + $client['http_client'] = new Reference($client['http_client_service_id']); + unset($client['http_client_service_id']); + } + + if (\array_key_exists('logger_service_id', $client) && \is_string($client['logger_service_id']) && $client['logger_service_id'] !== '') { + $client['logger'] = new Reference($client['logger_service_id']); + unset($client['logger_service_id']); + } + + $entry['client'] = $client; + } + + return $entry; + } + + /** + * @param array&array{type: string} $entry + * + * @return array&array{type: string} + */ + private function resolveAzureBlobReferences(array $entry) : array + { + if (\array_key_exists('client_service_id', $entry) && \is_string($entry['client_service_id']) && $entry['client_service_id'] !== '') { + $entry['client'] = new Reference($entry['client_service_id']); + unset($entry['client_service_id']); + } + + if (\array_key_exists('client', $entry) && \is_array($entry['client'])) { + $client = $entry['client']; + + $serviceKeyMap = [ + 'http_client_service' => 'http_client', + 'request_factory_service' => 'request_factory', + 'stream_factory_service' => 'stream_factory', + 'logger_service_id' => 'logger', + ]; + + foreach ($serviceKeyMap as $configKey => $resolvedKey) { + if (\array_key_exists($configKey, $client) && \is_string($client[$configKey]) && $client[$configKey] !== '') { + $client[$resolvedKey] = new Reference($client[$configKey]); + unset($client[$configKey]); + } + } + + $entry['client'] = $client; + } + + return $entry; + } + + /** + * @param array&array{type: string} $entry + * + * @return array&array{type: string} + */ + private function resolveServiceReferences(array $entry) : array + { + return match ($entry['type']) { + 'aws_s3' => $this->resolveAwsS3References($entry), + 'azure_blob' => $this->resolveAzureBlobReferences($entry), + default => $entry, + }; + } } diff --git a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AsyncAwsS3FilesystemFactory.php b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AsyncAwsS3FilesystemFactory.php index ffbae760d..a1630668f 100644 --- a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AsyncAwsS3FilesystemFactory.php +++ b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AsyncAwsS3FilesystemFactory.php @@ -10,17 +10,14 @@ use Flow\Bridge\Symfony\FilesystemBundle\Filesystem\FilesystemFactory; use Flow\Filesystem\Bridge\AsyncAWS\Options; use Flow\Filesystem\Filesystem; -use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; final readonly class AsyncAwsS3FilesystemFactory implements FilesystemFactory { - public function __construct(private ContainerInterface $container) - { - } - public function create(string $protocol, array $config) : Filesystem { - $allowed = ['bucket', 'client_service_id', 'client', 'options']; + $allowed = ['bucket', 'client', 'options']; $unknown = \array_diff(\array_keys($config), $allowed); if ($unknown !== []) { @@ -36,27 +33,25 @@ public function create(string $protocol, array $config) : Filesystem } $bucket = $config['bucket']; - $clientServiceId = $config['client_service_id'] ?? null; - $clientConfig = $config['client'] ?? null; - if (($clientServiceId === null) === ($clientConfig === null)) { + if (!\array_key_exists('client', $config) || $config['client'] === null) { throw new InvalidArgumentException('Filesystem factory for backend "aws_s3" requires exactly one of `client_service_id` or `client`.'); } - if (\is_string($clientServiceId) && $clientServiceId !== '') { - $client = $this->container->get($clientServiceId); + $client = $config['client']; - if (!$client instanceof S3Client) { - throw new InvalidArgumentException(\sprintf('Service "%s" is not an instance of %s.', $clientServiceId, S3Client::class)); - } + if ($client instanceof S3Client) { + $resolvedClient = $client; + } elseif (\is_array($client)) { + /** @var array $client */ + $resolvedClient = $this->buildClient($client); } else { - /** @var array $clientConfig */ - $client = $this->buildClient($clientConfig ?? []); + throw new InvalidArgumentException(\sprintf('Filesystem factory for backend "aws_s3" `client` must be an array or %s instance, got %s.', S3Client::class, \get_debug_type($client))); } $options = $this->buildOptions($config['options'] ?? null); - return aws_s3_filesystem($bucket, $client, $options, $protocol); + return aws_s3_filesystem($bucket, $resolvedClient, $options, $protocol); } public function type() : string @@ -69,7 +64,7 @@ public function type() : string */ private function buildClient(array $clientConfig) : S3Client { - $allowed = ['region', 'access_key_id', 'access_key_secret', 'session_token', 'endpoint', 'path_style_endpoint', 'shared_credentials_file', 'shared_config_file', 'profile', 'debug', 'http_client_service_id', 'logger_service_id']; + $allowed = ['region', 'access_key_id', 'access_key_secret', 'session_token', 'endpoint', 'path_style_endpoint', 'shared_credentials_file', 'shared_config_file', 'profile', 'debug', 'http_client', 'logger']; $unknown = \array_diff(\array_keys($clientConfig), $allowed); if ($unknown !== []) { @@ -83,22 +78,20 @@ private function buildClient(array $clientConfig) : S3Client $httpClient = null; $logger = null; - if (\array_key_exists('http_client_service_id', $clientConfig)) { - $id = $clientConfig['http_client_service_id']; - - if (\is_string($id) && $id !== '') { - $httpClient = $this->container->get($id); + if (\array_key_exists('http_client', $clientConfig) && $clientConfig['http_client'] !== null) { + if (!$clientConfig['http_client'] instanceof HttpClientInterface) { + throw new InvalidArgumentException(\sprintf('Filesystem factory for backend "aws_s3" `client.http_client_service_id` must reference a service implementing %s.', HttpClientInterface::class)); } - unset($clientConfig['http_client_service_id']); + $httpClient = $clientConfig['http_client']; + unset($clientConfig['http_client']); } - if (\array_key_exists('logger_service_id', $clientConfig)) { - $id = $clientConfig['logger_service_id']; - - if (\is_string($id) && $id !== '') { - $logger = $this->container->get($id); + if (\array_key_exists('logger', $clientConfig) && $clientConfig['logger'] !== null) { + if (!$clientConfig['logger'] instanceof LoggerInterface) { + throw new InvalidArgumentException(\sprintf('Filesystem factory for backend "aws_s3" `client.logger_service_id` must reference a service implementing %s.', LoggerInterface::class)); } - unset($clientConfig['logger_service_id']); + $logger = $clientConfig['logger']; + unset($clientConfig['logger']); } $keyMap = [ diff --git a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AzureBlobFilesystemFactory.php b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AzureBlobFilesystemFactory.php index 5f239ab5e..8dfda326f 100644 --- a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AzureBlobFilesystemFactory.php +++ b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Filesystem/Factory/AzureBlobFilesystemFactory.php @@ -12,20 +12,15 @@ use Flow\Filesystem\Bridge\Azure\Options; use Flow\Filesystem\Filesystem; use Http\Discovery\{Psr17FactoryDiscovery, Psr18ClientDiscovery}; -use Psr\Container\ContainerInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\{RequestFactoryInterface, StreamFactoryInterface}; use Psr\Log\LoggerInterface; final readonly class AzureBlobFilesystemFactory implements FilesystemFactory { - public function __construct(private ContainerInterface $container) - { - } - public function create(string $protocol, array $config) : Filesystem { - $allowed = ['container', 'client_service_id', 'client', 'options']; + $allowed = ['container', 'client', 'options']; $unknown = \array_diff(\array_keys($config), $allowed); if ($unknown !== []) { @@ -40,23 +35,21 @@ public function create(string $protocol, array $config) : Filesystem throw new InvalidArgumentException('Filesystem factory for backend "azure_blob" requires a non-empty `container` option.'); } - $container = $config['container']; - $clientServiceId = $config['client_service_id'] ?? null; - $clientConfig = $config['client'] ?? null; + $containerName = $config['container']; - if (($clientServiceId === null) === ($clientConfig === null)) { + if (!\array_key_exists('client', $config) || $config['client'] === null) { throw new InvalidArgumentException('Filesystem factory for backend "azure_blob" requires exactly one of `client_service_id` or `client`.'); } - if (\is_string($clientServiceId) && $clientServiceId !== '') { - $blobService = $this->container->get($clientServiceId); + $client = $config['client']; - if (!$blobService instanceof BlobServiceInterface) { - throw new InvalidArgumentException(\sprintf('Service "%s" is not an instance of %s.', $clientServiceId, BlobServiceInterface::class)); - } + if ($client instanceof BlobServiceInterface) { + $blobService = $client; + } elseif (\is_array($client)) { + /** @var array $client */ + $blobService = $this->buildBlobService($containerName, $client); } else { - /** @var array $clientConfig */ - $blobService = $this->buildBlobService($container, $clientConfig ?? []); + throw new InvalidArgumentException(\sprintf('Filesystem factory for backend "azure_blob" `client` must be an array or %s instance, got %s.', BlobServiceInterface::class, \get_debug_type($client))); } $options = $this->buildOptions($config['options'] ?? null); @@ -74,7 +67,7 @@ public function type() : string */ private function buildBlobService(string $containerName, array $clientConfig) : BlobServiceInterface { - $allowed = ['account_name', 'auth', 'url_factory', 'http_client_service', 'request_factory_service', 'stream_factory_service', 'logger_service_id']; + $allowed = ['account_name', 'auth', 'url_factory', 'http_client', 'request_factory', 'stream_factory', 'logger']; $unknown = \array_diff(\array_keys($clientConfig), $allowed); if ($unknown !== []) { @@ -109,10 +102,10 @@ private function buildBlobService(string $containerName, array $clientConfig) : $authFactory = azure_shared_key_authorization_factory($accountName, $clientConfig['auth']['shared_key']); $configuration = azure_blob_service_config($accountName, $containerName); - $httpClient = $this->resolveService($clientConfig['http_client_service'] ?? null, ClientInterface::class) ?? Psr18ClientDiscovery::find(); - $requestFactory = $this->resolveService($clientConfig['request_factory_service'] ?? null, RequestFactoryInterface::class) ?? Psr17FactoryDiscovery::findRequestFactory(); - $streamFactory = $this->resolveService($clientConfig['stream_factory_service'] ?? null, StreamFactoryInterface::class) ?? Psr17FactoryDiscovery::findStreamFactory(); - $logger = $this->resolveService($clientConfig['logger_service_id'] ?? null, LoggerInterface::class); + $httpClient = $this->resolveResolvedService($clientConfig['http_client'] ?? null, ClientInterface::class, 'http_client_service') ?? Psr18ClientDiscovery::find(); + $requestFactory = $this->resolveResolvedService($clientConfig['request_factory'] ?? null, RequestFactoryInterface::class, 'request_factory_service') ?? Psr17FactoryDiscovery::findRequestFactory(); + $streamFactory = $this->resolveResolvedService($clientConfig['stream_factory'] ?? null, StreamFactoryInterface::class, 'stream_factory_service') ?? Psr17FactoryDiscovery::findStreamFactory(); + $logger = $this->resolveResolvedService($clientConfig['logger'] ?? null, LoggerInterface::class, 'logger_service_id'); $httpFactory = azure_http_factory($requestFactory, $streamFactory); @@ -178,16 +171,14 @@ private function buildOptions(mixed $optionsConfig) : Options * * @return null|T */ - private function resolveService(mixed $serviceId, string $expectedClass) : ?object + private function resolveResolvedService(mixed $service, string $expectedClass, string $configKey) : ?object { - if (!\is_string($serviceId) || $serviceId === '') { + if ($service === null) { return null; } - $service = $this->container->get($serviceId); - if (!$service instanceof $expectedClass) { - throw new InvalidArgumentException(\sprintf('Service "%s" is not an instance of %s.', $serviceId, $expectedClass)); + throw new InvalidArgumentException(\sprintf('Filesystem factory for backend "azure_blob" `client.%s` must reference a service implementing %s.', $configKey, $expectedClass)); } return $service; diff --git a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Resources/config/services.php b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Resources/config/services.php index f49d3487c..085697085 100644 --- a/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Resources/config/services.php +++ b/src/bridge/symfony/filesystem-bundle/src/Flow/Bridge/Symfony/FilesystemBundle/Resources/config/services.php @@ -32,7 +32,6 @@ $services ->set('.flow.filesystem.factory.aws_s3', AsyncAwsS3FilesystemFactory::class) ->private() - ->args([service('service_container')]) ->tag('flow.filesystem.factory', ['type' => 'aws_s3']); } @@ -40,7 +39,6 @@ $services ->set('.flow.filesystem.factory.azure_blob', AzureBlobFilesystemFactory::class) ->private() - ->args([service('service_container')]) ->tag('flow.filesystem.factory', ['type' => 'azure_blob']); } diff --git a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Double/StubBlobService.php b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Double/StubBlobService.php new file mode 100644 index 000000000..abb927d83 --- /dev/null +++ b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Double/StubBlobService.php @@ -0,0 +1,76 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestContainerConfigurator(static function (ContainerBuilder $container) : void { + $clientDefinition = new Definition(S3Client::class); + $clientDefinition->setArguments([[ + 'accessKeyId' => 'key', + 'accessKeySecret' => 'secret', + 'region' => 'us-east-1', + ]]); + $clientDefinition->setPublic(false); + + $container->setDefinition('async_aws.client.s3', $clientDefinition); + }); + + $kernel->addTestExtensionConfig('flow_filesystem', [ + 'fstabs' => [ + 'default' => [ + 'filesystems' => [ + 'aws-s3' => [ + 'type' => 'aws_s3', + 'bucket' => 'my-bucket', + 'client_service_id' => 'async_aws.client.s3', + ], + ], + ], + ], + ]); + }, + ]); + + $table = $this->symfonyContext()->getService(FilesystemTable::class . ' $defaultFstab', FilesystemTable::class); + + self::assertInstanceOf(AsyncAWSS3Filesystem::class, $table->for('aws-s3')); + } +} diff --git a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Integration/AzureBlobPrivateClientServiceTest.php b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Integration/AzureBlobPrivateClientServiceTest.php new file mode 100644 index 000000000..4137154b0 --- /dev/null +++ b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Integration/AzureBlobPrivateClientServiceTest.php @@ -0,0 +1,46 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestContainerConfigurator(static function (ContainerBuilder $container) : void { + $clientDefinition = new Definition(StubBlobService::class); + $clientDefinition->setPublic(false); + + $container->setDefinition('azure_storage.blob_service', $clientDefinition); + }); + + $kernel->addTestExtensionConfig('flow_filesystem', [ + 'fstabs' => [ + 'default' => [ + 'filesystems' => [ + 'azure-blob' => [ + 'type' => 'azure_blob', + 'container' => 'my-container', + 'client_service_id' => 'azure_storage.blob_service', + ], + ], + ], + ], + ]); + }, + ]); + + $table = $this->symfonyContext()->getService(FilesystemTable::class . ' $defaultFstab', FilesystemTable::class); + + self::assertInstanceOf(AzureBlobFilesystem::class, $table->for('azure-blob')); + } +} diff --git a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/ConfigurationTest.php b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/ConfigurationTest.php index 79692380b..4ebac00b2 100644 --- a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/ConfigurationTest.php +++ b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/ConfigurationTest.php @@ -4,6 +4,8 @@ namespace Flow\Bridge\Symfony\FilesystemBundle\Tests\Unit; +use AsyncAws\S3\S3Client; +use Flow\Azure\SDK\BlobServiceInterface; use Flow\Bridge\Symfony\FilesystemBundle\Tests\Context\ConfigurationContext; use Flow\Bridge\Symfony\FilesystemBundle\Tests\Double\TelemetryStubFactory; use Flow\Telemetry\Provider\Clock\SystemClock; @@ -393,16 +395,27 @@ public function test_single_fstab_without_explicit_default_resolves_to_only_fsta public function test_valid_hyphenated_mount_names_are_preserved_without_normalization() : void { - $config = $this->context->processConfig([ - 'fstabs' => [ - 'default' => [ - 'filesystems' => [ - 'aws-s3' => ['type' => 'aws_s3', 'bucket' => 'b', 'client_service_id' => 'x'], - 'azure-blob' => ['type' => 'azure_blob', 'container' => 'c', 'client_service_id' => 'y'], + $config = $this->context->processConfig( + [ + 'fstabs' => [ + 'default' => [ + 'filesystems' => [ + 'aws-s3' => ['type' => 'aws_s3', 'bucket' => 'b', 'client_service_id' => 'x'], + 'azure-blob' => ['type' => 'azure_blob', 'container' => 'c', 'client_service_id' => 'y'], + ], ], ], ], - ]); + static function (ContainerBuilder $container) : void { + $container->setDefinition('x', (new Definition(S3Client::class)) + ->setArguments([['accessKeyId' => 'k', 'accessKeySecret' => 's', 'region' => 'us-east-1']]) + ->setPublic(false)); + $container->setDefinition('y', (new Definition(BlobServiceInterface::class)) + ->setSynthetic(true) + ->setPublic(false)); + $container->set('y', self::createStub(BlobServiceInterface::class)); + }, + ); self::assertArrayHasKey('aws-s3', $config['fstabs']['default']['filesystems']); self::assertArrayHasKey('azure-blob', $config['fstabs']['default']['filesystems']); diff --git a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AsyncAwsS3FilesystemFactoryTest.php b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AsyncAwsS3FilesystemFactoryTest.php index 5ebe6b2d3..5303935d2 100644 --- a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AsyncAwsS3FilesystemFactoryTest.php +++ b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AsyncAwsS3FilesystemFactoryTest.php @@ -10,14 +10,13 @@ use Flow\Filesystem\Bridge\AsyncAWS\AsyncAWSS3Filesystem; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpClient\HttpClient; final class AsyncAwsS3FilesystemFactoryTest extends TestCase { public function test_applies_block_size_option() : void { - $filesystem = (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + $filesystem = (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', 'client' => ['region' => 'us-east-1', 'access_key_id' => 'k', 'access_key_secret' => 's'], 'options' => ['block_size' => 6 * 1024 * 1024], @@ -26,45 +25,41 @@ public function test_applies_block_size_option() : void self::assertInstanceOf(AsyncAWSS3Filesystem::class, $filesystem); } - public function test_mode_a_builds_filesystem_from_client_service_id() : void + public function test_mode_a_builds_filesystem_from_resolved_client() : void { - $container = new ContainerBuilder(); - $container->set('app.s3_client', new S3Client(['accessKeyId' => 'k', 'accessKeySecret' => 's', 'region' => 'us-east-1'])); - - $filesystem = (new AsyncAwsS3FilesystemFactory($container))->create('aws-s3', ['bucket' => 'my-bucket', 'client_service_id' => 'app.s3_client']); + $filesystem = (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ + 'bucket' => 'my-bucket', + 'client' => new S3Client(['accessKeyId' => 'k', 'accessKeySecret' => 's', 'region' => 'us-east-1']), + ]); self::assertInstanceOf(AsyncAWSS3Filesystem::class, $filesystem); self::assertSame('aws-s3', $filesystem->mount()->protocol); } - public function test_mode_b_builds_client_from_inline_config() : void + public function test_mode_b_accepts_resolved_http_client_and_logger() : void { - $filesystem = (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ - 'bucket' => 'my-bucket', + $filesystem = (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ + 'bucket' => 'b', 'client' => [ - 'region' => 'eu-west-1', - 'access_key_id' => 'key', - 'access_key_secret' => 'secret', + 'region' => 'us-east-1', + 'access_key_id' => 'k', + 'access_key_secret' => 's', + 'http_client' => HttpClient::create(), + 'logger' => new NullLogger(), ], ]); self::assertInstanceOf(AsyncAWSS3Filesystem::class, $filesystem); } - public function test_mode_b_resolves_http_client_and_logger_from_container() : void + public function test_mode_b_builds_client_from_inline_config() : void { - $container = new ContainerBuilder(); - $container->set('app.http_client', HttpClient::create()); - $container->set('app.logger', new NullLogger()); - - $filesystem = (new AsyncAwsS3FilesystemFactory($container))->create('aws-s3', [ - 'bucket' => 'b', + $filesystem = (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ + 'bucket' => 'my-bucket', 'client' => [ - 'region' => 'us-east-1', - 'access_key_id' => 'k', - 'access_key_secret' => 's', - 'http_client_service_id' => 'app.http_client', - 'logger_service_id' => 'app.logger', + 'region' => 'eu-west-1', + 'access_key_id' => 'key', + 'access_key_secret' => 'secret', ], ]); @@ -73,7 +68,7 @@ public function test_mode_b_resolves_http_client_and_logger_from_container() : v public function test_mount_protocol_is_propagated_to_filesystem() : void { - $filesystem = (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('warehouse', [ + $filesystem = (new AsyncAwsS3FilesystemFactory())->create('warehouse', [ 'bucket' => 'b', 'client' => ['region' => 'us-east-1', 'access_key_id' => 'k', 'access_key_secret' => 's'], ]); @@ -86,7 +81,7 @@ public function test_throws_on_invalid_block_size_type() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`options.block_size` must be an integer'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', 'client' => ['region' => 'us-east-1'], 'options' => ['block_size' => 'not-an-int'], @@ -98,7 +93,7 @@ public function test_throws_on_missing_bucket() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('non-empty `bucket`'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', ['client' => []]); + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', ['client' => []]); } public function test_throws_on_unknown_client_keys() : void @@ -106,7 +101,7 @@ public function test_throws_on_unknown_client_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`client` contains unknown keys: [nope]'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', 'client' => ['nope' => 'x'], ]); @@ -117,7 +112,7 @@ public function test_throws_on_unknown_options_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`options` contains unknown keys: [nope]'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', 'client' => ['region' => 'us-east-1'], 'options' => ['nope' => 1], @@ -129,42 +124,58 @@ public function test_throws_on_unknown_top_level_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('received unknown keys: [container]'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', 'client' => ['region' => 'us-east-1'], 'container' => 'nope', ]); } - public function test_throws_when_both_client_modes_supplied() : void + public function test_throws_when_client_is_invalid_type() : void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('exactly one of `client_service_id` or `client`'); + $this->expectExceptionMessage('`client` must be an array or AsyncAws\\S3\\S3Client instance'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', - 'client_service_id' => 'some.service', - 'client' => ['region' => 'us-east-1'], + 'client' => new \stdClass(), ]); } - public function test_throws_when_client_service_not_s3_client() : void + public function test_throws_when_http_client_is_wrong_type() : void { - $container = new ContainerBuilder(); - $container->set('app.wrong', new \stdClass()); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('`client.http_client_service_id` must reference a service implementing'); + + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ + 'bucket' => 'b', + 'client' => [ + 'region' => 'us-east-1', + 'http_client' => new \stdClass(), + ], + ]); + } + public function test_throws_when_logger_is_wrong_type() : void + { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('is not an instance of'); + $this->expectExceptionMessage('`client.logger_service_id` must reference a service implementing'); - (new AsyncAwsS3FilesystemFactory($container))->create('aws-s3', ['bucket' => 'b', 'client_service_id' => 'app.wrong']); + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ + 'bucket' => 'b', + 'client' => [ + 'region' => 'us-east-1', + 'logger' => new \stdClass(), + ], + ]); } - public function test_throws_when_no_client_mode_supplied() : void + public function test_throws_when_no_client_supplied() : void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('exactly one of'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', ['bucket' => 'b']); + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', ['bucket' => 'b']); } public function test_throws_when_options_is_not_array() : void @@ -172,7 +183,7 @@ public function test_throws_when_options_is_not_array() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`options` must be an array'); - (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->create('aws-s3', [ + (new AsyncAwsS3FilesystemFactory())->create('aws-s3', [ 'bucket' => 'b', 'client' => ['region' => 'us-east-1'], 'options' => 'not-an-array', @@ -181,6 +192,6 @@ public function test_throws_when_options_is_not_array() : void public function test_type_returns_aws_s3() : void { - self::assertSame('aws_s3', (new AsyncAwsS3FilesystemFactory(new ContainerBuilder()))->type()); + self::assertSame('aws_s3', (new AsyncAwsS3FilesystemFactory())->type()); } } diff --git a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AzureBlobFilesystemFactoryTest.php b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AzureBlobFilesystemFactoryTest.php index 8da0c59b8..896c4279f 100644 --- a/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AzureBlobFilesystemFactoryTest.php +++ b/src/bridge/symfony/filesystem-bundle/tests/Flow/Bridge/Symfony/FilesystemBundle/Tests/Unit/Filesystem/Factory/AzureBlobFilesystemFactoryTest.php @@ -11,14 +11,13 @@ use Nyholm\Psr7\Factory\Psr17Factory; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpClient\Psr18Client; final class AzureBlobFilesystemFactoryTest extends TestCase { public function test_applies_block_size_option() : void { - $filesystem = (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + $filesystem = (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => \base64_encode('k')]], 'options' => ['block_size' => 8 * 1024 * 1024, 'list_blob_max_results' => 100], @@ -27,50 +26,41 @@ public function test_applies_block_size_option() : void self::assertInstanceOf(AzureBlobFilesystem::class, $filesystem); } - public function test_mode_a_builds_filesystem_from_client_service_id() : void + public function test_mode_a_builds_filesystem_from_resolved_client() : void { - $container = new ContainerBuilder(); - $container->set('app.blob_service', self::createStub(BlobServiceInterface::class)); - - $filesystem = (new AzureBlobFilesystemFactory($container))->create('azure-blob', [ + $filesystem = (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'my-container', - 'client_service_id' => 'app.blob_service', + 'client' => self::createStub(BlobServiceInterface::class), ]); self::assertInstanceOf(AzureBlobFilesystem::class, $filesystem); self::assertSame('azure-blob', $filesystem->mount()->protocol); } - public function test_mode_b_builds_blob_service_from_inline_shared_key() : void + public function test_mode_b_accepts_resolved_http_and_factories() : void { - $filesystem = (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ - 'container' => 'my-container', + $filesystem = (new AzureBlobFilesystemFactory())->create('azure-blob', [ + 'container' => 'c', 'client' => [ - 'account_name' => 'myaccount', - 'auth' => ['shared_key' => \base64_encode('secret-key')], + 'account_name' => 'a', + 'auth' => ['shared_key' => \base64_encode('k')], + 'http_client' => new Psr18Client(), + 'request_factory' => new Psr17Factory(), + 'stream_factory' => new Psr17Factory(), + 'logger' => new NullLogger(), ], ]); self::assertInstanceOf(AzureBlobFilesystem::class, $filesystem); } - public function test_mode_b_resolves_optional_http_and_factories_from_container() : void + public function test_mode_b_builds_blob_service_from_inline_shared_key() : void { - $container = new ContainerBuilder(); - $container->set('app.http_client', new Psr18Client()); - $container->set('app.request_factory', new Psr17Factory()); - $container->set('app.stream_factory', new Psr17Factory()); - $container->set('app.logger', new NullLogger()); - - $filesystem = (new AzureBlobFilesystemFactory($container))->create('azure-blob', [ - 'container' => 'c', + $filesystem = (new AzureBlobFilesystemFactory())->create('azure-blob', [ + 'container' => 'my-container', 'client' => [ - 'account_name' => 'a', - 'auth' => ['shared_key' => \base64_encode('k')], - 'http_client_service' => 'app.http_client', - 'request_factory_service' => 'app.request_factory', - 'stream_factory_service' => 'app.stream_factory', - 'logger_service_id' => 'app.logger', + 'account_name' => 'myaccount', + 'auth' => ['shared_key' => \base64_encode('secret-key')], ], ]); @@ -79,7 +69,7 @@ public function test_mode_b_resolves_optional_http_and_factories_from_container( public function test_mode_b_uses_azurite_url_factory_when_host_set() : void { - $filesystem = (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + $filesystem = (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'my-container', 'client' => [ 'account_name' => 'devstoreaccount1', @@ -96,7 +86,7 @@ public function test_throws_on_invalid_block_size_type() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('block_size'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => 'k']], 'options' => ['block_size' => 'nope'], @@ -108,7 +98,7 @@ public function test_throws_on_invalid_list_blob_max_results_type() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('list_blob_max_results'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => 'k']], 'options' => ['list_blob_max_results' => 'nope'], @@ -120,7 +110,7 @@ public function test_throws_on_missing_container() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('non-empty `container`'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', ['client' => []]); + (new AzureBlobFilesystemFactory())->create('azure-blob', ['client' => []]); } public function test_throws_on_unknown_auth_keys() : void @@ -128,7 +118,7 @@ public function test_throws_on_unknown_auth_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`client.auth` contains unknown keys: [sas_token]'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => \base64_encode('k'), 'sas_token' => 'x']], ]); @@ -139,7 +129,7 @@ public function test_throws_on_unknown_client_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`client` contains unknown keys: [nope]'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => \base64_encode('k')], 'nope' => 1], ]); @@ -150,7 +140,7 @@ public function test_throws_on_unknown_options_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`options` contains unknown keys: [nope]'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => \base64_encode('k')]], 'options' => ['nope' => 1], @@ -162,36 +152,36 @@ public function test_throws_on_unknown_top_level_keys() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('received unknown keys: [bucket]'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => \base64_encode('k')]], 'bucket' => 'no', ]); } - public function test_throws_when_both_client_modes_supplied() : void + public function test_throws_when_client_is_invalid_type() : void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('exactly one of'); + $this->expectExceptionMessage('`client` must be an array or Flow\\Azure\\SDK\\BlobServiceInterface instance'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', - 'client_service_id' => 'x', - 'client' => ['account_name' => 'a'], + 'client' => new \stdClass(), ]); } - public function test_throws_when_client_service_not_blob_service() : void + public function test_throws_when_http_client_is_wrong_type() : void { - $container = new ContainerBuilder(); - $container->set('app.wrong', new \stdClass()); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('is not an instance of'); + $this->expectExceptionMessage('`client.http_client_service` must reference a service implementing'); - (new AzureBlobFilesystemFactory($container))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', - 'client_service_id' => 'app.wrong', + 'client' => [ + 'account_name' => 'a', + 'auth' => ['shared_key' => \base64_encode('k')], + 'http_client' => new \stdClass(), + ], ]); } @@ -200,7 +190,7 @@ public function test_throws_when_mode_b_missing_account_name() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`client.account_name`'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['auth' => ['shared_key' => 'k']], ]); @@ -211,18 +201,18 @@ public function test_throws_when_mode_b_missing_shared_key() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`client.auth.shared_key`'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a'], ]); } - public function test_throws_when_no_client_mode_supplied() : void + public function test_throws_when_no_client_supplied() : void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('exactly one of'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', ['container' => 'c']); + (new AzureBlobFilesystemFactory())->create('azure-blob', ['container' => 'c']); } public function test_throws_when_options_is_not_array() : void @@ -230,7 +220,7 @@ public function test_throws_when_options_is_not_array() : void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('`options` must be an array'); - (new AzureBlobFilesystemFactory(new ContainerBuilder()))->create('azure-blob', [ + (new AzureBlobFilesystemFactory())->create('azure-blob', [ 'container' => 'c', 'client' => ['account_name' => 'a', 'auth' => ['shared_key' => 'k']], 'options' => 'nope', @@ -239,6 +229,6 @@ public function test_throws_when_options_is_not_array() : void public function test_type_returns_azure_blob() : void { - self::assertSame('azure_blob', (new AzureBlobFilesystemFactory(new ContainerBuilder()))->type()); + self::assertSame('azure_blob', (new AzureBlobFilesystemFactory())->type()); } } diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CreateDatabaseCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CreateDatabaseCommand.php index 67abb9a49..c869bae13 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CreateDatabaseCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CreateDatabaseCommand.php @@ -9,12 +9,12 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Client\ConnectionParameters; use Flow\PostgreSql\Client\Infrastructure\PgSql\PgSqlClient; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'flow:database:create', description: 'Create the configured database')] final class CreateDatabaseCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CurrentCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CurrentCommand.php index e96151504..be5767172 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CurrentCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/CurrentCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\Store\MigrationStore; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:current', description: 'Output the current migration version')] final class CurrentCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DiffCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DiffCommand.php index 57eeef9d7..7a324a51b 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DiffCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DiffCommand.php @@ -9,13 +9,13 @@ use Flow\PostgreSql\Migrations\Configuration as MigrationsConfiguration; use Flow\PostgreSql\Migrations\Exception\MigrationException; use Flow\PostgreSql\Migrations\Generator\DiffMigrationGenerator; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:diff', description: 'Generate a migration by comparing the current database to the catalog')] final class DiffCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DropDatabaseCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DropDatabaseCommand.php index 5522fe039..2184df3e4 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DropDatabaseCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/DropDatabaseCommand.php @@ -9,12 +9,12 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Client\ConnectionParameters; use Flow\PostgreSql\Client\Infrastructure\PgSql\PgSqlClient; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'flow:database:drop', description: 'Drop the configured database')] final class DropDatabaseCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ExecuteCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ExecuteCommand.php index 84da03843..8e2001ba9 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ExecuteCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ExecuteCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\{Direction, Migrator, Version}; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:execute', description: 'Execute a single migration')] final class ExecuteCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/GenerateCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/GenerateCommand.php index 6380e70f5..0a718cd04 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/GenerateCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/GenerateCommand.php @@ -8,13 +8,13 @@ use Flow\PostgreSql\Migrations\Configuration as MigrationsConfiguration; use Flow\PostgreSql\Migrations\Generator\MigrationGenerator; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:generate', description: 'Generate a blank migration class')] final class GenerateCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/LatestCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/LatestCommand.php index 820eb56f8..c1e1e55d0 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/LatestCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/LatestCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\{MigrationState, Migrator}; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:latest', description: 'Output the latest available migration version')] final class LatestCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ListCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ListCommand.php index 5c7e264fa..11b53740c 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ListCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/ListCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\{MigrationState, Migrator}; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:list', description: 'List all available migrations')] final class ListCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/MigrateCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/MigrateCommand.php index 16c753c9f..2dc4ebd51 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/MigrateCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/MigrateCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\{Configuration as MigrationsConfiguration, Direction, MigrationState, Migrator, VersionResolver}; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:migrate', description: 'Execute migrations')] final class MigrateCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/RunSqlCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/RunSqlCommand.php index b7c06f473..b59b065a7 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/RunSqlCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/RunSqlCommand.php @@ -6,12 +6,12 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Client\Client; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption}; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'flow:sql:run', description: 'Execute SQL directly on the database')] final class RunSqlCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/StatusCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/StatusCommand.php index fb87abc23..3107647d8 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/StatusCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/StatusCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\Migrator; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:status', description: 'View the migration status')] final class StatusCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/UpToDateCommand.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/UpToDateCommand.php index 16a91fb70..2ed9ffb0f 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/UpToDateCommand.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/UpToDateCommand.php @@ -7,13 +7,13 @@ use function Flow\Types\DSL\{type_instance_of, type_string}; use Flow\PostgreSql\Migrations\Migrator; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface, InputOption}; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'flow:migrations:up-to-date', description: 'Check if all migrations have been executed')] final class UpToDateCommand extends Command diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Compiler/CommandLocatorPass.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Compiler/CommandLocatorPass.php new file mode 100644 index 000000000..9fd1d4d19 --- /dev/null +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Compiler/CommandLocatorPass.php @@ -0,0 +1,57 @@ +hasParameter('flow.postgresql.connections')) { + return; + } + + /** @var list $connections */ + $connections = $container->getParameter('flow.postgresql.connections'); + + $services = []; + + foreach ($connections as $connection) { + foreach (['client', 'connection_parameters'] as $kind) { + $serviceId = "flow.postgresql.{$connection}.{$kind}"; + + if ($container->hasDefinition($serviceId)) { + $services[$serviceId] = new ServiceClosureArgument(new Reference($serviceId)); + } + } + } + + if ($container->hasParameter('flow.postgresql.migrations.connections')) { + /** @var list $migrationConnections */ + $migrationConnections = $container->getParameter('flow.postgresql.migrations.connections'); + + foreach ($migrationConnections as $connection) { + foreach (['configuration', 'migrator', 'store', 'version_resolver', 'generator', 'diff_generator'] as $kind) { + $serviceId = "flow.postgresql.{$connection}.migrations.{$kind}"; + + if ($container->hasDefinition($serviceId)) { + $services[$serviceId] = new ServiceClosureArgument(new Reference($serviceId)); + } + } + } + } + + $locatorDef = new Definition(ServiceLocator::class, [$services]); + $locatorDef->addTag('container.service_locator'); + $locatorDef->setPublic(false); + + $container->setDefinition(self::LOCATOR_SERVICE_ID, $locatorDef); + } +} diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Configuration.php deleted file mode 100644 index 96208d1c5..000000000 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/Configuration.php +++ /dev/null @@ -1,197 +0,0 @@ -getRootNode() - ->children() - ->arrayNode('connections') - ->requiresAtLeastOneElement() - ->useAttributeAsKey('name') - ->arrayPrototype() - ->children() - ->scalarNode('dsn') - ->isRequired() - ->cannotBeEmpty() - ->info('PostgreSQL connection DSN (e.g. postgresql://user:pass@localhost:5432/dbname)') - ->end() - ->booleanNode('test_transaction_rollback') - ->defaultFalse() - ->info('When true and flow-php/phpunit-postgresql-bridge is installed, wraps the connection with StaticClient for transaction rollback in tests.') - ->end() - ->arrayNode('context') - ->info('Extra key/value pairs merged into the Flow\\PostgreSql\\Client\\Context for every mapper call. Values can be literals, @service_id references, %parameter% placeholders, or %env(VAR)% expressions.') - ->useAttributeAsKey('name') - ->variablePrototype()->end() - ->end() - ->arrayNode('telemetry') - ->children() - ->scalarNode('service_id') - ->isRequired() - ->cannotBeEmpty() - ->info('Service ID of the Telemetry instance (e.g. flow.telemetry)') - ->end() - ->scalarNode('clock_service_id') - ->defaultNull() - ->info('Service ID of a PSR ClockInterface implementation. Default: creates SystemClock') - ->end() - ->booleanNode('trace_queries')->defaultTrue()->end() - ->booleanNode('trace_transactions')->defaultTrue()->end() - ->booleanNode('collect_metrics')->defaultTrue()->end() - ->booleanNode('log_queries')->defaultFalse()->end() - ->integerNode('max_query_length')->defaultValue(1000)->min(0)->end() - ->booleanNode('include_parameters')->defaultFalse()->end() - ->integerNode('max_parameters')->defaultValue(10)->min(0)->end() - ->integerNode('max_parameter_length')->defaultValue(100)->min(0)->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('messenger') - ->info('Enables the Symfony Messenger PostgreSQL transport. Requires flow-php/symfony-postgresql-messenger-bridge.') - ->canBeEnabled() - ->children() - ->scalarNode('table_name') - ->defaultValue('messenger_messages') - ->cannotBeEmpty() - ->info('Name of the table that stores messenger messages.') - ->end() - ->scalarNode('schema') - ->defaultValue('public') - ->cannotBeEmpty() - ->info('Schema that owns the messenger table.') - ->end() - ->end() - ->end() - ->arrayNode('cache') - ->info('Defines PostgreSQL-backed Symfony Cache pools. Requires flow-php/symfony-postgresql-cache-bridge.') - ->addDefaultsIfNotSet() - ->children() - ->arrayNode('pools') - ->info('Named cache pools. Each becomes a service "flow.postgresql.cache.pool." usable as adapter: in framework.cache.pools.') - ->useAttributeAsKey('name') - ->arrayPrototype() - ->children() - ->scalarNode('connection') - ->defaultNull() - ->info('flow_postgresql.connections key to use. Defaults to the first declared connection when null.') - ->end() - ->scalarNode('table_name')->defaultValue('cache_items')->cannotBeEmpty()->end() - ->scalarNode('schema')->defaultValue('public')->cannotBeEmpty()->end() - ->scalarNode('id_col')->defaultValue('item_id')->cannotBeEmpty()->end() - ->scalarNode('data_col')->defaultValue('item_data')->cannotBeEmpty()->end() - ->scalarNode('lifetime_col')->defaultValue('item_lifetime')->cannotBeEmpty()->end() - ->scalarNode('time_col')->defaultValue('item_time')->cannotBeEmpty()->end() - ->scalarNode('namespace') - ->defaultValue('') - ->info('Cache pool namespace. Allowed chars: -+.A-Za-z0-9') - ->end() - ->integerNode('default_lifetime')->defaultValue(0)->min(0)->end() - ->scalarNode('marshaller_service_id')->defaultNull()->end() - ->booleanNode('share_connection') - ->defaultFalse() - ->info('When true, the pool reuses the named connection\'s Client instead of opening its own pg_connect. Off by default.') - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('session') - ->info('Defines the PostgreSQL-backed Symfony Session handler. Requires flow-php/symfony-postgresql-session-bridge.') - ->canBeEnabled() - ->children() - ->scalarNode('connection') - ->defaultNull() - ->info('flow_postgresql.connections key to use. Defaults to the first declared connection when null.') - ->end() - ->scalarNode('table_name')->defaultValue('sessions')->cannotBeEmpty()->end() - ->scalarNode('schema')->defaultValue('public')->cannotBeEmpty()->end() - ->scalarNode('id_col')->defaultValue('sess_id')->cannotBeEmpty()->end() - ->scalarNode('data_col')->defaultValue('sess_data')->cannotBeEmpty()->end() - ->scalarNode('lifetime_col')->defaultValue('sess_lifetime')->cannotBeEmpty()->end() - ->scalarNode('time_col')->defaultValue('sess_time')->cannotBeEmpty()->end() - ->enumNode('lock_mode') - ->values(['none', 'advisory', 'transactional']) - ->defaultValue('transactional') - ->info('Locking strategy. "transactional" uses SELECT FOR UPDATE; "advisory" uses pg_advisory_lock; "none" disables locking.') - ->end() - ->integerNode('ttl') - ->defaultNull() - ->min(0) - ->info('Session lifetime in seconds. When null, falls back to ini "session.gc_maxlifetime".') - ->end() - ->booleanNode('share_connection') - ->defaultFalse() - ->info('When true, the handler reuses the named connection\'s Client instead of opening its own pg_connect. Off by default.') - ->end() - ->end() - ->end() - ->arrayNode('migrations') - ->canBeEnabled() - ->children() - ->scalarNode('directory') - ->defaultValue('%kernel.project_dir%/migrations') - ->cannotBeEmpty() - ->end() - ->scalarNode('namespace') - ->defaultValue('App\\Migrations') - ->cannotBeEmpty() - ->end() - ->scalarNode('table_name') - ->defaultValue('flow_migrations') - ->cannotBeEmpty() - ->end() - ->scalarNode('table_schema') - ->defaultValue('public') - ->cannotBeEmpty() - ->end() - ->scalarNode('migration_file_name') - ->defaultValue('migration.php') - ->cannotBeEmpty() - ->end() - ->scalarNode('rollback_file_name') - ->defaultValue('rollback.php') - ->cannotBeEmpty() - ->end() - ->booleanNode('all_or_nothing') - ->defaultFalse() - ->info('Wrap all migrations in a single transaction (default: false)') - ->end() - ->booleanNode('generate_rollback') - ->defaultTrue() - ->info('Generate rollback files when creating migrations (default: true)') - ->end() - ->end() - ->end() - ->arrayNode('catalog_providers') - ->info('List of catalog providers to merge into the target schema. Each entry must have either "catalog_provider_id" or "catalog".') - ->arrayPrototype() - ->children() - ->scalarNode('catalog_provider_id') - ->defaultNull() - ->info('Service ID of Flow\\PostgreSql\\Schema\\CatalogProvider') - ->end() - ->variableNode('catalog') - ->defaultNull() - ->info('Inline catalog definition matching Catalog::fromArray() shape') - ->end() - ->end() - ->end() - ->end() - ->end(); - - return $treeBuilder; - } -} diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/FlowPostgreSqlExtension.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/FlowPostgreSqlExtension.php deleted file mode 100644 index 9eb2c4c34..000000000 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/DependencyInjection/FlowPostgreSqlExtension.php +++ /dev/null @@ -1,505 +0,0 @@ - $configs - */ - public function load(array $configs, ContainerBuilder $container) : void - { - $configuration = new Configuration(); - - /** @var array{connections: array, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool}, catalog_providers: list}>} $config */ - $config = $this->processConfiguration($configuration, $configs); - - $isFirst = true; - $connectionNames = \array_keys($config['connections']); - - foreach ($config['connections'] as $name => $connectionConfig) { - $this->registerConnection($name, $connectionConfig, $container, $isFirst); - $isFirst = false; - } - - $this->registerCatalogProviders($config['catalog_providers'] ?? [], $container); - - $container->setParameter('flow.postgresql.connections', $connectionNames); - $container->setParameter('flow.postgresql.default_connection', $connectionNames[0]); - - $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load('database.php'); - $loader->load('format.php'); - - if ($config['migrations']['enabled']) { - $isFirst = true; - - foreach ($connectionNames as $name) { - $this->registerMigrations($name, $config['migrations'], $container, $isFirst); - $isFirst = false; - } - - $container->setParameter('flow.postgresql.migrations.connections', $connectionNames); - $container->setParameter('flow.postgresql.migrations.default_connection', $connectionNames[0]); - - $loader->load('migrations.php'); - } - - $this->registerMessenger($config['messenger'], $connectionNames, $container); - $this->registerCache($config['cache'] ?? [], $connectionNames, $container); - $this->registerSession($config['session'] ?? [], $connectionNames, $container); - } - - /** - * @param array{pools?: array} $cacheConfig - * @param list $connectionNames - */ - private function registerCache(array $cacheConfig, array $connectionNames, ContainerBuilder $container) : void - { - if (!\class_exists(FlowPostgreSqlCacheAdapter::class)) { - return; - } - - $pools = $cacheConfig['pools'] ?? []; - - if ($pools === []) { - return; - } - - foreach ($pools as $name => $poolConfig) { - $this->registerCachePool((string) $name, $poolConfig, $connectionNames, $container); - } - } - - /** - * @param array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool} $poolConfig - * @param list $connectionNames - */ - private function registerCachePool(string $name, array $poolConfig, array $connectionNames, ContainerBuilder $container) : void - { - $connectionName = $poolConfig['connection'] ?? $connectionNames[0]; - - if (!\in_array($connectionName, $connectionNames, true)) { - throw new \LogicException(\sprintf( - 'Cache pool "%s" references unknown connection "%s". Declared connections: %s', - $name, - $connectionName, - \implode(', ', $connectionNames), - )); - } - - $catalogDef = new Definition(CacheCatalogProvider::class, [ - $poolConfig['table_name'], - $poolConfig['schema'], - $poolConfig['id_col'], - $poolConfig['data_col'], - $poolConfig['lifetime_col'], - $poolConfig['time_col'], - ]); - $catalogDef->addTag('flow.postgresql.catalog_provider'); - $container->setDefinition("flow.postgresql.cache.pool.{$name}.catalog_provider", $catalogDef); - - $connectionRef = ($poolConfig['share_connection'] ?? false) - ? new Reference("flow.postgresql.{$connectionName}.client") - : new Reference("flow.postgresql.{$connectionName}.connection_parameters"); - - $adapterDef = new Definition(FlowPostgreSqlCacheAdapter::class, [ - $connectionRef, - $poolConfig['namespace'], - $poolConfig['default_lifetime'], - [ - 'db_table' => $poolConfig['table_name'], - 'db_schema' => $poolConfig['schema'], - 'db_id_col' => $poolConfig['id_col'], - 'db_data_col' => $poolConfig['data_col'], - 'db_lifetime_col' => $poolConfig['lifetime_col'], - 'db_time_col' => $poolConfig['time_col'], - ], - $poolConfig['marshaller_service_id'] !== null - ? new Reference($poolConfig['marshaller_service_id']) - : null, - ]); - $adapterDef->setPublic(true); - $container->setDefinition("flow.postgresql.cache.pool.{$name}", $adapterDef); - } - - /** - * @param list}> $catalogProviders - */ - private function registerCatalogProviders(array $catalogProviders, ContainerBuilder $container) : void - { - $configProviderServiceIds = []; - - foreach ($catalogProviders as $i => $providerConfig) { - if (\array_key_exists('catalog', $providerConfig) && $providerConfig['catalog'] !== null) { - $providerDef = new Definition(ArrayCatalogProvider::class, [$providerConfig['catalog']]); - $providerDef->addTag('flow.postgresql.catalog_provider'); - $container->setDefinition("flow.postgresql.catalog_provider.{$i}", $providerDef); - } elseif (\array_key_exists('catalog_provider_id', $providerConfig) && $providerConfig['catalog_provider_id'] !== null) { - $configProviderServiceIds[] = type_string()->assert($providerConfig['catalog_provider_id']); - } - } - - if ($configProviderServiceIds !== []) { - $container->setParameter('flow.postgresql.catalog_provider.service_ids', $configProviderServiceIds); - } - } - - /** - * @param array{dsn: string, test_transaction_rollback: bool, context?: array, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}} $connectionConfig - */ - private function registerConnection(string $name, array $connectionConfig, ContainerBuilder $container, bool $isFirst) : void - { - $parserDef = new Definition(DsnParser::class); - $container->setDefinition("flow.postgresql.{$name}.dsn_parser", $parserDef); - - $paramsDef = new Definition(ConnectionParameters::class); - $paramsDef->setFactory([new Reference("flow.postgresql.{$name}.dsn_parser"), 'parse']); - $paramsDef->setArguments([$connectionConfig['dsn']]); - $container->setDefinition("flow.postgresql.{$name}.connection_parameters", $paramsDef); - - $paramsDef->setPublic(true); - - $clientArguments = [new Reference("flow.postgresql.{$name}.connection_parameters")]; - - if (\array_key_exists('context', $connectionConfig) && $connectionConfig['context'] !== []) { - $contextDef = new Definition(Context::class, [ - null, - $connectionConfig['context'], - ]); - $container->setDefinition("flow.postgresql.{$name}.context", $contextDef); - - $clientArguments[] = null; - $clientArguments[] = new Reference("flow.postgresql.{$name}.context"); - } - - $clientDef = new Definition(PgSqlClient::class); - $clientDef->setFactory([PgSqlClient::class, 'connect']); - $clientDef->setArguments($clientArguments); - $clientDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.client", $clientDef); - - if ($connectionConfig['test_transaction_rollback']) { - $this->registerStaticConnection($name, $container); - } - - if (\array_key_exists('telemetry', $connectionConfig)) { - $this->registerTelemetry($name, $connectionConfig['telemetry'], $container); - } - - if ($isFirst) { - $container->setAlias(Client::class, "flow.postgresql.{$name}.client"); - $container->setAlias(ConnectionParameters::class, "flow.postgresql.{$name}.connection_parameters"); - } - } - - /** - * @param array{enabled: bool, table_name: string, schema: string} $messengerConfig - * @param list $connectionNames - */ - private function registerMessenger(array $messengerConfig, array $connectionNames, ContainerBuilder $container) : void - { - if (!\class_exists(FlowPostgreSqlTransportFactory::class)) { - return; - } - - if (!$messengerConfig['enabled']) { - return; - } - - $catalogProviderDef = new Definition(MessengerCatalogProvider::class, [ - $messengerConfig['table_name'], - $messengerConfig['schema'], - ]); - $catalogProviderDef->addTag('flow.postgresql.catalog_provider'); - $container->setDefinition('flow.postgresql.messenger.catalog_provider', $catalogProviderDef); - - $locatorServices = []; - - foreach ($connectionNames as $name) { - $locatorServices[$name] = new ServiceClosureArgument(new Reference("flow.postgresql.{$name}.client")); - } - - $locatorDef = new Definition(ServiceLocator::class, [$locatorServices]); - $locatorDef->addTag('container.service_locator'); - $container->setDefinition('flow.postgresql.messenger.client_locator', $locatorDef); - - $factoryDef = new Definition(FlowPostgreSqlTransportFactory::class, [ - new Reference('flow.postgresql.messenger.client_locator'), - ]); - $factoryDef->addTag('messenger.transport_factory'); - $container->setDefinition('flow.postgresql.messenger.transport_factory', $factoryDef); - } - - /** - * @param array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool} $mc - */ - private function registerMigrations(string $name, array $mc, ContainerBuilder $container, bool $isFirst) : void - { - - $catalogProviderRef = new Reference('flow.postgresql.catalog_provider'); - - $configDef = new Definition(MigrationsConfiguration::class, [ - new Reference("flow.postgresql.{$name}.client"), - $catalogProviderRef, - $mc['directory'], - $mc['namespace'], - $mc['table_name'], - $mc['table_schema'], - $mc['migration_file_name'], - $mc['rollback_file_name'], - $mc['all_or_nothing'], - $mc['generate_rollback'], - ]); - $configDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.configuration", $configDef); - - $fsDef = new Definition(NativeLocalFilesystem::class); - $container->setDefinition("flow.postgresql.{$name}.migrations.filesystem", $fsDef); - - $pathDef = new Definition(Path::class); - $pathDef->setFactory([Path::class, 'from']); - $pathDef->setArguments([$mc['directory']]); - $container->setDefinition("flow.postgresql.{$name}.migrations.path", $pathDef); - - $repoDef = new Definition(FilesystemMigrationRepository::class, [ - new Reference("flow.postgresql.{$name}.migrations.filesystem"), - new Reference("flow.postgresql.{$name}.migrations.path"), - new Reference("flow.postgresql.{$name}.migrations.configuration"), - ]); - $repoDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.repository", $repoDef); - - $factoryDef = new Definition(MigrationsFactory::class, [ - new Reference("flow.postgresql.{$name}.migrations.configuration"), - new Reference("flow.postgresql.{$name}.migrations.repository"), - ]); - $container->setDefinition("flow.postgresql.{$name}.migrations.factory", $factoryDef); - - $migratorDef = new Definition(Migrator::class); - $migratorDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createMigrator']); - $migratorDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.migrator", $migratorDef); - - $storeDef = new Definition(MigrationStore::class); - $storeDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createStore']); - $storeDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.store", $storeDef); - - $executorDef = new Definition(MigrationExecutor::class); - $executorDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createExecutor']); - $container->setDefinition("flow.postgresql.{$name}.migrations.executor", $executorDef); - - $resolverDef = new Definition(VersionResolver::class); - $resolverDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createVersionResolver']); - $resolverDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.version_resolver", $resolverDef); - - $twigLoaderDef = new Definition(FilesystemLoader::class, [ - [__DIR__ . '/../Resources/templates'], - ]); - $container->setDefinition("flow.postgresql.{$name}.migrations.twig_loader", $twigLoaderDef); - - $twigDef = new Definition(Environment::class, [ - new Reference("flow.postgresql.{$name}.migrations.twig_loader"), - ]); - $container->setDefinition("flow.postgresql.{$name}.migrations.twig", $twigDef); - - $versionGenDef = new Definition(TimestampVersionGenerator::class); - $container->setDefinition("flow.postgresql.{$name}.migrations.version_generator", $versionGenDef); - - $generatorDef = new Definition(TwigMigrationGenerator::class, [ - new Reference("flow.postgresql.{$name}.migrations.configuration"), - new Reference("flow.postgresql.{$name}.migrations.version_generator"), - new Reference("flow.postgresql.{$name}.migrations.twig"), - new Reference("flow.postgresql.{$name}.migrations.filesystem"), - ]); - $generatorDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.generator", $generatorDef); - - $diffGenDef = new Definition(DiffMigrationGenerator::class); - $diffGenDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createDiffGenerator']); - $diffGenDef->setArguments([new Reference("flow.postgresql.{$name}.migrations.generator")]); - $diffGenDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.migrations.diff_generator", $diffGenDef); - - if ($isFirst) { - $container->setAlias(MigrationsConfiguration::class, "flow.postgresql.{$name}.migrations.configuration"); - $container->setAlias(MigrationsFactory::class, "flow.postgresql.{$name}.migrations.factory"); - $container->setAlias(Migrator::class, "flow.postgresql.{$name}.migrations.migrator"); - $container->setAlias(MigrationStore::class, "flow.postgresql.{$name}.migrations.store"); - $container->setAlias(MigrationRepository::class, "flow.postgresql.{$name}.migrations.repository"); - $container->setAlias(MigrationExecutor::class, "flow.postgresql.{$name}.migrations.executor"); - $container->setAlias(VersionResolver::class, "flow.postgresql.{$name}.migrations.version_resolver"); - $container->setAlias(MigrationGenerator::class, "flow.postgresql.{$name}.migrations.generator"); - $container->setAlias(DiffMigrationGenerator::class, "flow.postgresql.{$name}.migrations.diff_generator"); - } - } - - /** - * @param array{enabled?: bool, connection?: ?string, table_name?: string, schema?: string, id_col?: string, data_col?: string, lifetime_col?: string, time_col?: string, lock_mode?: string, ttl?: ?int, share_connection?: bool} $sessionConfig - * @param list $connectionNames - */ - private function registerSession(array $sessionConfig, array $connectionNames, ContainerBuilder $container) : void - { - if (!\class_exists(FlowPostgreSqlSessionHandler::class)) { - return; - } - - if (!($sessionConfig['enabled'] ?? false)) { - return; - } - - $connectionName = $sessionConfig['connection'] ?? $connectionNames[0]; - - if (!\in_array($connectionName, $connectionNames, true)) { - throw new \LogicException(\sprintf( - 'Session references unknown connection "%s". Declared connections: %s', - $connectionName, - \implode(', ', $connectionNames), - )); - } - - $tableName = $sessionConfig['table_name'] ?? 'sessions'; - $schema = $sessionConfig['schema'] ?? 'public'; - $idCol = $sessionConfig['id_col'] ?? 'sess_id'; - $dataCol = $sessionConfig['data_col'] ?? 'sess_data'; - $lifetimeCol = $sessionConfig['lifetime_col'] ?? 'sess_lifetime'; - $timeCol = $sessionConfig['time_col'] ?? 'sess_time'; - - $catalogDef = new Definition(SessionCatalogProvider::class, [ - $tableName, - $schema, - $idCol, - $dataCol, - $lifetimeCol, - $timeCol, - ]); - $catalogDef->addTag('flow.postgresql.catalog_provider'); - $container->setDefinition('flow.postgresql.session.catalog_provider', $catalogDef); - - $lockMode = match ($sessionConfig['lock_mode'] ?? 'transactional') { - 'none' => FlowPostgreSqlSessionHandler::LOCK_NONE, - 'advisory' => FlowPostgreSqlSessionHandler::LOCK_ADVISORY, - default => FlowPostgreSqlSessionHandler::LOCK_TRANSACTIONAL, - }; - - $connectionRef = ($sessionConfig['share_connection'] ?? false) - ? new Reference("flow.postgresql.{$connectionName}.client") - : new Reference("flow.postgresql.{$connectionName}.connection_parameters"); - - $handlerDef = new Definition(FlowPostgreSqlSessionHandler::class, [ - $connectionRef, - [ - 'db_table' => $tableName, - 'db_schema' => $schema, - 'db_id_col' => $idCol, - 'db_data_col' => $dataCol, - 'db_lifetime_col' => $lifetimeCol, - 'db_time_col' => $timeCol, - 'lock_mode' => $lockMode, - 'ttl' => $sessionConfig['ttl'] ?? null, - ], - ]); - $handlerDef->setPublic(true); - $container->setDefinition('flow.postgresql.session.handler', $handlerDef); - - $commandDef = new Definition(SessionPurgeCommand::class, [ - new Reference('flow.postgresql.session.handler'), - ]); - $commandDef->addTag('console.command'); - $container->setDefinition('flow.postgresql.session.purge_command', $commandDef); - } - - private function registerStaticConnection(string $name, ContainerBuilder $container) : void - { - if (!\class_exists(StaticClient::class)) { - throw new \LogicException(\sprintf( - 'Connection "%s" has test_transaction_rollback set to true, but flow-php/phpunit-postgresql-bridge is not installed. Run "composer require --dev flow-php/phpunit-postgresql-bridge".', - $name, - )); - } - - $container->getDefinition("flow.postgresql.{$name}.client") - ->setFactory([StaticClient::class, 'connect']); - } - - /** - * @param array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int} $telemetryConfig - */ - private function registerTelemetry(string $name, array $telemetryConfig, ContainerBuilder $container) : void - { - $optionsDef = new Definition(PostgreSqlTelemetryOptions::class, [ - $telemetryConfig['trace_queries'], - $telemetryConfig['trace_transactions'], - $telemetryConfig['collect_metrics'], - $telemetryConfig['log_queries'], - $telemetryConfig['max_query_length'], - $telemetryConfig['include_parameters'], - $telemetryConfig['max_parameters'], - $telemetryConfig['max_parameter_length'], - ]); - $container->setDefinition("flow.postgresql.{$name}.telemetry.options", $optionsDef); - - if ($telemetryConfig['clock_service_id'] !== null) { - $clockRef = new Reference($telemetryConfig['clock_service_id']); - } else { - $container->setDefinition("flow.postgresql.{$name}.telemetry.clock", new Definition(SystemClock::class)); - $clockRef = new Reference("flow.postgresql.{$name}.telemetry.clock"); - } - - $configDef = new Definition(PostgreSqlTelemetryConfig::class, [ - new Reference($telemetryConfig['service_id']), - $clockRef, - new Reference("flow.postgresql.{$name}.telemetry.options"), - ]); - $container->setDefinition("flow.postgresql.{$name}.telemetry.config", $configDef); - - $traceableDef = new Definition(TraceableClient::class, [ - new Reference("flow.postgresql.{$name}.client.telemetry.inner"), - new Reference("flow.postgresql.{$name}.telemetry.config"), - ]); - $traceableDef->setDecoratedService("flow.postgresql.{$name}.client"); - $traceableDef->setPublic(true); - $container->setDefinition("flow.postgresql.{$name}.client.telemetry", $traceableDef); - } -} diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php index 7b66df252..b23727ffc 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php @@ -4,21 +4,49 @@ namespace Flow\Bridge\Symfony\PostgreSqlBundle; +use function Flow\Types\DSL\type_string; +use Flow\Bridge\PHPUnit\PostgreSQL\StaticClient; use Flow\Bridge\Symfony\PostgreSqlBundle\Attribute\AsCatalogProvider; -use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Compiler\CatalogProviderPass; -use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\FlowPostgreSqlExtension; -use Symfony\Component\DependencyInjection\{ChildDefinition, ContainerBuilder}; -use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Flow\Bridge\Symfony\PostgreSqlBundle\CatalogProvider\ArrayCatalogProvider; +use Flow\Bridge\Symfony\PostgreSqlBundle\Command\SessionPurgeCommand; +use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Compiler\{CatalogProviderPass, CommandLocatorPass}; +use Flow\Bridge\Symfony\PostgreSqlBundle\Generator\TwigMigrationGenerator; +use Flow\Bridge\Symfony\PostgreSqlBundle\Messenger\FlowPostgreSqlTransportFactory; +use Flow\Bridge\Symfony\PostgreSqlBundle\Repository\FilesystemMigrationRepository; +use Flow\Bridge\Symfony\PostgreSQLCache\{CacheCatalogProvider, FlowPostgreSqlCacheAdapter}; +use Flow\Bridge\Symfony\PostgreSQLMessenger\MessengerCatalogProvider; +use Flow\Bridge\Symfony\PostgreSQLSession\{FlowPostgreSqlSessionHandler, SessionCatalogProvider}; +use Flow\Filesystem\Local\NativeLocalFilesystem; +use Flow\Filesystem\Path; +use Flow\PostgreSql\Client\{Client, ConnectionParameters, Context, DsnParser}; +use Flow\PostgreSql\Client\Infrastructure\PgSql\PgSqlClient; +use Flow\PostgreSql\Client\Telemetry\{PostgreSqlTelemetryConfig, PostgreSqlTelemetryOptions, TraceableClient}; +use Flow\PostgreSql\Migrations\{Configuration as MigrationsConfiguration, MigrationsFactory, Migrator, VersionResolver}; +use Flow\PostgreSql\Migrations\Executor\MigrationExecutor; +use Flow\PostgreSql\Migrations\Generator\{DiffMigrationGenerator, MigrationGenerator}; +use Flow\PostgreSql\Migrations\Repository\MigrationRepository; +use Flow\PostgreSql\Migrations\Store\MigrationStore; +use Flow\PostgreSql\Migrations\VersionGenerator\TimestampVersionGenerator; +use Flow\Telemetry\Provider\Clock\SystemClock; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\{ChildDefinition, ContainerBuilder, Definition, Reference, ServiceLocator}; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Twig\Environment; +use Twig\Loader\FilesystemLoader; -final class FlowPostgreSqlBundle extends Bundle +final class FlowPostgreSqlBundle extends AbstractBundle { + protected string $extensionAlias = 'flow_postgresql'; + #[\Override] public function build(ContainerBuilder $container) : void { parent::build($container); $container->addCompilerPass(new CatalogProviderPass()); + $container->addCompilerPass(new CommandLocatorPass()); $container->registerAttributeForAutoconfiguration( AsCatalogProvider::class, @@ -29,8 +57,639 @@ static function (ChildDefinition $definition, AsCatalogProvider $attribute, \Ref } #[\Override] - public function getContainerExtension() : ExtensionInterface + public function configure(DefinitionConfigurator $definition) : void + { + $definition->rootNode() + ->children() + ->arrayNode('connections') + ->requiresAtLeastOneElement() + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('dsn') + ->isRequired() + ->cannotBeEmpty() + ->info('PostgreSQL connection DSN (e.g. postgresql://user:pass@localhost:5432/dbname)') + ->end() + ->booleanNode('test_transaction_rollback') + ->defaultFalse() + ->info('When true and flow-php/phpunit-postgresql-bridge is installed, wraps the connection with StaticClient for transaction rollback in tests.') + ->end() + ->arrayNode('context') + ->info('Extra key/value pairs merged into the Flow\\PostgreSql\\Client\\Context for every mapper call. Values can be literals, @service_id references, %parameter% placeholders, or %env(VAR)% expressions.') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->arrayNode('telemetry') + ->children() + ->scalarNode('service_id') + ->isRequired() + ->cannotBeEmpty() + ->info('Service ID of the Telemetry instance (e.g. flow.telemetry)') + ->end() + ->scalarNode('clock_service_id') + ->defaultNull() + ->info('Service ID of a PSR ClockInterface implementation. Default: creates SystemClock') + ->end() + ->booleanNode('trace_queries')->defaultTrue()->end() + ->booleanNode('trace_transactions')->defaultTrue()->end() + ->booleanNode('collect_metrics')->defaultTrue()->end() + ->booleanNode('log_queries')->defaultFalse()->end() + ->integerNode('max_query_length')->defaultValue(1000)->min(0)->end() + ->booleanNode('include_parameters')->defaultFalse()->end() + ->integerNode('max_parameters')->defaultValue(10)->min(0)->end() + ->integerNode('max_parameter_length')->defaultValue(100)->min(0)->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('messenger') + ->info('Enables the Symfony Messenger PostgreSQL transport. Requires flow-php/symfony-postgresql-messenger-bridge.') + ->canBeEnabled() + ->children() + ->scalarNode('table_name') + ->defaultValue('messenger_messages') + ->cannotBeEmpty() + ->info('Name of the table that stores messenger messages.') + ->end() + ->scalarNode('schema') + ->defaultValue('public') + ->cannotBeEmpty() + ->info('Schema that owns the messenger table.') + ->end() + ->end() + ->end() + ->arrayNode('cache') + ->info('Defines PostgreSQL-backed Symfony Cache pools. Requires flow-php/symfony-postgresql-cache-bridge.') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('pools') + ->info('Named cache pools. Each becomes a service "flow.postgresql.cache.pool." usable as adapter: in framework.cache.pools.') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('connection') + ->defaultNull() + ->info('flow_postgresql.connections key to use. Defaults to the first declared connection when null.') + ->end() + ->scalarNode('table_name')->defaultValue('cache_items')->cannotBeEmpty()->end() + ->scalarNode('schema')->defaultValue('public')->cannotBeEmpty()->end() + ->scalarNode('id_col')->defaultValue('item_id')->cannotBeEmpty()->end() + ->scalarNode('data_col')->defaultValue('item_data')->cannotBeEmpty()->end() + ->scalarNode('lifetime_col')->defaultValue('item_lifetime')->cannotBeEmpty()->end() + ->scalarNode('time_col')->defaultValue('item_time')->cannotBeEmpty()->end() + ->scalarNode('namespace') + ->defaultValue('') + ->info('Cache pool namespace. Allowed chars: -+.A-Za-z0-9') + ->end() + ->integerNode('default_lifetime')->defaultValue(0)->min(0)->end() + ->scalarNode('marshaller_service_id')->defaultNull()->end() + ->booleanNode('share_connection') + ->defaultFalse() + ->info('When true, the pool reuses the named connection\'s Client instead of opening its own pg_connect. Off by default.') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('session') + ->info('Defines the PostgreSQL-backed Symfony Session handler. Requires flow-php/symfony-postgresql-session-bridge.') + ->canBeEnabled() + ->children() + ->scalarNode('connection') + ->defaultNull() + ->info('flow_postgresql.connections key to use. Defaults to the first declared connection when null.') + ->end() + ->scalarNode('table_name')->defaultValue('sessions')->cannotBeEmpty()->end() + ->scalarNode('schema')->defaultValue('public')->cannotBeEmpty()->end() + ->scalarNode('id_col')->defaultValue('sess_id')->cannotBeEmpty()->end() + ->scalarNode('data_col')->defaultValue('sess_data')->cannotBeEmpty()->end() + ->scalarNode('lifetime_col')->defaultValue('sess_lifetime')->cannotBeEmpty()->end() + ->scalarNode('time_col')->defaultValue('sess_time')->cannotBeEmpty()->end() + ->enumNode('lock_mode') + ->values(['none', 'advisory', 'transactional']) + ->defaultValue('transactional') + ->info('Locking strategy. "transactional" uses SELECT FOR UPDATE; "advisory" uses pg_advisory_lock; "none" disables locking.') + ->end() + ->integerNode('ttl') + ->defaultNull() + ->min(0) + ->info('Session lifetime in seconds. When null, falls back to ini "session.gc_maxlifetime".') + ->end() + ->booleanNode('share_connection') + ->defaultFalse() + ->info('When true, the handler reuses the named connection\'s Client instead of opening its own pg_connect. Off by default.') + ->end() + ->end() + ->end() + ->arrayNode('migrations') + ->canBeEnabled() + ->children() + ->scalarNode('directory') + ->defaultValue('%kernel.project_dir%/migrations') + ->cannotBeEmpty() + ->end() + ->scalarNode('namespace') + ->defaultValue('App\\Migrations') + ->cannotBeEmpty() + ->end() + ->scalarNode('table_name') + ->defaultValue('flow_migrations') + ->cannotBeEmpty() + ->end() + ->scalarNode('table_schema') + ->defaultValue('public') + ->cannotBeEmpty() + ->end() + ->scalarNode('migration_file_name') + ->defaultValue('migration.php') + ->cannotBeEmpty() + ->end() + ->scalarNode('rollback_file_name') + ->defaultValue('rollback.php') + ->cannotBeEmpty() + ->end() + ->booleanNode('all_or_nothing') + ->defaultFalse() + ->info('Wrap all migrations in a single transaction (default: false)') + ->end() + ->booleanNode('generate_rollback') + ->defaultTrue() + ->info('Generate rollback files when creating migrations (default: true)') + ->end() + ->end() + ->end() + ->arrayNode('catalog_providers') + ->info('List of catalog providers to merge into the target schema. Each entry must have either "catalog_provider_id" or "catalog".') + ->arrayPrototype() + ->children() + ->scalarNode('catalog_provider_id') + ->defaultNull() + ->info('Service ID of Flow\\PostgreSql\\Schema\\CatalogProvider') + ->end() + ->variableNode('catalog') + ->defaultNull() + ->info('Inline catalog definition matching Catalog::fromArray() shape') + ->end() + ->end() + ->end() + ->end() + ->end(); + } + + /** + * @param array{connections: array, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool}, catalog_providers: list}>} $config + */ + #[\Override] + public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container) : void + { + $isFirst = true; + $connectionNames = \array_keys($config['connections']); + + foreach ($config['connections'] as $name => $connectionConfig) { + $this->registerConnection($name, $connectionConfig, $container, $isFirst); + $isFirst = false; + } + + $this->registerCatalogProviders($config['catalog_providers'] ?? [], $container); + + $container->setParameter('flow.postgresql.connections', $connectionNames); + $container->setParameter('flow.postgresql.default_connection', $connectionNames[0]); + + $configurator->import(__DIR__ . '/Resources/config/database.php'); + $configurator->import(__DIR__ . '/Resources/config/format.php'); + + if ($config['migrations']['enabled']) { + $isFirst = true; + + foreach ($connectionNames as $name) { + $this->registerMigrations($name, $config['migrations'], $container, $isFirst); + $isFirst = false; + } + + $container->setParameter('flow.postgresql.migrations.connections', $connectionNames); + $container->setParameter('flow.postgresql.migrations.default_connection', $connectionNames[0]); + + $configurator->import(__DIR__ . '/Resources/config/migrations.php'); + } + + $this->registerMessenger($config['messenger'], $connectionNames, $container); + $this->registerCache($config['cache'] ?? [], $connectionNames, $container); + $this->registerSession($config['session'] ?? [], $connectionNames, $container); + } + + /** + * @param array{pools?: array} $cacheConfig + * @param list $connectionNames + */ + private function registerCache(array $cacheConfig, array $connectionNames, ContainerBuilder $container) : void + { + if (!\class_exists(FlowPostgreSqlCacheAdapter::class)) { + return; + } + + $pools = $cacheConfig['pools'] ?? []; + + if ($pools === []) { + return; + } + + foreach ($pools as $name => $poolConfig) { + $this->registerCachePool((string) $name, $poolConfig, $connectionNames, $container); + } + } + + /** + * @param array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool} $poolConfig + * @param list $connectionNames + */ + private function registerCachePool(string $name, array $poolConfig, array $connectionNames, ContainerBuilder $container) : void + { + $connectionName = $poolConfig['connection'] ?? $connectionNames[0]; + + if (!\in_array($connectionName, $connectionNames, true)) { + throw new \LogicException(\sprintf( + 'Cache pool "%s" references unknown connection "%s". Declared connections: %s', + $name, + $connectionName, + \implode(', ', $connectionNames), + )); + } + + $catalogDef = new Definition(CacheCatalogProvider::class, [ + $poolConfig['table_name'], + $poolConfig['schema'], + $poolConfig['id_col'], + $poolConfig['data_col'], + $poolConfig['lifetime_col'], + $poolConfig['time_col'], + ]); + $catalogDef->addTag('flow.postgresql.catalog_provider'); + $container->setDefinition("flow.postgresql.cache.pool.{$name}.catalog_provider", $catalogDef); + + $connectionRef = ($poolConfig['share_connection'] ?? false) + ? new Reference("flow.postgresql.{$connectionName}.client") + : new Reference("flow.postgresql.{$connectionName}.connection_parameters"); + + $adapterDef = new Definition(FlowPostgreSqlCacheAdapter::class, [ + $connectionRef, + $poolConfig['namespace'], + $poolConfig['default_lifetime'], + [ + 'db_table' => $poolConfig['table_name'], + 'db_schema' => $poolConfig['schema'], + 'db_id_col' => $poolConfig['id_col'], + 'db_data_col' => $poolConfig['data_col'], + 'db_lifetime_col' => $poolConfig['lifetime_col'], + 'db_time_col' => $poolConfig['time_col'], + ], + $poolConfig['marshaller_service_id'] !== null + ? new Reference($poolConfig['marshaller_service_id']) + : null, + ]); + $adapterDef->setPublic(true); + $container->setDefinition("flow.postgresql.cache.pool.{$name}", $adapterDef); + } + + /** + * @param list}> $catalogProviders + */ + private function registerCatalogProviders(array $catalogProviders, ContainerBuilder $container) : void + { + $configProviderServiceIds = []; + + foreach ($catalogProviders as $i => $providerConfig) { + if (\array_key_exists('catalog', $providerConfig) && $providerConfig['catalog'] !== null) { + $providerDef = new Definition(ArrayCatalogProvider::class, [$providerConfig['catalog']]); + $providerDef->addTag('flow.postgresql.catalog_provider'); + $container->setDefinition("flow.postgresql.catalog_provider.{$i}", $providerDef); + } elseif (\array_key_exists('catalog_provider_id', $providerConfig) && $providerConfig['catalog_provider_id'] !== null) { + $configProviderServiceIds[] = type_string()->assert($providerConfig['catalog_provider_id']); + } + } + + if ($configProviderServiceIds !== []) { + $container->setParameter('flow.postgresql.catalog_provider.service_ids', $configProviderServiceIds); + } + } + + /** + * @param array{dsn: string, test_transaction_rollback: bool, context?: array, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}} $connectionConfig + */ + private function registerConnection(string $name, array $connectionConfig, ContainerBuilder $container, bool $isFirst) : void + { + $parserDef = new Definition(DsnParser::class); + $container->setDefinition("flow.postgresql.{$name}.dsn_parser", $parserDef); + + $paramsDef = new Definition(ConnectionParameters::class); + $paramsDef->setFactory([new Reference("flow.postgresql.{$name}.dsn_parser"), 'parse']); + $paramsDef->setArguments([$connectionConfig['dsn']]); + $container->setDefinition("flow.postgresql.{$name}.connection_parameters", $paramsDef); + + $paramsDef->setPublic(true); + + $clientArguments = [new Reference("flow.postgresql.{$name}.connection_parameters")]; + + if (\array_key_exists('context', $connectionConfig) && $connectionConfig['context'] !== []) { + $contextDef = new Definition(Context::class, [ + null, + $connectionConfig['context'], + ]); + $container->setDefinition("flow.postgresql.{$name}.context", $contextDef); + + $clientArguments[] = null; + $clientArguments[] = new Reference("flow.postgresql.{$name}.context"); + } + + $clientDef = new Definition(PgSqlClient::class); + $clientDef->setFactory([PgSqlClient::class, 'connect']); + $clientDef->setArguments($clientArguments); + $clientDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.client", $clientDef); + + if ($connectionConfig['test_transaction_rollback']) { + $this->registerStaticConnection($name, $container); + } + + if (\array_key_exists('telemetry', $connectionConfig)) { + $this->registerTelemetry($name, $connectionConfig['telemetry'], $container); + } + + if ($isFirst) { + $container->setAlias(Client::class, "flow.postgresql.{$name}.client"); + $container->setAlias(ConnectionParameters::class, "flow.postgresql.{$name}.connection_parameters"); + } + } + + /** + * @param array{enabled: bool, table_name: string, schema: string} $messengerConfig + * @param list $connectionNames + */ + private function registerMessenger(array $messengerConfig, array $connectionNames, ContainerBuilder $container) : void { - return new FlowPostgreSqlExtension(); + if (!\class_exists(FlowPostgreSqlTransportFactory::class)) { + return; + } + + if (!$messengerConfig['enabled']) { + return; + } + + $catalogProviderDef = new Definition(MessengerCatalogProvider::class, [ + $messengerConfig['table_name'], + $messengerConfig['schema'], + ]); + $catalogProviderDef->addTag('flow.postgresql.catalog_provider'); + $container->setDefinition('flow.postgresql.messenger.catalog_provider', $catalogProviderDef); + + $locatorServices = []; + + foreach ($connectionNames as $name) { + $locatorServices[$name] = new ServiceClosureArgument(new Reference("flow.postgresql.{$name}.client")); + } + + $locatorDef = new Definition(ServiceLocator::class, [$locatorServices]); + $locatorDef->addTag('container.service_locator'); + $container->setDefinition('flow.postgresql.messenger.client_locator', $locatorDef); + + $factoryDef = new Definition(FlowPostgreSqlTransportFactory::class, [ + new Reference('flow.postgresql.messenger.client_locator'), + ]); + $factoryDef->addTag('messenger.transport_factory'); + $container->setDefinition('flow.postgresql.messenger.transport_factory', $factoryDef); + } + + /** + * @param array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool} $mc + */ + private function registerMigrations(string $name, array $mc, ContainerBuilder $container, bool $isFirst) : void + { + $catalogProviderRef = new Reference('flow.postgresql.catalog_provider'); + + $configDef = new Definition(MigrationsConfiguration::class, [ + new Reference("flow.postgresql.{$name}.client"), + $catalogProviderRef, + $mc['directory'], + $mc['namespace'], + $mc['table_name'], + $mc['table_schema'], + $mc['migration_file_name'], + $mc['rollback_file_name'], + $mc['all_or_nothing'], + $mc['generate_rollback'], + ]); + $configDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.configuration", $configDef); + + $fsDef = new Definition(NativeLocalFilesystem::class); + $container->setDefinition("flow.postgresql.{$name}.migrations.filesystem", $fsDef); + + $pathDef = new Definition(Path::class); + $pathDef->setFactory([Path::class, 'from']); + $pathDef->setArguments([$mc['directory']]); + $container->setDefinition("flow.postgresql.{$name}.migrations.path", $pathDef); + + $repoDef = new Definition(FilesystemMigrationRepository::class, [ + new Reference("flow.postgresql.{$name}.migrations.filesystem"), + new Reference("flow.postgresql.{$name}.migrations.path"), + new Reference("flow.postgresql.{$name}.migrations.configuration"), + ]); + $repoDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.repository", $repoDef); + + $factoryDef = new Definition(MigrationsFactory::class, [ + new Reference("flow.postgresql.{$name}.migrations.configuration"), + new Reference("flow.postgresql.{$name}.migrations.repository"), + ]); + $container->setDefinition("flow.postgresql.{$name}.migrations.factory", $factoryDef); + + $migratorDef = new Definition(Migrator::class); + $migratorDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createMigrator']); + $migratorDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.migrator", $migratorDef); + + $storeDef = new Definition(MigrationStore::class); + $storeDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createStore']); + $storeDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.store", $storeDef); + + $executorDef = new Definition(MigrationExecutor::class); + $executorDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createExecutor']); + $container->setDefinition("flow.postgresql.{$name}.migrations.executor", $executorDef); + + $resolverDef = new Definition(VersionResolver::class); + $resolverDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createVersionResolver']); + $resolverDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.version_resolver", $resolverDef); + + $twigLoaderDef = new Definition(FilesystemLoader::class, [ + [__DIR__ . '/Resources/templates'], + ]); + $container->setDefinition("flow.postgresql.{$name}.migrations.twig_loader", $twigLoaderDef); + + $twigDef = new Definition(Environment::class, [ + new Reference("flow.postgresql.{$name}.migrations.twig_loader"), + ]); + $container->setDefinition("flow.postgresql.{$name}.migrations.twig", $twigDef); + + $versionGenDef = new Definition(TimestampVersionGenerator::class); + $container->setDefinition("flow.postgresql.{$name}.migrations.version_generator", $versionGenDef); + + $generatorDef = new Definition(TwigMigrationGenerator::class, [ + new Reference("flow.postgresql.{$name}.migrations.configuration"), + new Reference("flow.postgresql.{$name}.migrations.version_generator"), + new Reference("flow.postgresql.{$name}.migrations.twig"), + new Reference("flow.postgresql.{$name}.migrations.filesystem"), + ]); + $generatorDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.generator", $generatorDef); + + $diffGenDef = new Definition(DiffMigrationGenerator::class); + $diffGenDef->setFactory([new Reference("flow.postgresql.{$name}.migrations.factory"), 'createDiffGenerator']); + $diffGenDef->setArguments([new Reference("flow.postgresql.{$name}.migrations.generator")]); + $diffGenDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.migrations.diff_generator", $diffGenDef); + + if ($isFirst) { + $container->setAlias(MigrationsConfiguration::class, "flow.postgresql.{$name}.migrations.configuration"); + $container->setAlias(MigrationsFactory::class, "flow.postgresql.{$name}.migrations.factory"); + $container->setAlias(Migrator::class, "flow.postgresql.{$name}.migrations.migrator"); + $container->setAlias(MigrationStore::class, "flow.postgresql.{$name}.migrations.store"); + $container->setAlias(MigrationRepository::class, "flow.postgresql.{$name}.migrations.repository"); + $container->setAlias(MigrationExecutor::class, "flow.postgresql.{$name}.migrations.executor"); + $container->setAlias(VersionResolver::class, "flow.postgresql.{$name}.migrations.version_resolver"); + $container->setAlias(MigrationGenerator::class, "flow.postgresql.{$name}.migrations.generator"); + $container->setAlias(DiffMigrationGenerator::class, "flow.postgresql.{$name}.migrations.diff_generator"); + } + } + + /** + * @param array{enabled?: bool, connection?: ?string, table_name?: string, schema?: string, id_col?: string, data_col?: string, lifetime_col?: string, time_col?: string, lock_mode?: string, ttl?: ?int, share_connection?: bool} $sessionConfig + * @param list $connectionNames + */ + private function registerSession(array $sessionConfig, array $connectionNames, ContainerBuilder $container) : void + { + if (!\class_exists(FlowPostgreSqlSessionHandler::class)) { + return; + } + + if (!($sessionConfig['enabled'] ?? false)) { + return; + } + + $connectionName = $sessionConfig['connection'] ?? $connectionNames[0]; + + if (!\in_array($connectionName, $connectionNames, true)) { + throw new \LogicException(\sprintf( + 'Session references unknown connection "%s". Declared connections: %s', + $connectionName, + \implode(', ', $connectionNames), + )); + } + + $tableName = $sessionConfig['table_name'] ?? 'sessions'; + $schema = $sessionConfig['schema'] ?? 'public'; + $idCol = $sessionConfig['id_col'] ?? 'sess_id'; + $dataCol = $sessionConfig['data_col'] ?? 'sess_data'; + $lifetimeCol = $sessionConfig['lifetime_col'] ?? 'sess_lifetime'; + $timeCol = $sessionConfig['time_col'] ?? 'sess_time'; + + $catalogDef = new Definition(SessionCatalogProvider::class, [ + $tableName, + $schema, + $idCol, + $dataCol, + $lifetimeCol, + $timeCol, + ]); + $catalogDef->addTag('flow.postgresql.catalog_provider'); + $container->setDefinition('flow.postgresql.session.catalog_provider', $catalogDef); + + $lockMode = match ($sessionConfig['lock_mode'] ?? 'transactional') { + 'none' => FlowPostgreSqlSessionHandler::LOCK_NONE, + 'advisory' => FlowPostgreSqlSessionHandler::LOCK_ADVISORY, + default => FlowPostgreSqlSessionHandler::LOCK_TRANSACTIONAL, + }; + + $connectionRef = ($sessionConfig['share_connection'] ?? false) + ? new Reference("flow.postgresql.{$connectionName}.client") + : new Reference("flow.postgresql.{$connectionName}.connection_parameters"); + + $handlerDef = new Definition(FlowPostgreSqlSessionHandler::class, [ + $connectionRef, + [ + 'db_table' => $tableName, + 'db_schema' => $schema, + 'db_id_col' => $idCol, + 'db_data_col' => $dataCol, + 'db_lifetime_col' => $lifetimeCol, + 'db_time_col' => $timeCol, + 'lock_mode' => $lockMode, + 'ttl' => $sessionConfig['ttl'] ?? null, + ], + ]); + $handlerDef->setPublic(true); + $container->setDefinition('flow.postgresql.session.handler', $handlerDef); + + $commandDef = new Definition(SessionPurgeCommand::class, [ + new Reference('flow.postgresql.session.handler'), + ]); + $commandDef->addTag('console.command'); + $container->setDefinition('flow.postgresql.session.purge_command', $commandDef); + } + + private function registerStaticConnection(string $name, ContainerBuilder $container) : void + { + if (!\class_exists(StaticClient::class)) { + throw new \LogicException(\sprintf( + 'Connection "%s" has test_transaction_rollback set to true, but flow-php/phpunit-postgresql-bridge is not installed. Run "composer require --dev flow-php/phpunit-postgresql-bridge".', + $name, + )); + } + + $container->getDefinition("flow.postgresql.{$name}.client") + ->setFactory([StaticClient::class, 'connect']); + } + + /** + * @param array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int} $telemetryConfig + */ + private function registerTelemetry(string $name, array $telemetryConfig, ContainerBuilder $container) : void + { + $optionsDef = new Definition(PostgreSqlTelemetryOptions::class, [ + $telemetryConfig['trace_queries'], + $telemetryConfig['trace_transactions'], + $telemetryConfig['collect_metrics'], + $telemetryConfig['log_queries'], + $telemetryConfig['max_query_length'], + $telemetryConfig['include_parameters'], + $telemetryConfig['max_parameters'], + $telemetryConfig['max_parameter_length'], + ]); + $container->setDefinition("flow.postgresql.{$name}.telemetry.options", $optionsDef); + + if ($telemetryConfig['clock_service_id'] !== null) { + $clockRef = new Reference($telemetryConfig['clock_service_id']); + } else { + $container->setDefinition("flow.postgresql.{$name}.telemetry.clock", new Definition(SystemClock::class)); + $clockRef = new Reference("flow.postgresql.{$name}.telemetry.clock"); + } + + $configDef = new Definition(PostgreSqlTelemetryConfig::class, [ + new Reference($telemetryConfig['service_id']), + $clockRef, + new Reference("flow.postgresql.{$name}.telemetry.options"), + ]); + $container->setDefinition("flow.postgresql.{$name}.telemetry.config", $configDef); + + $traceableDef = new Definition(TraceableClient::class, [ + new Reference("flow.postgresql.{$name}.client.telemetry.inner"), + new Reference("flow.postgresql.{$name}.telemetry.config"), + ]); + $traceableDef->setDecoratedService("flow.postgresql.{$name}.client"); + $traceableDef->setPublic(true); + $container->setDefinition("flow.postgresql.{$name}.client.telemetry", $traceableDef); } } diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/database.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/database.php index fe0349b21..0692b851d 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/database.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/database.php @@ -11,14 +11,14 @@ $services = $container->services(); $services->set('flow.postgresql.command.database_create', CreateDatabaseCommand::class) - ->args([service('service_container'), param('flow.postgresql.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.database_drop', DropDatabaseCommand::class) - ->args([service('service_container'), param('flow.postgresql.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.sql_run', RunSqlCommand::class) - ->args([service('service_container'), param('flow.postgresql.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.default_connection')]) ->tag('console.command'); }; diff --git a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/migrations.php b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/migrations.php index 023bd1d69..cb54afb43 100644 --- a/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/migrations.php +++ b/src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Resources/config/migrations.php @@ -18,38 +18,38 @@ $services = $container->services(); $services->set('flow.postgresql.command.current', CurrentCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.latest', LatestCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.status', StatusCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.list', ListCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.migrate', MigrateCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.execute', ExecuteCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.diff', DiffCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.generate', GenerateCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); $services->set('flow.postgresql.command.up_to_date', UpToDateCommand::class) - ->args([service('service_container'), param('flow.postgresql.migrations.default_connection')]) + ->args([service('flow.postgresql.command_locator'), param('flow.postgresql.migrations.default_connection')]) ->tag('console.command'); }; diff --git a/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Context/ConfigurationContext.php b/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Context/ConfigurationContext.php new file mode 100644 index 000000000..008aba614 --- /dev/null +++ b/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Context/ConfigurationContext.php @@ -0,0 +1,35 @@ + $config + * + * @return array + */ + public function processConfig(array $config) : array + { + $extension = (new FlowPostgreSqlBundle())->getContainerExtension(); + + if (!$extension instanceof ConfigurationExtensionInterface) { + throw new \LogicException('FlowPostgreSqlBundle extension does not expose a configuration tree.'); + } + + $configuration = $extension->getConfiguration([], new ContainerBuilder()); + + if ($configuration === null) { + throw new \LogicException('FlowPostgreSqlBundle extension exposes no configuration tree.'); + } + + return (new Processor())->processConfiguration($configuration, [$config]); + } +} diff --git a/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/FlowPostgreSqlExtensionTest.php b/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/FlowPostgreSqlExtensionTest.php index 8e9dcdd31..05cad71af 100644 --- a/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/FlowPostgreSqlExtensionTest.php +++ b/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/FlowPostgreSqlExtensionTest.php @@ -5,7 +5,7 @@ namespace Flow\Bridge\Symfony\PostgreSqlBundle\Tests\Integration; use Flow\Bridge\Symfony\PostgreSqlBundle\Command\{CreateDatabaseCommand, DropDatabaseCommand, GenerateCommand, RunSqlCommand, SessionPurgeCommand, UpToDateCommand}; -use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\FlowPostgreSqlExtension; +use Flow\Bridge\Symfony\PostgreSqlBundle\FlowPostgreSqlBundle; use Flow\Bridge\Symfony\PostgreSqlBundle\Messenger\FlowPostgreSqlTransportFactory; use Flow\Bridge\Symfony\PostgreSqlBundle\Tests\Fixtures\{AttributeTestCatalogProvider, TestKernel, VoidTelemetryFactory}; use Flow\Bridge\Symfony\PostgreSQLCache\{CacheCatalogProvider, FlowPostgreSqlCacheAdapter}; @@ -25,7 +25,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use Symfony\Component\DependencyInjection\{ContainerBuilder, Definition, Reference}; -#[CoversClass(FlowPostgreSqlExtension::class)] +#[CoversClass(FlowPostgreSqlBundle::class)] final class FlowPostgreSqlExtensionTest extends KernelTestCase { public function test_attribute_based_catalog_provider_auto_discovery() : void diff --git a/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php index 23af4562a..2c751d716 100644 --- a/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -4,16 +4,22 @@ namespace Flow\Bridge\Symfony\PostgreSqlBundle\Tests\Unit\DependencyInjection; -use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Configuration; +use Flow\Bridge\Symfony\PostgreSqlBundle\Tests\Context\ConfigurationContext; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; -use Symfony\Component\Config\Definition\Processor; final class ConfigurationTest extends TestCase { + private ConfigurationContext $context; + + protected function setUp() : void + { + $this->context = new ConfigurationContext(); + } + public function test_cache_pool_connection_can_be_null() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -22,14 +28,14 @@ public function test_cache_pool_connection_can_be_null() : void 'app' => [], ], ], - ]]); + ]); self::assertNull($config['cache']['pools']['app']['connection']); } public function test_cache_pool_share_connection_can_be_enabled() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -38,14 +44,14 @@ public function test_cache_pool_share_connection_can_be_enabled() : void 'app' => ['share_connection' => true], ], ], - ]]); + ]); self::assertTrue($config['cache']['pools']['app']['share_connection']); } public function test_cache_pools_custom_columns_and_namespace() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -65,7 +71,7 @@ public function test_cache_pools_custom_columns_and_namespace() : void ], ], ], - ]]); + ]); $pool = $config['cache']['pools']['sessions']; self::assertSame('default', $pool['connection']); @@ -82,7 +88,7 @@ public function test_cache_pools_custom_columns_and_namespace() : void public function test_cache_pools_default_table_and_schema() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -91,7 +97,7 @@ public function test_cache_pools_default_table_and_schema() : void 'app' => [], ], ], - ]]); + ]); $pool = $config['cache']['pools']['app']; self::assertSame('cache_items', $pool['table_name']); @@ -108,11 +114,11 @@ public function test_cache_pools_default_table_and_schema() : void public function test_cache_section_can_be_omitted() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], - ]]); + ]); self::assertSame([], $config['cache']['pools']); } @@ -135,7 +141,7 @@ public function test_catalog_providers_at_top_level_with_inline_catalog() : void ], ]; - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -147,7 +153,7 @@ public function test_catalog_providers_at_top_level_with_inline_catalog() : void 'catalog_providers' => [ ['catalog' => $catalogData], ], - ]]); + ]); self::assertCount(1, $config['catalog_providers']); self::assertSame($catalogData, $config['catalog_providers'][0]['catalog']); @@ -155,7 +161,7 @@ public function test_catalog_providers_at_top_level_with_inline_catalog() : void public function test_catalog_providers_at_top_level_with_service_reference() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -171,7 +177,7 @@ public function test_catalog_providers_at_top_level_with_service_reference() : v 'catalog_providers' => [ ['catalog_provider_id' => 'app.catalog_provider'], ], - ]]); + ]); self::assertSame('app.catalog_provider', $config['catalog_providers'][0]['catalog_provider_id']); self::assertSame('/custom/migrations', $config['migrations']['directory']); @@ -182,7 +188,7 @@ public function test_catalog_providers_at_top_level_with_service_reference() : v public function test_catalog_providers_multiple_entries() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -192,7 +198,7 @@ public function test_catalog_providers_multiple_entries() : void ['catalog' => ['schemas' => []]], ['catalog_provider_id' => 'app.second_provider'], ], - ]]); + ]); self::assertCount(2, $config['catalog_providers']); self::assertSame('app.second_provider', $config['catalog_providers'][1]['catalog_provider_id']); @@ -202,14 +208,14 @@ public function test_connections_requires_at_least_one_element() : void { $this->expectException(InvalidConfigurationException::class); - (new Processor())->processConfiguration(new Configuration(), [[ + $this->context->processConfig([ 'connections' => [], - ]]); + ]); } public function test_context_accepts_arbitrary_variables() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -222,7 +228,7 @@ public function test_context_accepts_arbitrary_variables() : void ], ], ], - ]]); + ]); self::assertSame([ 'tenant_id' => 42, @@ -235,13 +241,13 @@ public function test_context_accepts_arbitrary_variables() : void public function test_context_defaults_to_empty() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', ], ], - ]]); + ]); self::assertSame([], $config['connections']['default']['context']); } @@ -250,16 +256,16 @@ public function test_dsn_is_required() : void { $this->expectException(InvalidConfigurationException::class); - (new Processor())->processConfiguration(new Configuration(), [[ + $this->context->processConfig([ 'connections' => [ 'default' => [], ], - ]]); + ]); } public function test_messenger_custom_table_and_schema() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -268,7 +274,7 @@ public function test_messenger_custom_table_and_schema() : void 'table_name' => 'custom_queue', 'schema' => 'app', ], - ]]); + ]); self::assertTrue($config['messenger']['enabled']); self::assertSame('custom_queue', $config['messenger']['table_name']); @@ -277,14 +283,14 @@ public function test_messenger_custom_table_and_schema() : void public function test_messenger_defaults() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], 'messenger' => [ 'enabled' => true, ], - ]]); + ]); self::assertTrue($config['messenger']['enabled']); self::assertSame('messenger_messages', $config['messenger']['table_name']); @@ -293,18 +299,18 @@ public function test_messenger_defaults() : void public function test_messenger_disabled_by_default() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], - ]]); + ]); self::assertFalse($config['messenger']['enabled']); } public function test_migrations_default_values() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -313,7 +319,7 @@ public function test_migrations_default_values() : void 'migrations' => [ 'enabled' => true, ], - ]]); + ]); self::assertSame('%kernel.project_dir%/migrations', $config['migrations']['directory']); self::assertSame('App\\Migrations', $config['migrations']['namespace']); @@ -325,7 +331,7 @@ public function test_migrations_default_values() : void public function test_migrations_enabled_without_catalog_providers_is_valid_at_config_level() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -334,14 +340,14 @@ public function test_migrations_enabled_without_catalog_providers_is_valid_at_co 'migrations' => [ 'enabled' => true, ], - ]]); + ]); self::assertTrue($config['migrations']['enabled']); } public function test_multiple_connections() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db1', @@ -350,7 +356,7 @@ public function test_multiple_connections() : void 'dsn' => 'postgresql://user:pass@localhost:5432/db2', ], ], - ]]); + ]); self::assertArrayHasKey('default', $config['connections']); self::assertArrayHasKey('analytics', $config['connections']); @@ -360,25 +366,25 @@ public function test_multiple_connections() : void public function test_session_default_disabled() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], - ]]); + ]); self::assertFalse($config['session']['enabled']); } public function test_session_defaults_when_enabled() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], 'session' => [ 'enabled' => true, ], - ]]); + ]); $session = $config['session']; self::assertTrue($session['enabled']); @@ -398,7 +404,7 @@ public function test_session_lock_mode_rejects_invalid_value() : void { $this->expectException(InvalidConfigurationException::class); - (new Processor())->processConfiguration(new Configuration(), [[ + $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -406,12 +412,12 @@ public function test_session_lock_mode_rejects_invalid_value() : void 'enabled' => true, 'lock_mode' => 'pessimistic', ], - ]]); + ]); } public function test_session_overrides() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -427,7 +433,7 @@ public function test_session_overrides() : void 'lock_mode' => 'advisory', 'ttl' => 7200, ], - ]]); + ]); $session = $config['session']; self::assertSame('default', $session['connection']); @@ -443,7 +449,7 @@ public function test_session_overrides() : void public function test_session_share_connection_can_be_enabled() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -451,7 +457,7 @@ public function test_session_share_connection_can_be_enabled() : void 'enabled' => true, 'share_connection' => true, ], - ]]); + ]); self::assertTrue($config['session']['share_connection']); } @@ -460,7 +466,7 @@ public function test_session_ttl_rejects_negative() : void { $this->expectException(InvalidConfigurationException::class); - (new Processor())->processConfiguration(new Configuration(), [[ + $this->context->processConfig([ 'connections' => [ 'default' => ['dsn' => 'postgresql://user:pass@localhost:5432/db'], ], @@ -468,18 +474,18 @@ public function test_session_ttl_rejects_negative() : void 'enabled' => true, 'ttl' => -1, ], - ]]); + ]); } public function test_single_connection_with_defaults() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', ], ], - ]]); + ]); self::assertSame('postgresql://user:pass@localhost:5432/db', $config['connections']['default']['dsn']); self::assertFalse($config['migrations']['enabled']); @@ -487,13 +493,13 @@ public function test_single_connection_with_defaults() : void public function test_telemetry_not_present_by_default() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', ], ], - ]]); + ]); self::assertArrayNotHasKey('telemetry', $config['connections']['default']); } @@ -502,19 +508,19 @@ public function test_telemetry_requires_service_id() : void { $this->expectException(InvalidConfigurationException::class); - (new Processor())->processConfiguration(new Configuration(), [[ + $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', 'telemetry' => [], ], ], - ]]); + ]); } public function test_telemetry_with_custom_options() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -532,7 +538,7 @@ public function test_telemetry_with_custom_options() : void ], ], ], - ]]); + ]); $telemetry = $config['connections']['default']['telemetry']; self::assertSame('my.telemetry', $telemetry['service_id']); @@ -549,7 +555,7 @@ public function test_telemetry_with_custom_options() : void public function test_telemetry_with_defaults() : void { - $config = (new Processor())->processConfiguration(new Configuration(), [[ + $config = $this->context->processConfig([ 'connections' => [ 'default' => [ 'dsn' => 'postgresql://user:pass@localhost:5432/db', @@ -558,7 +564,7 @@ public function test_telemetry_with_defaults() : void ], ], ], - ]]); + ]); $telemetry = $config['connections']['default']['telemetry']; self::assertSame('flow.telemetry', $telemetry['service_id']);