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

nepada / message-bus-nette / 5726083386

pending completion
5726083386

push

github

web-flow
Update phpstan/phpstan requirement from 1.10.22 to 1.10.26 (#105)

128 of 140 relevant lines covered (91.43%)

0.91 hits per line

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

91.79
/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 Nette\Utils\Strings;
34
use Psr\Container\ContainerInterface;
35
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
36
use Symfony\Component\Messenger\MessageBus;
37
use Symfony\Component\Messenger\MessageBusInterface;
38
use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware;
39
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
40

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

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

51
    public function getConfigSchema(): Nette\Schema\Schema
52
    {
53
        $serviceDefinition = Expect::structure([
1✔
54
            'class' => Expect::string()->required(),
1✔
55
            'arguments' => Expect::array(),
1✔
56
        ])
57
            ->castTo('array')
1✔
58
            ->assert(fn (array $definition): bool => class_exists($definition['class']), 'Class must exist');
1✔
59
        $serviceReference = Expect::string()
1✔
60
            ->assert(fn (string $service): bool => Strings::startsWith($service, '@'));
1✔
61
        $service = Expect::anyOf($serviceDefinition, $serviceReference)
1✔
62
            ->before(function ($type) {
1✔
63
                if (is_string($type) && ! Strings::startsWith($type, '@')) {
64
                    return ['class' => $type];
65
                }
66
                return $type;
67
            });
1✔
68
        return Expect::structure([
1✔
69
            'logger' => $service,
1✔
70
            'allowNestedCommandHandling' => Expect::bool(false),
1✔
71
            'clearEntityManager' => Expect::structure([
1✔
72
                'onStart' => Expect::bool(true),
1✔
73
                'onSuccess' => Expect::bool(true),
1✔
74
                'onError' => Expect::bool(true),
1✔
75
            ]),
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
     * @return string
137
     */
138
    protected function resolveLoggerService($definition): string
139
    {
140
        if (is_string($definition)) {
×
141
            return $definition;
×
142
        }
143

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

241
        return $serviceNameByMessageType;
1✔
242
    }
243

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

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

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

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

279
        return $serviceNamesByMessageType;
1✔
280
    }
281

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

287
}
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

© 2026 Coveralls, Inc