Completed
Push — master ( 67a92e...b03a00 )
by Grégoire
01:54
created

SonataNotificationExtension.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\EasyExtendsBundle\Mapper\DoctrineCollector;
17
use Sonata\NotificationBundle\Backend\AMQPBackend;
18
use Sonata\NotificationBundle\Backend\MessageManagerBackend;
19
use Sonata\NotificationBundle\Model\MessageInterface;
20
use Symfony\Component\Config\FileLocator;
21
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
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
        /*
48
         * NEXT_MAJOR: Remove the check for ServiceClosureArgument as well as core_legacy.xml.
49
         */
50
        if (class_exists(ServiceClosureArgument::class)) {
51
            $loader->load('core.xml');
52
        } else {
53
            $loader->load('core_legacy.xml');
54
        }
55
56
        $loader->load('backend.xml');
57
        $loader->load('consumer.xml');
58
        $loader->load('command.xml');
59
60
        $bundles = $container->getParameter('kernel.bundles');
61
62
        if ('sonata.notification.backend.doctrine' === $config['backend']) {
63
            $loader->load('doctrine_orm.xml');
64
            $loader->load('selector.xml');
65
            $loader->load('event.xml');
66
67
            if (isset($bundles['FOSRestBundle'], $bundles['NelmioApiDocBundle'])) {
68
                $loader->load('api_controllers.xml');
69
                $loader->load('api_form.xml');
70
            }
71
72
            // for now, only support for ORM
73
            if ($config['admin']['enabled'] && isset($bundles['SonataDoctrineORMAdminBundle'])) {
74
                $loader->load('admin.xml');
75
            }
76
        }
77
78
        if ($config['consumers']['register_default']) {
79
            $loader->load('default_consumers.xml');
80
        }
81
82
        if (isset($bundles['LiipMonitorBundle'])) {
83
            $loader->load('checkmonitor.xml');
84
        }
85
86
        $container->setAlias('sonata.notification.backend', $config['backend']);
87
        // NEXT_MAJOR: remove this getter when requiring sf3.4+
88
        $container->getAlias('sonata.notification.backend')->setPublic(true);
89
        $container->setParameter('sonata.notification.backend', $config['backend']);
90
91
        $this->registerDoctrineMapping($config);
92
        $this->registerParameters($container, $config);
93
        $this->configureBackends($container, $config);
94
        $this->configureClass($container, $config);
95
        $this->configureListeners($container, $config);
96
        $this->configureAdmin($container, $config);
97
    }
98
99
    /**
100
     * @param ContainerBuilder $container
101
     * @param array            $config
102
     */
103
    public function configureClass(ContainerBuilder $container, $config): void
104
    {
105
        // admin configuration
106
        $container->setParameter('sonata.notification.admin.message.entity', $config['class']['message']);
107
108
        // manager configuration
109
        $container->setParameter('sonata.notification.manager.message.entity', $config['class']['message']);
110
    }
111
112
    /**
113
     * @param ContainerBuilder $container
114
     * @param array            $config
115
     */
116
    public function configureAdmin(ContainerBuilder $container, $config): void
117
    {
118
        $container->setParameter('sonata.notification.admin.message.class', $config['admin']['message']['class']);
119
        $container->setParameter('sonata.notification.admin.message.controller', $config['admin']['message']['controller']);
120
        $container->setParameter('sonata.notification.admin.message.translation_domain', $config['admin']['message']['translation']);
121
    }
122
123
    /**
124
     * @param ContainerBuilder $container
125
     * @param array            $config
126
     */
127
    public function registerParameters(ContainerBuilder $container, $config): void
128
    {
129
        $container->setParameter('sonata.notification.message.class', $config['class']['message']);
130
        $container->setParameter('sonata.notification.admin.message.entity', $config['class']['message']);
131
    }
132
133
    /**
134
     * @param ContainerBuilder $container
135
     * @param array            $config
136
     */
137
    public function configureBackends(ContainerBuilder $container, $config): void
138
    {
139
        if (isset($config['backends']['rabbitmq']) && 'sonata.notification.backend.rabbitmq' === $config['backend']) {
140
            $this->configureRabbitmq($container, $config);
141
        } else {
142
            $container->removeDefinition('sonata.notification.backend.rabbitmq');
143
        }
144
145
        if (isset($config['backends']['doctrine']) && 'sonata.notification.backend.doctrine' === $config['backend']) {
146
            $checkLevel = [
147
                MessageInterface::STATE_DONE => $config['backends']['doctrine']['states']['done'],
148
                MessageInterface::STATE_ERROR => $config['backends']['doctrine']['states']['error'],
149
                MessageInterface::STATE_IN_PROGRESS => $config['backends']['doctrine']['states']['in_progress'],
150
                MessageInterface::STATE_OPEN => $config['backends']['doctrine']['states']['open'],
151
            ];
152
153
            $pause = $config['backends']['doctrine']['pause'];
154
            $maxAge = $config['backends']['doctrine']['max_age'];
155
            $batchSize = $config['backends']['doctrine']['batch_size'];
156
            $container->setAlias('sonata.notification.manager.message', $config['backends']['doctrine']['message_manager']);
157
158
            $this->configureDoctrineBackends($container, $config, $checkLevel, $pause, $maxAge, $batchSize);
0 ignored issues
show
$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...
159
160
            // NEXT_MAJOR: remove this getter when requiring sf3.4+
161
            $container->getAlias('sonata.notification.manager.message')->setPublic(true);
162
        } else {
163
            $container->removeDefinition('sonata.notification.backend.doctrine');
164
        }
165
    }
166
167
    /**
168
     * @param array $config
169
     */
170
    public function registerDoctrineMapping(array $config): void
171
    {
172
        $collector = DoctrineCollector::getInstance();
173
174
        $collector->addIndex($config['class']['message'], 'idx_state', [
175
            'state',
176
        ]);
177
178
        $collector->addIndex($config['class']['message'], 'idx_created_at', [
179
            'created_at',
180
        ]);
181
    }
182
183
    /**
184
     * @param array $config
185
     */
186
    protected function checkConfiguration(array $config): void
187
    {
188
        if (isset($config['backends']) && \count($config['backends']) > 1) {
189
            throw new \RuntimeException('more than one backend configured, you can have only one backend configuration');
190
        }
191
192
        if (!isset($config['backends']['rabbitmq']) && 'sonata.notification.backend.rabbitmq' === $config['backend']) {
193
            throw new \RuntimeException('Please configure the sonata_notification.backends.rabbitmq section');
194
        }
195
196
        if (!isset($config['backends']['doctrine']) && 'sonata.notification.backend.doctrine' === $config['backend']) {
197
            throw new \RuntimeException('Please configure the sonata_notification.backends.doctrine section');
198
        }
199
    }
200
201
    /**
202
     * @param ContainerBuilder $container
203
     * @param array            $config
204
     */
205
    protected function configureListeners(ContainerBuilder $container, array $config): void
206
    {
207
        $ids = $config['iteration_listeners'];
208
209
        if ('sonata.notification.backend.doctrine' === $config['backend']) {
210
            // this one clean the unit of work after every iteration
211
            $ids[] = 'sonata.notification.event.doctrine_optimize';
212
213
            if ($config['backends']['doctrine']['batch_size'] > 1) {
214
                // if the backend is doctrine and the batch size > 1, then
215
                // the unit of work must be cleaned wisely to avoid any issue
216
                // while persisting entities
217
                $ids = [
218
                    'sonata.notification.event.doctrine_backend_optimize',
219
                ];
220
            }
221
        }
222
223
        $container->setParameter('sonata.notification.event.iteration_listeners', $ids);
224
    }
225
226
    /**
227
     * @param ContainerBuilder $container
228
     * @param array            $config
229
     * @param bool             $checkLevel
230
     * @param int              $pause
231
     * @param int              $maxAge
232
     * @param int              $batchSize
233
     *
234
     * @throws \RuntimeException
235
     */
236
    protected function configureDoctrineBackends(ContainerBuilder $container, array $config, $checkLevel, $pause, $maxAge, $batchSize): void
237
    {
238
        $queues = $config['queues'];
239
        $qBackends = [];
240
241
        $definition = $container->getDefinition('sonata.notification.backend.doctrine');
242
243
        // no queue defined, set a default one
244
        if (0 == \count($queues)) {
245
            $queues = [[
246
                'queue' => 'default',
247
                'default' => true,
248
                'types' => [],
249
            ]];
250
        }
251
252
        $defaultSet = false;
253
        $declaredQueues = [];
254
255
        foreach ($queues as $pos => &$queue) {
256
            if (\in_array($queue['queue'], $declaredQueues)) {
257
                throw new \RuntimeException('The doctrine backend does not support 2 identicals queue name, please rename one queue');
258
            }
259
260
            $declaredQueues[] = $queue['queue'];
261
262
            // make the configuration compatible with old code and rabbitmq
263
            if (isset($queue['routing_key']) && \strlen($queue['routing_key']) > 0) {
264
                $queue['types'] = [$queue['routing_key']];
265
            }
266
267
            if (empty($queue['types']) && false === $queue['default']) {
268
                throw new \RuntimeException('You cannot declared a doctrine queue with no type defined with default = false');
269
            }
270
271
            if (!empty($queue['types']) && true === $queue['default']) {
272
                throw new \RuntimeException('You cannot declared a doctrine queue with types defined with default = true');
273
            }
274
275
            $id = $this->createDoctrineQueueBackend($container, $definition->getArgument(0), $checkLevel, $pause, $maxAge, $batchSize, $queue['queue'], $queue['types']);
276
            $qBackends[$pos] = [
277
                'types' => $queue['types'],
278
                'backend' => new Reference($id),
279
            ];
280
281
            if (true === $queue['default']) {
282
                if (true === $defaultSet) {
283
                    throw new \RuntimeException('You can only set one doctrine default queue in your sonata notification configuration.');
284
                }
285
286
                $defaultSet = true;
287
                $defaultQueue = $queue['queue'];
288
            }
289
        }
290
291
        if (false === $defaultSet) {
292
            throw new \RuntimeException('You need to specify a valid default queue for the doctrine backend!');
293
        }
294
295
        $definition
296
            ->replaceArgument(1, $queues)
297
            ->replaceArgument(2, $defaultQueue)
0 ignored issues
show
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...
298
            ->replaceArgument(3, $qBackends)
299
        ;
300
    }
301
302
    /**
303
     * @param ContainerBuilder $container
304
     * @param string           $manager
305
     * @param bool             $checkLevel
306
     * @param int              $pause
307
     * @param int              $maxAge
308
     * @param int              $batchSize
309
     * @param string           $key
310
     * @param array            $types
311
     *
312
     * @return string
313
     */
314
    protected function createDoctrineQueueBackend(ContainerBuilder $container, $manager, $checkLevel, $pause, $maxAge, $batchSize, $key, array $types = [])
315
    {
316
        if ('' == $key) {
317
            $id = 'sonata.notification.backend.doctrine.default_'.$this->amqpCounter++;
318
        } else {
319
            $id = 'sonata.notification.backend.doctrine.'.$key;
320
        }
321
322
        $definition = new Definition(MessageManagerBackend::class, [$manager, $checkLevel, $pause, $maxAge, $batchSize, $types]);
323
        $definition->setPublic(false);
324
325
        $container->setDefinition($id, $definition);
326
327
        return $id;
328
    }
329
330
    /**
331
     * @param ContainerBuilder $container
332
     * @param array            $config
333
     */
334
    protected function configureRabbitmq(ContainerBuilder $container, array $config): void
335
    {
336
        $queues = $config['queues'];
337
        $connection = $config['backends']['rabbitmq']['connection'];
338
        $baseExchange = $config['backends']['rabbitmq']['exchange'];
339
        $amqBackends = [];
340
341
        if (0 == \count($queues)) {
342
            $queues = [[
343
                'queue' => 'default',
344
                'default' => true,
345
                'routing_key' => '',
346
                'recover' => false,
347
                'dead_letter_exchange' => null,
348
                'dead_letter_routing_key' => null,
349
                'ttl' => null,
350
                'prefetch_count' => null,
351
            ]];
352
        }
353
354
        $deadLetterRoutingKeys = $this->getQueuesParameters('dead_letter_routing_key', $queues);
355
        $routingKeys = $this->getQueuesParameters('routing_key', $queues);
356
357
        foreach ($deadLetterRoutingKeys as $key) {
358
            if (!\in_array($key, $routingKeys)) {
359
                throw new \RuntimeException(sprintf(
360
                    'You must configure the queue having the routing_key "%s" same as dead_letter_routing_key', $key
361
                ));
362
            }
363
        }
364
365
        $declaredQueues = [];
366
367
        $defaultSet = false;
368
        foreach ($queues as $pos => $queue) {
369
            if (\in_array($queue['queue'], $declaredQueues)) {
370
                throw new \RuntimeException('The RabbitMQ backend does not support 2 identicals queue name, please rename one queue');
371
            }
372
373
            $declaredQueues[] = $queue['queue'];
374
375
            if ($queue['dead_letter_routing_key']) {
376
                if (null === $queue['dead_letter_exchange']) {
377
                    throw new \RuntimeException(
378
                        'dead_letter_exchange must be configured when dead_letter_routing_key is set'
379
                    );
380
                }
381
            }
382
383
            if (\in_array($queue['routing_key'], $deadLetterRoutingKeys)) {
384
                $exchange = $this->getAMQPDeadLetterExchangeByRoutingKey($queue['routing_key'], $queues);
385
            } else {
386
                $exchange = $baseExchange;
387
            }
388
389
            $id = $this->createAMQPBackend(
390
                $container,
391
                $exchange,
392
                $queue['queue'],
393
                $queue['recover'],
394
                $queue['routing_key'],
395
                $queue['dead_letter_exchange'],
396
                $queue['dead_letter_routing_key'],
397
                $queue['ttl'],
398
                $queue['prefetch_count']
399
            );
400
401
            $amqBackends[$pos] = [
402
                'type' => $queue['routing_key'],
403
                'backend' => new Reference($id),
404
            ];
405
406
            if (true === $queue['default']) {
407
                if (true === $defaultSet) {
408
                    throw new \RuntimeException('You can only set one rabbitmq default queue in your sonata notification configuration.');
409
                }
410
                $defaultSet = true;
411
                $defaultQueue = $queue['routing_key'];
412
            }
413
        }
414
415
        if (false === $defaultSet) {
416
            throw new \RuntimeException('You need to specify a valid default queue for the rabbitmq backend!');
417
        }
418
419
        $container->getDefinition('sonata.notification.backend.rabbitmq')
420
            ->replaceArgument(0, $connection)
421
            ->replaceArgument(1, $queues)
422
            ->replaceArgument(2, $defaultQueue)
0 ignored issues
show
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...
423
            ->replaceArgument(3, $amqBackends)
424
        ;
425
    }
426
427
    /**
428
     * @param ContainerBuilder $container
429
     * @param string           $exchange
430
     * @param string           $name
431
     * @param string           $recover
432
     * @param string           $key
433
     * @param string           $deadLetterExchange
434
     * @param string           $deadLetterRoutingKey
435
     * @param int|null         $ttl
436
     * @param int|null         $prefetchCount
437
     *
438
     * @return string
439
     */
440
    protected function createAMQPBackend(ContainerBuilder $container, $exchange, $name, $recover, $key = '', $deadLetterExchange = null, $deadLetterRoutingKey = null, $ttl = null, $prefetchCount = null)
441
    {
442
        $id = 'sonata.notification.backend.rabbitmq.'.$this->amqpCounter++;
443
444
        $definition = new Definition(
445
            AMQPBackend::class,
446
            [
447
                $exchange,
448
                $name,
449
                $recover,
450
                $key,
451
                $deadLetterExchange,
452
                $deadLetterRoutingKey,
453
                $ttl,
454
                $prefetchCount,
455
            ]
456
        );
457
        $definition->setPublic(false);
458
        $container->setDefinition($id, $definition);
459
460
        return $id;
461
    }
462
463
    /**
464
     * @param string $name
465
     * @param array  $queues
466
     *
467
     * @return string[]
468
     */
469
    private function getQueuesParameters($name, array $queues)
470
    {
471
        $params = array_unique(array_map(function ($q) use ($name) {
472
            return $q[$name];
473
        }, $queues));
474
475
        $idx = array_search(null, $params);
476
        if (false !== $idx) {
477
            unset($params[$idx]);
478
        }
479
480
        return $params;
481
    }
482
483
    /**
484
     * @param string $key
485
     * @param array  $queues
486
     *
487
     * @return string
488
     */
489
    private function getAMQPDeadLetterExchangeByRoutingKey($key, array $queues)
490
    {
491
        foreach ($queues as $queue) {
492
            if ($queue['dead_letter_routing_key'] === $key) {
493
                return $queue['dead_letter_exchange'];
494
            }
495
        }
496
    }
497
}
498