Completed
Push — master ( 3f74db...61022b )
by Vincent
15s queued 11s
created

SonataNotificationExtension   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 9
dl 0
loc 458
rs 3.52
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
B load() 0 57 8
A configureClass() 0 8 1
A configureAdmin() 0 6 1
A registerParameters() 0 5 1
A configureBackends() 0 28 5
A registerDoctrineMapping() 0 17 1
B checkConfiguration() 0 14 7
A configureListeners() 0 20 3
C configureDoctrineBackends() 0 65 13
A createDoctrineQueueBackend() 0 15 2
C configureRabbitmq() 0 93 12
A createAMQPBackend() 0 22 1
A registerSonataDoctrineMapping() 0 7 1
A getQueuesParameters() 0 13 2
A getAMQPDeadLetterExchangeByRoutingKey() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like SonataNotificationExtension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SonataNotificationExtension, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\NotificationBundle\DependencyInjection;
15
16
use Sonata\Doctrine\Mapper\DoctrineCollector;
17
use Sonata\EasyExtendsBundle\Mapper\DoctrineCollector as DeprecatedDoctrineCollector;
18
use Sonata\NotificationBundle\Backend\AMQPBackend;
19
use Sonata\NotificationBundle\Backend\MessageManagerBackend;
20
use Sonata\NotificationBundle\Model\MessageInterface;
21
use Symfony\Component\Config\FileLocator;
22
use Symfony\Component\DependencyInjection\ContainerBuilder;
23
use Symfony\Component\DependencyInjection\Definition;
24
use Symfony\Component\DependencyInjection\Loader;
25
use Symfony\Component\DependencyInjection\Reference;
26
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
27
28
class SonataNotificationExtension extends Extension
29
{
30
    /**
31
     * @var int
32
     */
33
    protected $amqpCounter = 0;
34
35
    /**
36
     * {@inheritdoc}
37
     */
38
    public function load(array $configs, ContainerBuilder $container): void
39
    {
40
        $configuration = new Configuration();
41
        $config = $this->processConfiguration($configuration, $configs);
42
43
        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
44
45
        $this->checkConfiguration($config);
46
47
        $loader->load('core.xml');
48
49
        $loader->load('backend.xml');
50
        $loader->load('consumer.xml');
51
        $loader->load('command.xml');
52
53
        $bundles = $container->getParameter('kernel.bundles');
54
55
        if ('sonata.notification.backend.doctrine' === $config['backend']) {
56
            $loader->load('doctrine_orm.xml');
57
            $loader->load('selector.xml');
58
            $loader->load('event.xml');
59
60
            if (isset($bundles['FOSRestBundle'], $bundles['NelmioApiDocBundle'])) {
61
                $loader->load('api_controllers.xml');
62
                $loader->load('api_form.xml');
63
            }
64
65
            // for now, only support for ORM
66
            if ($config['admin']['enabled'] && isset($bundles['SonataDoctrineORMAdminBundle'])) {
67
                $loader->load('admin.xml');
68
            }
69
        }
70
71
        if ($config['consumers']['register_default']) {
72
            $loader->load('default_consumers.xml');
73
        }
74
75
        if (isset($bundles['LiipMonitorBundle'])) {
76
            $loader->load('checkmonitor.xml');
77
        }
78
79
        $container->setAlias('sonata.notification.backend', $config['backend'])->setPublic(true);
80
        $container->setParameter('sonata.notification.backend', $config['backend']);
81
82
        if (isset($bundles['SonataDoctrineBundle'])) {
83
            $this->registerSonataDoctrineMapping($config);
84
        } else {
85
            // NEXT MAJOR: Remove next line and throw error when not registering SonataDoctrineBundle
86
            $this->registerDoctrineMapping($config);
87
        }
88
89
        $this->registerParameters($container, $config);
90
        $this->configureBackends($container, $config);
91
        $this->configureClass($container, $config);
92
        $this->configureListeners($container, $config);
93
        $this->configureAdmin($container, $config);
94
    }
95
96
    /**
97
     * @param array $config
98
     */
99
    public function configureClass(ContainerBuilder $container, $config): void
100
    {
101
        // admin configuration
102
        $container->setParameter('sonata.notification.admin.message.entity', $config['class']['message']);
103
104
        // manager configuration
105
        $container->setParameter('sonata.notification.manager.message.entity', $config['class']['message']);
106
    }
107
108
    /**
109
     * @param array $config
110
     */
111
    public function configureAdmin(ContainerBuilder $container, $config): void
112
    {
113
        $container->setParameter('sonata.notification.admin.message.class', $config['admin']['message']['class']);
114
        $container->setParameter('sonata.notification.admin.message.controller', $config['admin']['message']['controller']);
115
        $container->setParameter('sonata.notification.admin.message.translation_domain', $config['admin']['message']['translation']);
116
    }
117
118
    /**
119
     * @param array $config
120
     */
121
    public function registerParameters(ContainerBuilder $container, $config): void
122
    {
123
        $container->setParameter('sonata.notification.message.class', $config['class']['message']);
124
        $container->setParameter('sonata.notification.admin.message.entity', $config['class']['message']);
125
    }
126
127
    /**
128
     * @param array $config
129
     */
130
    public function configureBackends(ContainerBuilder $container, $config): void
131
    {
132
        if (isset($config['backends']['rabbitmq']) && 'sonata.notification.backend.rabbitmq' === $config['backend']) {
133
            $this->configureRabbitmq($container, $config);
134
        } else {
135
            $container->removeDefinition('sonata.notification.backend.rabbitmq');
136
        }
137
138
        if (isset($config['backends']['doctrine']) && 'sonata.notification.backend.doctrine' === $config['backend']) {
139
            $checkLevel = [
140
                MessageInterface::STATE_DONE => $config['backends']['doctrine']['states']['done'],
141
                MessageInterface::STATE_ERROR => $config['backends']['doctrine']['states']['error'],
142
                MessageInterface::STATE_IN_PROGRESS => $config['backends']['doctrine']['states']['in_progress'],
143
                MessageInterface::STATE_OPEN => $config['backends']['doctrine']['states']['open'],
144
            ];
145
146
            $pause = $config['backends']['doctrine']['pause'];
147
            $maxAge = $config['backends']['doctrine']['max_age'];
148
            $batchSize = $config['backends']['doctrine']['batch_size'];
149
            $container
150
                ->setAlias('sonata.notification.manager.message', $config['backends']['doctrine']['message_manager'])
151
                ->setPublic(true);
152
153
            $this->configureDoctrineBackends($container, $config, $checkLevel, $pause, $maxAge, $batchSize);
0 ignored issues
show
Documentation introduced by
$checkLevel is of type array, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
154
        } else {
155
            $container->removeDefinition('sonata.notification.backend.doctrine');
156
        }
157
    }
158
159
    /**
160
     * NEXT_MAJOR: Remove this method.
161
     */
162
    public function registerDoctrineMapping(array $config): void
163
    {
164
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
165
            'Using SonataEasyExtendsBundle is deprecated since sonata-project/notification-bundle 3.9. Please register SonataDoctrineBundle as a bundle instead.',
166
            E_USER_DEPRECATED
167
        );
168
169
        $collector = DeprecatedDoctrineCollector::getInstance();
170
171
        $collector->addIndex($config['class']['message'], 'idx_state', [
172
            'state',
173
        ]);
174
175
        $collector->addIndex($config['class']['message'], 'idx_created_at', [
176
            'created_at',
177
        ]);
178
    }
179
180
    protected function checkConfiguration(array $config): void
181
    {
182
        if (isset($config['backends']) && \count($config['backends']) > 1) {
183
            throw new \RuntimeException('more than one backend configured, you can have only one backend configuration');
184
        }
185
186
        if (!isset($config['backends']['rabbitmq']) && 'sonata.notification.backend.rabbitmq' === $config['backend']) {
187
            throw new \RuntimeException('Please configure the sonata_notification.backends.rabbitmq section');
188
        }
189
190
        if (!isset($config['backends']['doctrine']) && 'sonata.notification.backend.doctrine' === $config['backend']) {
191
            throw new \RuntimeException('Please configure the sonata_notification.backends.doctrine section');
192
        }
193
    }
194
195
    protected function configureListeners(ContainerBuilder $container, array $config): void
196
    {
197
        $ids = $config['iteration_listeners'];
198
199
        if ('sonata.notification.backend.doctrine' === $config['backend']) {
200
            // this one clean the unit of work after every iteration
201
            $ids[] = 'sonata.notification.event.doctrine_optimize';
202
203
            if ($config['backends']['doctrine']['batch_size'] > 1) {
204
                // if the backend is doctrine and the batch size > 1, then
205
                // the unit of work must be cleaned wisely to avoid any issue
206
                // while persisting entities
207
                $ids = [
208
                    'sonata.notification.event.doctrine_backend_optimize',
209
                ];
210
            }
211
        }
212
213
        $container->setParameter('sonata.notification.event.iteration_listeners', $ids);
214
    }
215
216
    /**
217
     * @param bool $checkLevel
218
     * @param int  $pause
219
     * @param int  $maxAge
220
     * @param int  $batchSize
221
     *
222
     * @throws \RuntimeException
223
     */
224
    protected function configureDoctrineBackends(ContainerBuilder $container, array $config, $checkLevel, $pause, $maxAge, $batchSize): void
225
    {
226
        $queues = $config['queues'];
227
        $qBackends = [];
228
229
        $definition = $container->getDefinition('sonata.notification.backend.doctrine');
230
231
        // no queue defined, set a default one
232
        if (0 === \count($queues)) {
233
            $queues = [[
234
                'queue' => 'default',
235
                'default' => true,
236
                'types' => [],
237
            ]];
238
        }
239
240
        $defaultSet = false;
241
        $declaredQueues = [];
242
243
        foreach ($queues as $pos => &$queue) {
244
            if (\in_array($queue['queue'], $declaredQueues, true)) {
245
                throw new \RuntimeException('The doctrine backend does not support 2 identicals queue name, please rename one queue');
246
            }
247
248
            $declaredQueues[] = $queue['queue'];
249
250
            // make the configuration compatible with old code and rabbitmq
251
            if (isset($queue['routing_key']) && \strlen($queue['routing_key']) > 0) {
252
                $queue['types'] = [$queue['routing_key']];
253
            }
254
255
            if (empty($queue['types']) && false === $queue['default']) {
256
                throw new \RuntimeException('You cannot declared a doctrine queue with no type defined with default = false');
257
            }
258
259
            if (!empty($queue['types']) && true === $queue['default']) {
260
                throw new \RuntimeException('You cannot declared a doctrine queue with types defined with default = true');
261
            }
262
263
            $id = $this->createDoctrineQueueBackend($container, $definition->getArgument(0), $checkLevel, $pause, $maxAge, $batchSize, $queue['queue'], $queue['types']);
264
            $qBackends[$pos] = [
265
                'types' => $queue['types'],
266
                'backend' => new Reference($id),
267
            ];
268
269
            if (true === $queue['default']) {
270
                if (true === $defaultSet) {
271
                    throw new \RuntimeException('You can only set one doctrine default queue in your sonata notification configuration.');
272
                }
273
274
                $defaultSet = true;
275
                $defaultQueue = $queue['queue'];
276
            }
277
        }
278
279
        if (false === $defaultSet) {
280
            throw new \RuntimeException('You need to specify a valid default queue for the doctrine backend!');
281
        }
282
283
        $definition
284
            ->replaceArgument(1, $queues)
285
            ->replaceArgument(2, $defaultQueue)
0 ignored issues
show
Bug introduced by
The variable $defaultQueue does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
286
            ->replaceArgument(3, $qBackends)
287
        ;
288
    }
289
290
    /**
291
     * @param string $manager
292
     * @param bool   $checkLevel
293
     * @param int    $pause
294
     * @param int    $maxAge
295
     * @param int    $batchSize
296
     * @param string $key
297
     *
298
     * @return string
299
     */
300
    protected function createDoctrineQueueBackend(ContainerBuilder $container, $manager, $checkLevel, $pause, $maxAge, $batchSize, $key, array $types = [])
301
    {
302
        if ('' === $key) {
303
            $id = 'sonata.notification.backend.doctrine.default_'.$this->amqpCounter++;
304
        } else {
305
            $id = 'sonata.notification.backend.doctrine.'.$key;
306
        }
307
308
        $definition = new Definition(MessageManagerBackend::class, [$manager, $checkLevel, $pause, $maxAge, $batchSize, $types]);
309
        $definition->setPublic(false);
310
311
        $container->setDefinition($id, $definition);
312
313
        return $id;
314
    }
315
316
    protected function configureRabbitmq(ContainerBuilder $container, array $config): void
317
    {
318
        $queues = $config['queues'];
319
        $connection = $config['backends']['rabbitmq']['connection'];
320
        $baseExchange = $config['backends']['rabbitmq']['exchange'];
321
        $amqBackends = [];
322
323
        if (0 === \count($queues)) {
324
            $queues = [[
325
                'queue' => 'default',
326
                'default' => true,
327
                'routing_key' => '',
328
                'recover' => false,
329
                'dead_letter_exchange' => null,
330
                'dead_letter_routing_key' => null,
331
                'ttl' => null,
332
                'prefetch_count' => null,
333
            ]];
334
        }
335
336
        $deadLetterRoutingKeys = $this->getQueuesParameters('dead_letter_routing_key', $queues);
337
        $routingKeys = $this->getQueuesParameters('routing_key', $queues);
338
339
        foreach ($deadLetterRoutingKeys as $key) {
340
            if (!\in_array($key, $routingKeys, true)) {
341
                throw new \RuntimeException(sprintf(
342
                    'You must configure the queue having the routing_key "%s" same as dead_letter_routing_key',
343
                    $key
344
                ));
345
            }
346
        }
347
348
        $declaredQueues = [];
349
350
        $defaultSet = false;
351
        foreach ($queues as $pos => $queue) {
352
            if (\in_array($queue['queue'], $declaredQueues, true)) {
353
                throw new \RuntimeException('The RabbitMQ backend does not support 2 identicals queue name, please rename one queue');
354
            }
355
356
            $declaredQueues[] = $queue['queue'];
357
358
            if ($queue['dead_letter_routing_key']) {
359
                if (null === $queue['dead_letter_exchange']) {
360
                    throw new \RuntimeException(
361
                        'dead_letter_exchange must be configured when dead_letter_routing_key is set'
362
                    );
363
                }
364
            }
365
366
            if (\in_array($queue['routing_key'], $deadLetterRoutingKeys, true)) {
367
                $exchange = $this->getAMQPDeadLetterExchangeByRoutingKey($queue['routing_key'], $queues);
368
            } else {
369
                $exchange = $baseExchange;
370
            }
371
372
            $id = $this->createAMQPBackend(
373
                $container,
374
                $exchange,
375
                $queue['queue'],
376
                $queue['recover'],
377
                $queue['routing_key'],
378
                $queue['dead_letter_exchange'],
379
                $queue['dead_letter_routing_key'],
380
                $queue['ttl'],
381
                $queue['prefetch_count']
382
            );
383
384
            $amqBackends[$pos] = [
385
                'type' => $queue['routing_key'],
386
                'backend' => new Reference($id),
387
            ];
388
389
            if (true === $queue['default']) {
390
                if (true === $defaultSet) {
391
                    throw new \RuntimeException('You can only set one rabbitmq default queue in your sonata notification configuration.');
392
                }
393
                $defaultSet = true;
394
                $defaultQueue = $queue['routing_key'];
395
            }
396
        }
397
398
        if (false === $defaultSet) {
399
            throw new \RuntimeException('You need to specify a valid default queue for the rabbitmq backend!');
400
        }
401
402
        $container->getDefinition('sonata.notification.backend.rabbitmq')
403
            ->replaceArgument(0, $connection)
404
            ->replaceArgument(1, $queues)
405
            ->replaceArgument(2, $defaultQueue)
0 ignored issues
show
Bug introduced by
The variable $defaultQueue does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
406
            ->replaceArgument(3, $amqBackends)
407
        ;
408
    }
409
410
    /**
411
     * @param string   $exchange
412
     * @param string   $name
413
     * @param string   $recover
414
     * @param string   $key
415
     * @param string   $deadLetterExchange
416
     * @param string   $deadLetterRoutingKey
417
     * @param int|null $ttl
418
     * @param int|null $prefetchCount
419
     *
420
     * @return string
421
     */
422
    protected function createAMQPBackend(ContainerBuilder $container, $exchange, $name, $recover, $key = '', $deadLetterExchange = null, $deadLetterRoutingKey = null, $ttl = null, $prefetchCount = null)
423
    {
424
        $id = 'sonata.notification.backend.rabbitmq.'.$this->amqpCounter++;
425
426
        $definition = new Definition(
427
            AMQPBackend::class,
428
            [
429
                $exchange,
430
                $name,
431
                $recover,
432
                $key,
433
                $deadLetterExchange,
434
                $deadLetterRoutingKey,
435
                $ttl,
436
                $prefetchCount,
437
            ]
438
        );
439
        $definition->setPublic(false);
440
        $container->setDefinition($id, $definition);
441
442
        return $id;
443
    }
444
445
    private function registerSonataDoctrineMapping(array $config): void
446
    {
447
        $collector = DoctrineCollector::getInstance();
448
449
        $collector->addIndex($config['class']['message'], 'idx_state', ['state']);
450
        $collector->addIndex($config['class']['message'], 'idx_created_at', ['created_at']);
451
    }
452
453
    /**
454
     * @param string $name
455
     *
456
     * @return string[]
457
     */
458
    private function getQueuesParameters($name, array $queues)
459
    {
460
        $params = array_unique(array_map(static function ($q) use ($name) {
461
            return $q[$name];
462
        }, $queues));
463
464
        $idx = array_search(null, $params, true);
465
        if (false !== $idx) {
466
            unset($params[$idx]);
467
        }
468
469
        return $params;
470
    }
471
472
    /**
473
     * @param string $key
474
     *
475
     * @return string
476
     */
477
    private function getAMQPDeadLetterExchangeByRoutingKey($key, array $queues)
478
    {
479
        foreach ($queues as $queue) {
480
            if ($queue['dead_letter_routing_key'] === $key) {
481
                return $queue['dead_letter_exchange'];
482
            }
483
        }
484
    }
485
}
486