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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
docs/superpowers/
vendor/
tests/coverage
!tests/coverage/.gitkeep
Expand Down
23 changes: 23 additions & 0 deletions packages/Dbal/src/Attribute/WithTenantResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Ecotone\Dbal\Attribute;

use Attribute;

/**
* licence Enterprise
*/
#[Attribute(Attribute::TARGET_METHOD)]
final class WithTenantResolver
{
public function __construct(public string $expression)
{
}

public function getExpression(): string
{
return $this->expression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
namespace Ecotone\Dbal\MultiTenant\Module;

use Ecotone\AnnotationFinder\AnnotationFinder;
use Ecotone\Dbal\Attribute\WithTenantResolver;
use Ecotone\Dbal\MultiTenant\HeaderBasedMultiTenantConnectionFactory;
use Ecotone\Dbal\MultiTenant\MultiTenantConfiguration;
use Ecotone\Dbal\MultiTenant\MultiTenantConnectionFactory;
use Ecotone\Dbal\MultiTenant\MultiTenantHeaderResolver;
use Ecotone\Messaging\Attribute\AsynchronousRunningEndpoint;
use Ecotone\Messaging\Attribute\ChannelAdapter;
use Ecotone\Messaging\Attribute\MessageConsumer;
use Ecotone\Messaging\Attribute\MessageGateway;
use Ecotone\Messaging\Attribute\ModuleAnnotation;
use Ecotone\Messaging\Attribute\PropagateHeaders;
Expand All @@ -18,16 +22,19 @@
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver;
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule;
use Ecotone\Messaging\Config\Configuration;
use Ecotone\Messaging\Config\ConfigurationException;
use Ecotone\Messaging\Config\Container\Definition;
use Ecotone\Messaging\Config\Container\Reference;
use Ecotone\Messaging\Config\ModulePackageList;
use Ecotone\Messaging\Config\ModuleReferenceSearchService;
use Ecotone\Messaging\Gateway\MessagingEntrypointService;
use Ecotone\Messaging\Handler\ExpressionEvaluationService;
use Ecotone\Messaging\Handler\InterfaceToCallRegistry;
use Ecotone\Messaging\Handler\Logger\LoggingGateway;
use Ecotone\Messaging\Handler\Processor\MethodInvoker\AroundInterceptorBuilder;
use Ecotone\Messaging\Handler\Processor\MethodInvoker\MethodInterceptorBuilder;
use Ecotone\Messaging\Precedence;
use Ecotone\Messaging\Support\LicensingException;
use Ecotone\Modelling\CommandBus;
use Ecotone\Modelling\EventBus;
use Ecotone\Modelling\MessageHandling\MetadataPropagator\MessageHeadersPropagatorInterceptor;
Expand All @@ -40,13 +47,55 @@
*/
final class MultiTenantConnectionFactoryModule extends NoExternalConfigurationModule implements AnnotationModule
{
/**
* @param array<int, string> $tenantResolverPlacements
* @param array<int, string> $invalidTenantResolverPlacements
*/
private function __construct(
private array $tenantResolverPlacements,
private array $invalidTenantResolverPlacements,
) {
}

public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static
{
return new self();
$allPlacements = [];
$invalid = [];
foreach ($annotationRegistrationService->findAnnotatedMethods(WithTenantResolver::class) as $annotatedMethod) {
$location = $annotatedMethod->getClassName() . '::' . $annotatedMethod->getMethodName();
$allPlacements[] = $location;

$isOnInboundAdapter = false;
foreach ($annotatedMethod->getMethodAnnotations() as $annotation) {
if ($annotation instanceof ChannelAdapter || $annotation instanceof MessageConsumer) {
$isOnInboundAdapter = true;
break;
}
}
if (! $isOnInboundAdapter) {
$invalid[] = $location;
}
}

return new self($allPlacements, $invalid);
}

public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void
{
if ($this->invalidTenantResolverPlacements !== []) {
throw ConfigurationException::create(sprintf(
"WithTenantResolver attribute on %s is invalid. WithTenantResolver may only be applied to inbound channel adapter methods (e.g. #[KafkaConsumer], #[AmqpConsumer], #[Scheduled]) where messages may arrive from outside the application without a tenant header. Internal Message Channels — including those used by synchronous and asynchronous CommandHandler / EventHandler / QueryHandler / ServiceActivator handlers — already carry the tenant context propagated from the originating bus call, so there is no header to derive there. If an asynchronous handler is processing externally-arrived messages, attach #[WithTenantResolver] to the inbound channel adapter that produces those messages, not to the handler.",
implode(', ', $this->invalidTenantResolverPlacements)
));
}

if ($this->tenantResolverPlacements !== [] && ! $messagingConfiguration->isRunningForEnterpriseLicence()) {
throw LicensingException::create(sprintf(
'WithTenantResolver attribute on %s requires Ecotone Enterprise licence.',
implode(', ', $this->tenantResolverPlacements)
));
}

$messagingConfiguration->registerMessageChannel(
SimpleMessageChannelBuilder::createPublishSubscribeChannel(HeaderBasedMultiTenantConnectionFactory::TENANT_ACTIVATED_CHANNEL_NAME)
);
Expand Down Expand Up @@ -118,6 +167,28 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
MessageGateway::class
)
);

$resolverReference = 'multi_tenant_header_resolver.' . $multiTenantConfig->getReferenceName();
$messagingConfiguration->registerServiceDefinition(
$resolverReference,
new Definition(
MultiTenantHeaderResolver::class,
[
$multiTenantConfig->getTenantHeaderName(),
Reference::to(ExpressionEvaluationService::REFERENCE),
]
)
);

$messagingConfiguration->registerBeforeMethodInterceptor(
MethodInterceptorBuilder::create(
Reference::to($resolverReference),
$interfaceToCallRegistry->getFor(MultiTenantHeaderResolver::class, 'resolve'),
Precedence::DEFAULT_PRECEDENCE,
WithTenantResolver::class,
true
)
);
}
}

Expand Down
56 changes: 56 additions & 0 deletions packages/Dbal/src/MultiTenant/MultiTenantHeaderResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Ecotone\Dbal\MultiTenant;

use Ecotone\Dbal\Attribute\WithTenantResolver;
use Ecotone\Messaging\Handler\ExpressionEvaluationService;
use Ecotone\Messaging\Message;
use Ecotone\Messaging\Support\InvalidArgumentException;

/**
* licence Enterprise
*/
final class MultiTenantHeaderResolver
{
public function __construct(
private string $tenantHeaderName,
private ExpressionEvaluationService $expressionEvaluationService,
) {
}

public function resolve(Message $message, ?WithTenantResolver $config = null): array
{
if ($config === null) {
return [];
}
if ($message->getHeaders()->containsKey($this->tenantHeaderName)) {
return [];
}

$value = $this->expressionEvaluationService->evaluate(
$config->getExpression(),
[
'payload' => $message->getPayload(),
'headers' => $message->getHeaders()->headers(),
]
);

if ($value === null) {
return [];
}

if (! is_string($value) && ! is_int($value)) {
$type = is_object($value) ? $value::class : gettype($value);
throw InvalidArgumentException::create(sprintf(
'WithTenantResolver expression for tenant header "%s" must evaluate to string|int|null, got %s. Expression: %s',
$this->tenantHeaderName,
$type,
$config->getExpression()
));
}

return [$this->tenantHeaderName => $value];
}
}
Loading
Loading