diff --git a/composer.json b/composer.json index 08e8e852c..28c76bd0b 100644 --- a/composer.json +++ b/composer.json @@ -129,6 +129,8 @@ "Symfony\\App\\MultiTenant\\": "packages/Symfony/tests/phpunit/MultiTenant/src", "Symfony\\App\\SingleTenant\\": "packages/Symfony/tests/phpunit/SingleTenant/src", "Symfony\\App\\Licence\\": "packages/Symfony/tests/phpunit/Licence/src", + "Symfony\\App\\EnvPlaceholderEndpoint\\": "packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/src", + "Symfony\\App\\EnvPlaceholderKafka\\": "packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src", "Tests\\Ecotone\\": "tests" } }, diff --git a/packages/Amqp/src/Attribute/RabbitConsumer.php b/packages/Amqp/src/Attribute/RabbitConsumer.php index 24a1ba482..60d2957bd 100644 --- a/packages/Amqp/src/Attribute/RabbitConsumer.php +++ b/packages/Amqp/src/Attribute/RabbitConsumer.php @@ -51,6 +51,7 @@ public function getDefinition(): Definition [ $this->getEndpointId(), $this->queueName, + $this->finalFailureStrategy, $this->connectionReference, ] ); diff --git a/packages/Ecotone/src/Messaging/Attribute/Asynchronous.php b/packages/Ecotone/src/Messaging/Attribute/Asynchronous.php index 01d57ff32..d5241de59 100644 --- a/packages/Ecotone/src/Messaging/Attribute/Asynchronous.php +++ b/packages/Ecotone/src/Messaging/Attribute/Asynchronous.php @@ -5,13 +5,15 @@ namespace Ecotone\Messaging\Attribute; use Attribute; +use Ecotone\Messaging\Config\Container\DefinedObject; +use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Support\Assert; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] /** * licence Apache-2.0 */ -class Asynchronous +class Asynchronous implements DefinedObject { private string|array $channelName; /** @var AsynchronousEndpointAttribute[] */ @@ -40,4 +42,9 @@ public function getAsynchronousExecution(): array { return $this->asynchronousExecution; } + + public function getDefinition(): Definition + { + return new Definition(self::class, [$this->channelName, $this->asynchronousExecution]); + } } diff --git a/packages/Ecotone/src/Messaging/Attribute/DelayedRetry.php b/packages/Ecotone/src/Messaging/Attribute/DelayedRetry.php index d3d089e50..093d9184d 100644 --- a/packages/Ecotone/src/Messaging/Attribute/DelayedRetry.php +++ b/packages/Ecotone/src/Messaging/Attribute/DelayedRetry.php @@ -5,13 +5,15 @@ namespace Ecotone\Messaging\Attribute; use Attribute; +use Ecotone\Messaging\Config\Container\DefinedObject; +use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Support\Assert; /** * licence Enterprise */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -final class DelayedRetry implements AsynchronousEndpointAttribute +final class DelayedRetry implements AsynchronousEndpointAttribute, DefinedObject { public function __construct( public readonly int $initialDelayMs, @@ -26,6 +28,17 @@ public function __construct( Assert::isTrue($deadLetterChannel === null || $deadLetterChannel !== '', 'DelayedRetry deadLetterChannel must be null or a non-empty channel name'); } + public function getDefinition(): Definition + { + return new Definition(self::class, [ + $this->initialDelayMs, + $this->multiplier, + $this->maxDelayMs, + $this->maxAttempts, + $this->deadLetterChannel, + ]); + } + public static function generateChannelName(string $handlerEndpointId): string { return 'ecotone.retry.' . $handlerEndpointId; diff --git a/packages/Ecotone/src/Messaging/Attribute/ErrorChannel.php b/packages/Ecotone/src/Messaging/Attribute/ErrorChannel.php index 137177995..4efc40223 100644 --- a/packages/Ecotone/src/Messaging/Attribute/ErrorChannel.php +++ b/packages/Ecotone/src/Messaging/Attribute/ErrorChannel.php @@ -5,13 +5,15 @@ namespace Ecotone\Messaging\Attribute; use Attribute; +use Ecotone\Messaging\Config\Container\DefinedObject; +use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Support\Assert; /** * licence Enterprise */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -class ErrorChannel implements AsynchronousEndpointAttribute +class ErrorChannel implements AsynchronousEndpointAttribute, DefinedObject { /** * @param string $errorChannelName Name of the error channel to send Message too @@ -21,4 +23,9 @@ public function __construct( ) { Assert::notNullAndEmpty($errorChannelName, 'Channel name can not be empty string'); } + + public function getDefinition(): Definition + { + return new Definition(self::class, [$this->errorChannelName]); + } } diff --git a/packages/Ecotone/src/Messaging/Attribute/WithoutDatabaseTransaction.php b/packages/Ecotone/src/Messaging/Attribute/WithoutDatabaseTransaction.php index c66cdf709..eff3fb88b 100644 --- a/packages/Ecotone/src/Messaging/Attribute/WithoutDatabaseTransaction.php +++ b/packages/Ecotone/src/Messaging/Attribute/WithoutDatabaseTransaction.php @@ -5,11 +5,17 @@ namespace Ecotone\Messaging\Attribute; use Attribute; +use Ecotone\Messaging\Config\Container\DefinedObject; +use Ecotone\Messaging\Config\Container\Definition; /** * licence Apache-2.0 */ #[Attribute] -class WithoutDatabaseTransaction implements AsynchronousEndpointAttribute +class WithoutDatabaseTransaction implements AsynchronousEndpointAttribute, DefinedObject { + public function getDefinition(): Definition + { + return new Definition(self::class); + } } diff --git a/packages/Ecotone/src/Messaging/Attribute/WithoutMessageCollector.php b/packages/Ecotone/src/Messaging/Attribute/WithoutMessageCollector.php index ad0369c78..22401a24c 100644 --- a/packages/Ecotone/src/Messaging/Attribute/WithoutMessageCollector.php +++ b/packages/Ecotone/src/Messaging/Attribute/WithoutMessageCollector.php @@ -5,11 +5,17 @@ namespace Ecotone\Messaging\Attribute; use Attribute; +use Ecotone\Messaging\Config\Container\DefinedObject; +use Ecotone\Messaging\Config\Container\Definition; /** * licence Enterprise */ #[Attribute] -class WithoutMessageCollector implements AsynchronousEndpointAttribute +class WithoutMessageCollector implements AsynchronousEndpointAttribute, DefinedObject { + public function getDefinition(): Definition + { + return new Definition(self::class); + } } diff --git a/packages/Ecotone/src/Messaging/Config/Container/DefinitionHelper.php b/packages/Ecotone/src/Messaging/Config/Container/DefinitionHelper.php index 329af7b89..e4b4af492 100644 --- a/packages/Ecotone/src/Messaging/Config/Container/DefinitionHelper.php +++ b/packages/Ecotone/src/Messaging/Config/Container/DefinitionHelper.php @@ -25,6 +25,16 @@ public static function buildDefinitionFromInstance(object $object): Definition public static function buildAttributeDefinitionFromInstance(object $object): AttributeDefinition { + if ($object instanceof DefinedObject) { + $definition = $object->getDefinition(); + + return new AttributeDefinition( + $definition->getClassName(), + $definition->getArguments(), + $definition->hasFactory() ? $definition->getFactory() : '', + ); + } + return new AttributeDefinition(get_class($object), [serialize($object)], [self::class, 'unserializeSerializedObject']); } @@ -37,7 +47,12 @@ public static function resolvePotentialComplexAttribute(AttributeDefinition $att { $attributeArguments = $attributeDefinition->getArguments(); if (self::isComplexArgument($attributeArguments)) { - return DefinitionHelper::buildDefinitionFromInstance($attributeDefinition->instance()); + $instance = $attributeDefinition->instance(); + if ($instance instanceof DefinedObject) { + return $instance->getDefinition(); + } + + return DefinitionHelper::buildDefinitionFromInstance($instance); } else { return $attributeDefinition; } diff --git a/packages/Symfony/composer.json b/packages/Symfony/composer.json index efebf148d..e9ef20a83 100644 --- a/packages/Symfony/composer.json +++ b/packages/Symfony/composer.json @@ -50,6 +50,8 @@ "doctrine/doctrine-bundle": "^2.7.2", "doctrine/orm": "^2.11|^3.0", "ecotone/dbal": "~1.313.2", + "ecotone/kafka": "~1.313.2", + "ext-rdkafka": "*", "monolog/monolog": "^2.9|^3.3.1", "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^10.5|^11.0", @@ -81,7 +83,9 @@ "Ecotone\\SymfonyBundle\\App\\": "App", "Symfony\\App\\MultiTenant\\": "tests/phpunit/MultiTenant/src", "Symfony\\App\\SingleTenant\\": "tests/phpunit/SingleTenant/src", - "Symfony\\App\\Licence\\": "tests/phpunit/Licence/src" + "Symfony\\App\\Licence\\": "tests/phpunit/Licence/src", + "Symfony\\App\\EnvPlaceholderEndpoint\\": "tests/phpunit/EnvPlaceholderEndpoint/src", + "Symfony\\App\\EnvPlaceholderKafka\\": "tests/phpunit/EnvPlaceholderKafka/src" } }, "scripts": { diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/EnvPlaceholderEndpointTest.php b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/EnvPlaceholderEndpointTest.php new file mode 100644 index 000000000..6a2ebe7f8 --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/EnvPlaceholderEndpointTest.php @@ -0,0 +1,51 @@ +boot(); + $container = $kernel->getContainer(); + $container->get(CacheClearer::class)->clear(''); + + $container->get(CommandBus::class)->sendWithRouting('order.place', 'order-1'); + + $container->get(ConfiguredMessagingSystem::class) + ->run('orders', ExecutionPollingMetadata::createWithTestingSetup()); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/config/bundles.php b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/config/bundles.php new file mode 100644 index 000000000..50a4a672a --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/config/bundles.php @@ -0,0 +1,8 @@ + ['all' => true], + EcotoneSymfonyBundle::class => ['all' => true], +]; diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/config/services.php b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/config/services.php new file mode 100644 index 000000000..244eb3036 --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/config/services.php @@ -0,0 +1,20 @@ +extension('ecotone', [ + 'skippedModulePackageNames' => ModulePackageList::allPackagesExcept([ + ModulePackageList::SYMFONY_PACKAGE, + ModulePackageList::ASYNCHRONOUS_PACKAGE, + ]), + 'licenceKey' => '%env(SYMFONY_LICENCE_KEY)%', + ]); + + $services = $containerConfigurator->services(); + + $services->load('Symfony\\App\\EnvPlaceholderEndpoint\\', '%kernel.project_dir%/src/') + ->autowire() + ->autoconfigure(); +}; diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/src/Configuration/EcotoneConfiguration.php b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/src/Configuration/EcotoneConfiguration.php new file mode 100644 index 000000000..8f553c8a1 --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderEndpoint/src/Configuration/EcotoneConfiguration.php @@ -0,0 +1,22 @@ + AttributeDefinition::fromObject) + * and embedded into the consumer's runtime definition once the DBAL error-handling + * path is active. Symfony then rewrites the %env()% placeholder inside the serialized + * blob, breaking its length prefix, so the consumer can never be built and the + * published Message is never consumed. + * + * licence Enterprise + * @internal + */ +final class EnvPlaceholderKafkaConsumerTest extends TestCase +{ + protected function setUp(): void + { + putenv('SYMFONY_LICENCE_KEY=' . LicenceTesting::VALID_LICENCE); + putenv('ECOTONE_KAFKA_SUFFIX=it' . substr(Uuid::v4()->toRfc4122(), 0, 8)); + + (new Filesystem())->remove([__DIR__ . '/var', '/tmp/ecotone']); + } + + protected function tearDown(): void + { + putenv('SYMFONY_LICENCE_KEY'); + putenv('ECOTONE_KAFKA_SUFFIX'); + + restore_exception_handler(); + } + + public function test_publishing_and_consuming_from_topic_built_from_env_placeholder(): void + { + $kernel = new Kernel('test', true); + $kernel->boot(); + $container = $kernel->getContainer(); + + $payload = Uuid::v7()->toRfc4122(); + $container->get(MessagePublisher::class)->send($payload); + + $container->get(ConfiguredMessagingSystem::class) + ->run('ordersKafkaConsumer', ExecutionPollingMetadata::createWithTestingSetup( + maxExecutionTimeInMilliseconds: 30000, + )); + + $this->assertSame( + [$payload], + $container->get(QueryBus::class)->sendWithRouting('ordersKafka.consumedPayloads'), + ); + } +} diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/config/bundles.php b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/config/bundles.php new file mode 100644 index 000000000..50a4a672a --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/config/bundles.php @@ -0,0 +1,8 @@ + ['all' => true], + EcotoneSymfonyBundle::class => ['all' => true], +]; diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/config/services.php b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/config/services.php new file mode 100644 index 000000000..f0e3cf684 --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/config/services.php @@ -0,0 +1,33 @@ +extension('ecotone', [ + 'skippedModulePackageNames' => ModulePackageList::allPackagesExcept([ + ModulePackageList::SYMFONY_PACKAGE, + ModulePackageList::ASYNCHRONOUS_PACKAGE, + ModulePackageList::KAFKA_PACKAGE, + ModulePackageList::DBAL_PACKAGE, + ]), + 'licenceKey' => '%env(SYMFONY_LICENCE_KEY)%', + ]); + + $services = $containerConfigurator->services(); + + $services->set(KafkaBrokerConfiguration::class) + ->factory([KafkaBrokerConfiguration::class, 'createWithDefaults']) + ->args([['%env(KAFKA_DSN)%']]) + ->public(); + + $services->set(DbalConnectionFactory::class) + ->args(['%env(DATABASE_DSN)%']) + ->public(); + + $services->load('Symfony\\App\\EnvPlaceholderKafka\\', '%kernel.project_dir%/src/') + ->autowire() + ->autoconfigure(); +}; diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src/Configuration/EcotoneConfiguration.php b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src/Configuration/EcotoneConfiguration.php new file mode 100644 index 000000000..ff61601ef --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src/Configuration/EcotoneConfiguration.php @@ -0,0 +1,21 @@ +withHeaderMapper('*'); + } +} diff --git a/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src/Configuration/Kernel.php b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src/Configuration/Kernel.php new file mode 100644 index 000000000..173ecf528 --- /dev/null +++ b/packages/Symfony/tests/phpunit/EnvPlaceholderKafka/src/Configuration/Kernel.php @@ -0,0 +1,18 @@ +consumedPayloads[] = $payload; + } + + /** + * @return string[] + */ + #[QueryHandler('ordersKafka.consumedPayloads')] + public function consumedPayloads(): array + { + return $this->consumedPayloads; + } +}