• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nepada / message-bus-nette / 7740739014

01 Feb 2024 11:49AM UTC coverage: 91.489%. Remained the same
7740739014

push

github

web-flow
Update nette/tester requirement from 2.5.1 to 2.5.2 (#119)

129 of 141 relevant lines covered (91.49%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.85
/src/MessageBusNette/DI/MessageBusExtension.php
1
<?php
2
declare(strict_types = 1);
3

4
namespace Nepada\MessageBusNette\DI;
5

6
use Nepada\MessageBus\Commands\CommandBus;
7
use Nepada\MessageBus\Commands\CommandHandler;
8
use Nepada\MessageBus\Commands\CommandHandlerLocator;
9
use Nepada\MessageBus\Commands\MessengerCommandBus;
10
use Nepada\MessageBus\Events\EventDispatcher;
11
use Nepada\MessageBus\Events\EventSubscriber;
12
use Nepada\MessageBus\Events\EventSubscribersLocator;
13
use Nepada\MessageBus\Events\MessengerEventDispatcher;
14
use Nepada\MessageBus\Logging\LogMessageResolver;
15
use Nepada\MessageBus\Logging\MessageContextResolver;
16
use Nepada\MessageBus\Logging\PrivateClassPropertiesExtractor;
17
use Nepada\MessageBus\Middleware\LoggingMiddleware;
18
use Nepada\MessageBus\Middleware\PreventNestedHandlingMiddleware;
19
use Nepada\MessageBus\StaticAnalysis\ConfigurableHandlerValidator;
20
use Nepada\MessageBus\StaticAnalysis\HandlerType;
21
use Nepada\MessageBus\StaticAnalysis\MessageHandlerValidationConfiguration;
22
use Nepada\MessageBus\StaticAnalysis\MessageHandlerValidator;
23
use Nepada\MessageBus\StaticAnalysis\MessageTypeExtractor;
24
use Nepada\MessageBus\StaticAnalysis\StaticAnalysisFailedException;
25
use Nepada\MessageBusDoctrine\Events\DispatchRecordedEventsFromEntities;
26
use Nepada\MessageBusDoctrine\Middleware\ClearEntityManagerMiddleware;
27
use Nepada\MessageBusDoctrine\Middleware\PreventOuterTransactionMiddleware;
28
use Nepada\MessageBusDoctrine\Middleware\TransactionMiddleware;
29
use Nette;
30
use Nette\DI\CompilerExtension;
31
use Nette\DI\Definitions\ServiceDefinition;
32
use Nette\Schema\Expect;
33
use Psr\Container\ContainerInterface;
34
use Symfony\Component\Messenger\MessageBus;
35
use Symfony\Component\Messenger\MessageBusInterface;
36
use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware;
37
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
38
use function str_starts_with;
39

40
/**
41
 * @property \stdClass $config
42
 */
43
class MessageBusExtension extends CompilerExtension
44
{
45

46
    private const PSR_CONTAINER_SERVICE = 'serviceLocator';
47
    private const COMMAND_HANDLER_LOCATOR_SERVICE = 'commands.handlerLocator';
48
    private const EVENT_SUBSCRIBER_LOCATOR_SERVICE = 'events.handlerLocator';
49

50
    public function getConfigSchema(): Nette\Schema\Schema
51
    {
52
        $serviceDefinition = Expect::structure([
1✔
53
            'class' => Expect::string()->required(),
1✔
54
            'arguments' => Expect::array(),
1✔
55
        ])
56
            ->castTo('array')
1✔
57
            ->assert(fn (array $definition): bool => class_exists($definition['class']), 'Class must exist');
1✔
58
        $serviceReference = Expect::string()
1✔
59
            ->assert(fn (string $service): bool => str_starts_with($service, '@'));
1✔
60
        $service = Expect::anyOf($serviceDefinition, $serviceReference)
1✔
61
            ->before(function ($type): mixed {
1✔
62
                if (is_string($type) && ! str_starts_with($type, '@')) {
63
                    return ['class' => $type];
64
                }
65
                return $type;
66
            });
1✔
67
        return Expect::structure([
1✔
68
            'logger' => $service,
1✔
69
            'allowNestedCommandHandling' => Expect::bool(false),
1✔
70
            'clearEntityManager' => Expect::structure([
1✔
71
                'onStart' => Expect::bool(true),
1✔
72
                'onSuccess' => Expect::bool(true),
1✔
73
                'onError' => Expect::bool(true),
1✔
74
            ]),
75
            'bleedingEdge' => Expect::bool(false),
1✔
76
        ]);
77
    }
78

79
    public function loadConfiguration(): void
80
    {
81
        $this->getContainerBuilder()->addDefinition($this->prefix(self::PSR_CONTAINER_SERVICE))
1✔
82
            ->setType(ContainerInterface::class)
1✔
83
            ->setFactory(PsrContainerAdapter::class)
1✔
84
            ->setAutowired(false);
1✔
85
        if ($this->isDoctrineSupportAvailable()) {
1✔
86
            $this->setupDoctrine();
1✔
87
        }
88
        $this->setupMiddlewares();
1✔
89
        $this->setupCommandBus();
1✔
90
        $this->setupEventBus();
1✔
91
    }
1✔
92

93
    protected function setupDoctrine(): void
94
    {
95
        $container = $this->getContainerBuilder();
1✔
96
        $container->addDefinition($this->prefix('doctrine.dispatchRecordedEventsFromEntities'))
1✔
97
            ->setType(DispatchRecordedEventsFromEntities::class);
1✔
98
        $container->addDefinition($this->prefix('middleware.preventOuterTransaction'))
1✔
99
            ->setType(PreventOuterTransactionMiddleware::class);
1✔
100
        $container->addDefinition($this->prefix('middleware.transaction'))
1✔
101
            ->setType(TransactionMiddleware::class);
1✔
102
        $container->addDefinition($this->prefix('middleware.clearEntityManager'))
1✔
103
            ->setType(ClearEntityManagerMiddleware::class)
1✔
104
            ->setArguments([
1✔
105
                'clearOnStart' => $this->config->clearEntityManager->onStart,
1✔
106
                'clearOnSuccess' => $this->config->clearEntityManager->onSuccess,
1✔
107
                'clearOnError' => $this->config->clearEntityManager->onError,
1✔
108
            ]);
109
    }
1✔
110

111
    protected function setupMiddlewares(): void
112
    {
113
        $container = $this->getContainerBuilder();
1✔
114

115
        $container->addDefinition($this->prefix('middleware.dispatchAfterCurrentBus'))
1✔
116
            ->setType(DispatchAfterCurrentBusMiddleware::class);
1✔
117

118
        $container->addDefinition($this->prefix('middleware.preventNestedHandling'))
1✔
119
            ->setType(PreventNestedHandlingMiddleware::class);
1✔
120

121
        $container->addDefinition($this->prefix('logging.privateClassPropertiesExtractor'))
1✔
122
            ->setType(PrivateClassPropertiesExtractor::class);
1✔
123
        $container->addDefinition($this->prefix('logging.logMessageResolver'))
1✔
124
            ->setType(LogMessageResolver::class);
1✔
125
        $container->addDefinition($this->prefix('logging.messageContextResolver'))
1✔
126
            ->setType(MessageContextResolver::class);
1✔
127
        $loggingMiddleware = $container->addDefinition($this->prefix('middleware.logging'))
1✔
128
            ->setType(LoggingMiddleware::class);
1✔
129
        if (isset($this->config->logger)) {
1✔
130
            $loggingMiddleware->setArgument('logger', $this->resolveLoggerService($this->config->logger));
×
131
        }
132
    }
1✔
133

134
    /**
135
     * @param string|array{class: string, arguments: array<mixed>|null} $definition
136
     */
137
    protected function resolveLoggerService(string|array $definition): string
138
    {
139
        if (is_string($definition)) {
×
140
            return $definition;
×
141
        }
142

143
        $serviceName = $this->prefix('logging.logger');
×
144
        $this->getContainerBuilder()->addDefinition($serviceName)
×
145
            ->setType($definition['class'])
×
146
            ->setArguments($definition['arguments'] ?? [])
×
147
            ->setAutowired(false);
×
148

149
        return "@{$serviceName}";
×
150
    }
151

152
    protected function setupCommandBus(): void
153
    {
154
        $container = $this->getContainerBuilder();
1✔
155

156
        $handlerLocator = $container->addDefinition($this->prefix(self::COMMAND_HANDLER_LOCATOR_SERVICE))
1✔
157
            ->setType(CommandHandlerLocator::class)
1✔
158
            ->setArguments(['container' => $this->prefix('@' . self::PSR_CONTAINER_SERVICE)]);
1✔
159

160
        $middlewares = [
1✔
161
            $this->prefix('@middleware.dispatchAfterCurrentBus'),
1✔
162
            $this->prefix('@middleware.logging'),
1✔
163
        ];
164
        if (! $this->config->allowNestedCommandHandling) {
1✔
165
            $middlewares[] = $this->prefix('@middleware.preventNestedHandling');
1✔
166
        }
167
        if ($this->isDoctrineSupportAvailable()) {
1✔
168
            $middlewares[] = $this->prefix('@middleware.preventOuterTransaction');
1✔
169
            $middlewares[] = $this->prefix('@middleware.clearEntityManager');
1✔
170
            $middlewares[] = $this->prefix('@middleware.transaction');
1✔
171
        }
172
        $middlewares[] = $container->addDefinition($this->prefix('commands.handleMessageMiddleware'))
1✔
173
            ->setFactory(HandleMessageMiddleware::class, [$handlerLocator, false]);
1✔
174

175
        $messageBus = $container->addDefinition($this->prefix('commands.messageBus'))
1✔
176
            ->setType(MessageBusInterface::class)
1✔
177
            ->setFactory(MessageBus::class, [$middlewares]);
1✔
178

179
        $container->addDefinition($this->prefix('commands.commandBus'))
1✔
180
            ->setType(CommandBus::class)
1✔
181
            ->setFactory(MessengerCommandBus::class, ['messageBus' => $messageBus]);
1✔
182
    }
1✔
183

184
    protected function setupEventBus(): void
185
    {
186
        $container = $this->getContainerBuilder();
1✔
187

188
        $handlerLocator = $container->addDefinition($this->prefix(self::EVENT_SUBSCRIBER_LOCATOR_SERVICE))
1✔
189
            ->setType(EventSubscribersLocator::class)
1✔
190
            ->setArguments(['container' => $this->prefix('@' . self::PSR_CONTAINER_SERVICE)]);
1✔
191

192
        $middlewares = [
1✔
193
            $this->prefix('@middleware.dispatchAfterCurrentBus'),
1✔
194
            $this->prefix('@middleware.logging'),
1✔
195
        ];
196
        $middlewares[] = $container->addDefinition($this->prefix('events.handleMessageMiddleware'))
1✔
197
            ->setFactory(HandleMessageMiddleware::class, [$handlerLocator, true]);
1✔
198

199
        $messageBus = $container->addDefinition($this->prefix('events.messageBus'))
1✔
200
            ->setType(MessageBusInterface::class)
1✔
201
            ->setFactory(MessageBus::class, [$middlewares]);
1✔
202

203
        $container->addDefinition($this->prefix('events.eventDispatcher'))
1✔
204
            ->setType(EventDispatcher::class)
1✔
205
            ->setFactory(MessengerEventDispatcher::class, ['messageBus' => $messageBus]);
1✔
206
    }
1✔
207

208
    /**
209
     * @throws StaticAnalysisFailedException
210
     */
211
    public function beforeCompile(): void
212
    {
213
        $container = $this->getContainerBuilder();
1✔
214

215
        $definition = $container->getDefinition($this->prefix(self::COMMAND_HANDLER_LOCATOR_SERVICE));
1✔
216
        assert($definition instanceof ServiceDefinition);
1✔
217
        $definition->setArgument('serviceNameByMessageType', $this->findAndValidateCommandHandlers());
1✔
218

219
        $definition = $container->getDefinition($this->prefix(self::EVENT_SUBSCRIBER_LOCATOR_SERVICE));
1✔
220
        assert($definition instanceof ServiceDefinition);
1✔
221
        $definition->setArgument('serviceNamesByMessageType', $this->findAndValidateEventSubscribers());
1✔
222
    }
1✔
223

224
    /**
225
     * @return string[]
226
     * @throws StaticAnalysisFailedException
227
     */
228
    protected function findAndValidateCommandHandlers(): array
229
    {
230
        $handlerValidator = new ConfigurableHandlerValidator(MessageHandlerValidationConfiguration::command($this->config->bleedingEdge));
1✔
231

232
        $serviceNameByMessageType = [];
1✔
233
        foreach ($this->findAndValidateMessageHandlers($handlerValidator, CommandHandler::class) as $messageType => $serviceNames) {
1✔
234
            if (count($serviceNames) !== 1) {
1✔
235
                throw new \LogicException(sprintf('Multiple command handler services (%s) for command %s.', implode(', ', $serviceNames), $messageType));
×
236
            }
237
            $serviceNameByMessageType[$messageType] = reset($serviceNames);
1✔
238
        }
239

240
        return $serviceNameByMessageType;
1✔
241
    }
242

243
    /**
244
     * @return string[][]
245
     * @throws StaticAnalysisFailedException
246
     */
247
    protected function findAndValidateEventSubscribers(): array
248
    {
249
        $subscriberValidator = new ConfigurableHandlerValidator(MessageHandlerValidationConfiguration::event($this->config->bleedingEdge));
1✔
250
        return $this->findAndValidateMessageHandlers($subscriberValidator, EventSubscriber::class);
1✔
251
    }
252

253
    /**
254
     * @param class-string<EventSubscriber|CommandHandler> $handlerType
255
     * @return string[][]
256
     * @throws StaticAnalysisFailedException
257
     */
258
    protected function findAndValidateMessageHandlers(MessageHandlerValidator $messageHandlerValidator, string $handlerType): array
1✔
259
    {
260
        $containerBuilder = $this->getContainerBuilder();
1✔
261
        $messageTypeExtractor = new MessageTypeExtractor();
1✔
262

263
        $serviceNamesByMessageType = [];
1✔
264
        foreach ($containerBuilder->findByType($handlerType) as $serviceName => $serviceDefinition) {
1✔
265
            /** @var class-string<EventSubscriber|CommandHandler>|null $handlerTypeString allow-narrowing */
266
            $handlerTypeString = $serviceDefinition->getType();
1✔
267
            if ($handlerTypeString === null) {
1✔
268
                throw new \LogicException('Type of handler service type must be defined in this context.');
×
269
            }
270
            $handlerType = HandlerType::fromString($handlerTypeString);
1✔
271
            $messageHandlerValidator->validate($handlerType);
1✔
272

273
            $messageType = $messageTypeExtractor->extract($handlerType);
1✔
274
            $serviceNamesByMessageType[$messageType->toString()][] = (string) $serviceName;
1✔
275
        }
276

277
        return $serviceNamesByMessageType;
1✔
278
    }
279

280
    private function isDoctrineSupportAvailable(): bool
281
    {
282
        return class_exists(TransactionMiddleware::class);
1✔
283
    }
284

285
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc