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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @MacPaw/platform-backend-engineers
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
/clover.xml
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"suggest": {
"symfony/doctrine-messenger": "Allow use doctrine as messenger transport"
},
"scripts": {
"composer-validate": [
"composer validate"
Expand Down
21 changes: 21 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
includes:
- phpstan-baseline.neon

parameters:
level: max
paths:
Expand Down
40 changes: 40 additions & 0 deletions src/DependencyInjection/SchemaContextCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Macpaw\SchemaContextBundle\DependencyInjection;

use Macpaw\SchemaContextBundle\Messenger\Transport\DoctrineTransportFactoryDecorator;
use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

final class SchemaContextCompilerPass implements CompilerPassInterface
{
public const TARGET_ID = 'messenger.transport.doctrine.factory';
public const DECORATOR_ID = 'messenger.doctrine_transport_factory.decorator';

public function process(ContainerBuilder $container): void
{
if ($container->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 "<decorator>.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);
}
}
52 changes: 52 additions & 0 deletions src/Messenger/Transport/DoctrineTransportFactoryDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Macpaw\SchemaContextBundle\Messenger\Transport;

use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
use Symfony\Component\Messenger\Transport\TransportInterface;

/**
* Decorator for DoctrineTransportFactory to support schema prefixes in table_name option.
*
* This decorator extends the default DoctrineTransportFactory to handle table names
* that include schema prefixes using the BaggageSchemaResolver for dynamic schema detection.
*/
final class DoctrineTransportFactoryDecorator implements TransportFactoryInterface
{
public function __construct(
private readonly TransportFactoryInterface $decoratedFactory,
private readonly BaggageSchemaResolver $baggageSchemaResolver,
private readonly string $defaultSchema = 'public',
) {
}

public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
{
// Get current schema from BaggageSchemaResolver
$currentSchema = $this->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);
}
}
11 changes: 11 additions & 0 deletions src/SchemaContextBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
132 changes: 132 additions & 0 deletions tests/Messenger/Transport/DoctrineTransportFactoryDecoratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

namespace Macpaw\SchemaContextBundle\Tests\Messenger\Transport;

use Macpaw\SchemaContextBundle\DependencyInjection\SchemaContextCompilerPass;
use Macpaw\SchemaContextBundle\Messenger\Transport\DoctrineTransportFactoryDecorator;
use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;

final class DoctrineTransportFactoryDecoratorTest extends TestCase
{
public function testCompilerPassNotRegisterDecoratorService(): void
{
$compilerPass = new SchemaContextCompilerPass();

$containerBuilder = $this->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),
);
}
}
Loading