diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..711106f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @MacPaw/platform-backend-engineers diff --git a/.gitignore b/.gitignore index 1473148..0cab60f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /phpunit.xml .phpunit.result.cache ###< phpunit/phpunit ### +/clover.xml diff --git a/composer.json b/composer.json index 25cf59c..37380b8 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,9 @@ "dealerdirect/phpcodesniffer-composer-installer": true } }, + "suggest": { + "symfony/doctrine-messenger": "Allow use doctrine as messenger transport" + }, "scripts": { "composer-validate": [ "composer validate" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..f509d41 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,21 @@ +parameters: + ignoreErrors: + - + message: "#^Class Macpaw\\\\SchemaContextBundle\\\\Messenger\\\\Transport\\\\DoctrineTransportFactoryDecorator implements generic interface Symfony\\\\Component\\\\Messenger\\\\Transport\\\\TransportFactoryInterface but does not specify its types\\: TTransport$#" + count: 1 + path: src/Messenger/Transport/DoctrineTransportFactoryDecorator.php + + - + message: "#^Method Macpaw\\\\SchemaContextBundle\\\\Messenger\\\\Transport\\\\DoctrineTransportFactoryDecorator\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Symfony\\\\Component\\\\Messenger\\\\Transport\\\\TransportFactoryInterface but does not specify its types\\: TTransport$#" + count: 1 + path: src/Messenger/Transport/DoctrineTransportFactoryDecorator.php + + - + message: "#^Method Macpaw\\\\SchemaContextBundle\\\\Messenger\\\\Transport\\\\DoctrineTransportFactoryDecorator\\:\\:createTransport\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Messenger/Transport/DoctrineTransportFactoryDecorator.php + + - + message: "#^Method Macpaw\\\\SchemaContextBundle\\\\Messenger\\\\Transport\\\\DoctrineTransportFactoryDecorator\\:\\:supports\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Messenger/Transport/DoctrineTransportFactoryDecorator.php diff --git a/phpstan.neon b/phpstan.neon index f6ecc8a..4fd315c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,6 @@ +includes: + - phpstan-baseline.neon + parameters: level: max paths: diff --git a/src/DependencyInjection/SchemaContextCompilerPass.php b/src/DependencyInjection/SchemaContextCompilerPass.php new file mode 100644 index 0000000..fc6d470 --- /dev/null +++ b/src/DependencyInjection/SchemaContextCompilerPass.php @@ -0,0 +1,40 @@ +hasDefinition(self::TARGET_ID) === false) { + return; + } + + $def = new Definition(DoctrineTransportFactoryDecorator::class); + $def->setAutowired(true); // avoid pulling the chain or adding tags + $def->setAutoconfigured(true); + $def->setPublic(false); + + // Decorate the *target* id; explicit inner id is ".inner" + $def->setDecoratedService(self::TARGET_ID, self::DECORATOR_ID . '.inner'); + + // Inject the inner/original factory + your resolver + $def->setArgument('$decoratedFactory', new Reference(self::DECORATOR_ID . '.inner')); + $def->setArgument('$baggageSchemaResolver', new Reference(BaggageSchemaResolver::class)); + $def->setArgument('$defaultSchema', $container->getParameter('schema_context.default_schema')); + + $container->setDefinition(self::DECORATOR_ID, $def); + } +} diff --git a/src/Messenger/Transport/DoctrineTransportFactoryDecorator.php b/src/Messenger/Transport/DoctrineTransportFactoryDecorator.php new file mode 100644 index 0000000..2ad641b --- /dev/null +++ b/src/Messenger/Transport/DoctrineTransportFactoryDecorator.php @@ -0,0 +1,52 @@ +baggageSchemaResolver->getSchema(); + + // If we have a schema and it's not the default 'public' schema, modify the table name + if ($currentSchema !== null && $currentSchema !== $this->defaultSchema) { + $originalTableName = sprintf( + '"%s"."%s"', + $currentSchema, + $options['table_name'] ?? 'messenger_messages', + ); + + // Create transport with schema-prefixed table name + $options['table_name'] = $originalTableName; + } + + // Create transport with the original factory + return $this->decoratedFactory->createTransport($dsn, $options, $serializer); + } + + public function supports(string $dsn, array $options): bool + { + return $this->decoratedFactory->supports($dsn, $options); + } +} diff --git a/src/SchemaContextBundle.php b/src/SchemaContextBundle.php index 7eb9034..b4464be 100644 --- a/src/SchemaContextBundle.php +++ b/src/SchemaContextBundle.php @@ -4,8 +4,19 @@ namespace Macpaw\SchemaContextBundle; +use Macpaw\SchemaContextBundle\DependencyInjection\SchemaContextCompilerPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class SchemaContextBundle extends Bundle { + public function build(ContainerBuilder $container) + { + $container->addCompilerPass( + new SchemaContextCompilerPass(), + PassConfig::TYPE_BEFORE_OPTIMIZATION, + 10 + ); + } } diff --git a/tests/Messenger/Transport/DoctrineTransportFactoryDecoratorTest.php b/tests/Messenger/Transport/DoctrineTransportFactoryDecoratorTest.php new file mode 100644 index 0000000..2c60877 --- /dev/null +++ b/tests/Messenger/Transport/DoctrineTransportFactoryDecoratorTest.php @@ -0,0 +1,132 @@ +createMock(ContainerBuilder::class); + + $containerBuilder->expects(self::once()) + ->method('hasDefinition') + ->willReturn(false); + + $containerBuilder->expects(self::never()) + ->method('setDefinition'); + + $compilerPass->process($containerBuilder); + } + + public function testCompilerPassRegisterDecoratorService(): void + { + $compilerPass = new SchemaContextCompilerPass(); + + $containerBuilder = $this->createMock(ContainerBuilder::class); + + $containerBuilder->expects(self::once()) + ->method('hasDefinition') + ->willReturn(true); + + $containerBuilder->expects(self::once()) + ->method('setDefinition') + ->with( + self::equalTo(SchemaContextCompilerPass::DECORATOR_ID), + self::callback(function (Definition $definition): bool { + // You can assert partial properties here + self::assertSame( + DoctrineTransportFactoryDecorator::class, + $definition->getClass(), + ); + + self::assertFalse($definition->isPublic()); + self::assertTrue($definition->isAutowired()); + self::assertTrue($definition->isAutoconfigured()); + self::assertIsArray($definition->getDecoratedService()); + self::assertArrayHasKey(0, $definition->getDecoratedService()); + self::assertEquals( + SchemaContextCompilerPass::TARGET_ID, + $definition->getDecoratedService()[0], + ); + + // Optional: check arguments only if needed + $args = $definition->getArguments(); + self::assertArrayHasKey('$decoratedFactory', $args); + + return true; + }), + ); + + $compilerPass->process($containerBuilder); + } + + public function testSchemaIsOverride(): void + { + $doctrineTransportMock = $this->createMock(TransportFactoryInterface::class); + $baggage = new BaggageSchemaResolver(); + $baggage->setSchema('test_schema'); + + $decorator = new DoctrineTransportFactoryDecorator( + $doctrineTransportMock, + $baggage, + ); + + $doctrineTransportMock->expects(self::once()) + ->method('createTransport') + ->with( + self::equalTo(''), + self::callback(function (array $options): bool { + self::assertArrayHasKey('table_name', $options); + self::assertEquals('"test_schema"."messenger_messages"', $options['table_name']); + + return true; + }), + ); + + $decorator->createTransport('', [], $this->createMock(SerializerInterface::class)); + } + + public function testSchemaIsDefault(): void + { + $doctrineTransportMock = $this->createMock(TransportFactoryInterface::class); + $baggage = new BaggageSchemaResolver(); + $baggage->setSchema('default'); + + $decorator = new DoctrineTransportFactoryDecorator( + $doctrineTransportMock, + $baggage, + 'default', + ); + + $doctrineTransportMock->expects(self::once()) + ->method('createTransport') + ->with( + self::equalTo(''), + self::callback(function (array $options): bool { + self::assertArrayHasKey('table_name', $options); + self::assertEquals('messenger_messages', $options['table_name']); + + return true; + }), + ); + + $decorator->createTransport( + '', + ['table_name' => 'messenger_messages'], + $this->createMock(SerializerInterface::class), + ); + } +}