Completed
Pull Request — master (#154)
by Fabien
05:40
created

decoratePluginWithProfilePlugin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 9
nc 1
nop 2
crap 2
1
<?php
2
3
namespace Http\HttplugBundle\DependencyInjection;
4
5
use Http\Client\Common\BatchClient;
6
use Http\Client\Common\FlexibleHttpClient;
7
use Http\Client\Common\HttpMethodsClient;
8
use Http\Client\Common\Plugin\AuthenticationPlugin;
9
use Http\Discovery\HttpAsyncClientDiscovery;
10
use Http\Discovery\HttpClientDiscovery;
11
use Http\HttplugBundle\ClientFactory\DummyClient;
12
use Http\HttplugBundle\ClientFactory\PluginClientFactory;
13
use Http\HttplugBundle\Collector\ProfileClientFactory;
14
use Http\HttplugBundle\Collector\ProfilePlugin;
15
use Http\Message\Authentication\BasicAuth;
16
use Http\Message\Authentication\Bearer;
17
use Http\Message\Authentication\Wsse;
18
use Psr\Http\Message\UriInterface;
19
use Symfony\Component\Config\FileLocator;
20
use Symfony\Component\DependencyInjection\ChildDefinition;
21
use Symfony\Component\DependencyInjection\ContainerBuilder;
22
use Symfony\Component\DependencyInjection\ContainerInterface;
23
use Symfony\Component\DependencyInjection\Definition;
24
use Symfony\Component\DependencyInjection\DefinitionDecorator;
25
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
26
use Symfony\Component\DependencyInjection\Reference;
27
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
28
29
/**
30
 * @author David Buchmann <[email protected]>
31
 * @author Tobias Nyholm <[email protected]>
32
 */
33
class HttplugExtension extends Extension
34
{
35
    /**
36
     * {@inheritdoc}
37
     */
38 10
    public function load(array $configs, ContainerBuilder $container)
39
    {
40 10
        $configuration = $this->getConfiguration($configs, $container);
41 10
        $config = $this->processConfiguration($configuration, $configs);
1 ignored issue
show
Bug introduced by
It seems like $configuration defined by $this->getConfiguration($configs, $container) on line 40 can be null; however, Symfony\Component\Depend...:processConfiguration() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
42
43 10
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
44
45 10
        $loader->load('services.xml');
46 10
        $loader->load('plugins.xml');
47
48
        // Register default services
49 10
        foreach ($config['classes'] as $service => $class) {
50 10
            if (!empty($class)) {
51 1
                $container->register(sprintf('httplug.%s.default', $service), $class);
52 1
            }
53 10
        }
54
55
        // Set main aliases
56 10
        foreach ($config['main_alias'] as $type => $id) {
57 10
            $container->setAlias(sprintf('httplug.%s', $type), $id);
58 10
        }
59
60
        // Configure toolbar
61 10
        if ($this->isConfigEnabled($container, $config['profiling'])) {
62 7
            $loader->load('data-collector.xml');
63
64 7
            if (!empty($config['profiling']['formatter'])) {
65
                // Add custom formatter
66
                $container
67 1
                    ->getDefinition('httplug.collector.formatter')
68 1
                    ->replaceArgument(0, new Reference($config['profiling']['formatter']))
69
                ;
70 1
            }
71
72
            $container
73 7
                ->getDefinition('httplug.formatter.full_http_message')
74 7
                ->addArgument($config['profiling']['captured_body_length'])
75
            ;
76 7
        }
77
78 10
        $this->configureClients($container, $config, $this->isConfigEnabled($container, $config['profiling']));
79 10
        $this->configureSharedPlugins($container, $config['plugins']); // must be after clients, as clients.X.plugins might use plugins as templates that will be removed
80 10
        $this->configureAutoDiscoveryClients($container, $config);
81 10
    }
82
83
    /**
84
     * Configure client services.
85
     *
86
     * @param ContainerBuilder $container
87
     * @param array            $config
88
     * @param bool             $profiling
89
     */
90 10
    private function configureClients(ContainerBuilder $container, array $config, $profiling)
91
    {
92 10
        $first = null;
93 10
        $clients = [];
94
95 10
        foreach ($config['clients'] as $name => $arguments) {
96 6
            if ($first === null) {
97
                // Save the name of the first configured client.
98 6
                $first = $name;
99 6
            }
100
101 6
            $this->configureClient($container, $name, $arguments, $this->isConfigEnabled($container, $config['profiling']));
102 6
            $clients[] = $name;
103 10
        }
104
105
        // If we have clients configured
106 10
        if ($first !== null) {
107
            // If we do not have a client named 'default'
108 6
            if (!isset($config['clients']['default'])) {
109
                // Alias the first client to httplug.client.default
110 6
                $container->setAlias('httplug.client.default', 'httplug.client.'.$first);
111 6
            }
112 6
        }
113
114 10
        if ($profiling) {
115 7
            $container->getDefinition('httplug.collector.collector')
116 7
                ->setArguments([$clients])
117
            ;
118 7
        }
119 10
    }
120
121
    /**
122
     * @param ContainerBuilder $container
123
     * @param array            $config
124
     */
125 10
    private function configureSharedPlugins(ContainerBuilder $container, array $config)
126
    {
127 10
        if (!empty($config['authentication'])) {
128
            $this->configureAuthentication($container, $config['authentication']);
129
        }
130 10
        unset($config['authentication']);
131
132 10
        foreach ($config as $name => $pluginConfig) {
133 10
            $pluginId = 'httplug.plugin.'.$name;
134
135 10
            if ($this->isConfigEnabled($container, $pluginConfig)) {
136 10
                $def = $container->getDefinition($pluginId);
137 10
                $this->configurePluginByName($name, $def, $pluginConfig, $container, $pluginId);
138 10
            } else {
139 10
                $container->removeDefinition($pluginId);
140
            }
141 10
        }
142 10
    }
143
144
    /**
145
     * @param string           $name
146
     * @param Definition       $definition
147
     * @param array            $config
148
     * @param ContainerBuilder $container  In case we need to add additional services for this plugin
149
     * @param string           $serviceId  Service id of the plugin, in case we need to add additional services for this plugin.
150
     */
151 10
    private function configurePluginByName($name, Definition $definition, array $config, ContainerInterface $container, $serviceId)
152
    {
153
        switch ($name) {
154 10
            case 'cache':
155
                $definition
156
                    ->replaceArgument(0, new Reference($config['cache_pool']))
157
                    ->replaceArgument(1, new Reference($config['stream_factory']))
158
                    ->replaceArgument(2, $config['config']);
159
                break;
160 10
            case 'cookie':
161
                $definition->replaceArgument(0, new Reference($config['cookie_jar']));
162
                break;
163 10
            case 'decoder':
164 10
                $definition->addArgument([
165 10
                    'use_content_encoding' => $config['use_content_encoding'],
166 10
                ]);
167 10
                break;
168 10
            case 'history':
169
                $definition->replaceArgument(0, new Reference($config['journal']));
170
                break;
171 10
            case 'logger':
172 10
                $definition->replaceArgument(0, new Reference($config['logger']));
173 10
                if (!empty($config['formatter'])) {
174
                    $definition->replaceArgument(1, new Reference($config['formatter']));
175
                }
176 10
                break;
177 10
            case 'redirect':
178 10
                $definition->addArgument([
179 10
                    'preserve_header' => $config['preserve_header'],
180 10
                    'use_default_for_multiple' => $config['use_default_for_multiple'],
181 10
                ]);
182 10
                break;
183 10
            case 'retry':
184 10
                $definition->addArgument([
185 10
                    'retries' => $config['retry'],
186 10
                ]);
187 10
                break;
188 10
            case 'stopwatch':
189 10
                $definition->replaceArgument(0, new Reference($config['stopwatch']));
190 10
                break;
191
192
            /* client specific plugins */
193
194 3
            case 'add_host':
195 3
                $uriService = $serviceId.'.host_uri';
196 3
                $this->createUri($container, $uriService, $config['host']);
0 ignored issues
show
Compatibility introduced by
$container of type object<Symfony\Component...ion\ContainerInterface> is not a sub-type of object<Symfony\Component...ction\ContainerBuilder>. It seems like you assume a concrete implementation of the interface Symfony\Component\Depend...tion\ContainerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
197 3
                $definition->replaceArgument(0, new Reference($uriService));
198 3
                $definition->replaceArgument(1, [
199 3
                    'replace' => $config['replace'],
200 3
                ]);
201 3
                break;
202 1
            case 'header_append':
203 1
            case 'header_defaults':
204 1
            case 'header_set':
205 1
            case 'header_remove':
206 1
                $definition->replaceArgument(0, $config['headers']);
207 1
                break;
208
209
            default:
210
                throw new \InvalidArgumentException(sprintf('Internal exception: Plugin %s is not handled', $name));
211
        }
212 10
    }
213
214
    /**
215
     * @param ContainerBuilder $container
216
     * @param array            $config
217
     *
218
     * @return array List of service ids for the authentication plugins.
219
     */
220 3
    private function configureAuthentication(ContainerBuilder $container, array $config, $servicePrefix = 'httplug.plugin.authentication')
221
    {
222 3
        $pluginServices = [];
223
224 3
        foreach ($config as $name => $values) {
225 3
            $authServiceKey = sprintf($servicePrefix.'.%s.auth', $name);
226 3
            switch ($values['type']) {
227 3
                case 'bearer':
228
                    $container->register($authServiceKey, Bearer::class)
229
                        ->addArgument($values['token']);
230
                    break;
231
                case 'basic':
232
                    $container->register($authServiceKey, BasicAuth::class)
233
                        ->addArgument($values['username'])
234
                        ->addArgument($values['password']);
235
                    break;
236
                case 'wsse':
237
                    $container->register($authServiceKey, Wsse::class)
238
                        ->addArgument($values['username'])
239
                        ->addArgument($values['password']);
240
                    break;
241
                case 'service':
242
                    $authServiceKey = $values['service'];
243
                    break;
244
                default:
245
                    throw new \LogicException(sprintf('Unknown authentication type: "%s"', $values['type']));
246
            }
247
248
            $pluginServiceKey = $servicePrefix.'.'.$name;
249
            $container->register($pluginServiceKey, AuthenticationPlugin::class)
250
                ->addArgument(new Reference($authServiceKey))
251
            ;
252
            $pluginServices[] = $pluginServiceKey;
253
        }
254
255
        return $pluginServices;
256
    }
257
258
    /**
259
     * @param ContainerBuilder $container
260
     * @param string           $clientName
261
     * @param array            $arguments
262
     * @param bool             $profiling
263
     */
264
    private function configureClient(ContainerBuilder $container, $clientName, array $arguments, $profiling)
265
    {
266
        $serviceId = 'httplug.client.'.$clientName;
267
268
        $plugins = [];
269
        foreach ($arguments['plugins'] as $plugin) {
270
            list($pluginName, $pluginConfig) = each($plugin);
271
            if ('reference' === $pluginName) {
272
                $plugins[] = $pluginConfig['id'];
273
            } elseif ('authentication' === $pluginName) {
274
                $plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication'));
275
            } else {
276
                $plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig);
277
            }
278
        }
279
280
        $pluginClientOptions = [];
281
        if ($profiling) {
282
            // Add the stopwatch plugin
283
            if (!in_array('httplug.plugin.stopwatch', $arguments['plugins'])) {
284
                array_unshift($plugins, 'httplug.plugin.stopwatch');
285
            }
286
287
            //Decorate each plugin with a ProfilePlugin instance.
288
            foreach ($plugins as $pluginServiceId) {
289
                $this->decoratePluginWithProfilePlugin($container, $pluginServiceId);
290
            }
291
292
            // Add the newstack plugin
293
            $newStackPluginId = $this->configureNewStackPlugin($container, $clientName, $serviceId);
294
            array_unshift($plugins, $newStackPluginId);
295
        }
296
297
        $container
298
            ->register($serviceId, DummyClient::class)
299
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
300
            ->addArgument(
301
                array_map(
302
                    function ($id) {
303
                        return new Reference($id);
304
                    },
305
                    $plugins
306
                )
307
            )
308
            ->addArgument(new Reference($arguments['factory']))
309
            ->addArgument($arguments['config'])
310
            ->addArgument($pluginClientOptions)
311
        ;
312
313
        /*
314
         * Decorate the client with clients from client-common
315
         */
316 View Code Duplication
        if ($arguments['flexible_client']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
            $container
318
                ->register($serviceId.'.flexible', FlexibleHttpClient::class)
319
                ->addArgument(new Reference($serviceId.'.flexible.inner'))
320
                ->setPublic(false)
321
                ->setDecoratedService($serviceId)
322
            ;
323
        }
324
325 View Code Duplication
        if ($arguments['http_methods_client']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
326
            $container
327
                ->register($serviceId.'.http_methods', HttpMethodsClient::class)
328
                ->setArguments([new Reference($serviceId.'.http_methods.inner'), new Reference('httplug.message_factory')])
329
                ->setPublic(false)
330
                ->setDecoratedService($serviceId)
331
            ;
332
        }
333
334 View Code Duplication
        if ($arguments['batch_client']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
335
            $container
336
                ->register($serviceId.'.batch_client', BatchClient::class)
337
                ->setArguments([new Reference($serviceId.'.batch_client.inner')])
338
                ->setPublic(false)
339
                ->setDecoratedService($serviceId)
340
            ;
341
        }
342
    }
343
344
    /**
345
     * Create a URI object with the default URI factory.
346
     *
347
     * @param ContainerBuilder $container
348
     * @param string           $serviceId Name of the private service to create
349
     * @param string           $uri       String representation of the URI
350
     */
351
    private function createUri(ContainerBuilder $container, $serviceId, $uri)
352
    {
353
        $container
354
            ->register($serviceId, UriInterface::class)
355
            ->setPublic(false)
356
            ->setFactory([new Reference('httplug.uri_factory'), 'createUri'])
357
            ->addArgument($uri)
358
        ;
359
    }
360
361
    /**
362
     * Make the user can select what client is used for auto discovery. If none is provided, a service will be created
363
     * by finding a client using auto discovery.
364
     *
365
     * @param ContainerBuilder $container
366
     * @param array            $config
367
     */
368
    private function configureAutoDiscoveryClients(ContainerBuilder $container, array $config)
369
    {
370
        $httpClient = $config['discovery']['client'];
371
372 View Code Duplication
        if (!empty($httpClient)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
373
            if ($httpClient === 'auto') {
374
                $httpClient = $this->registerAutoDiscoverableClient(
375
                    $container,
376
                    'auto_discovered_client',
377
                    $this->configureAutoDiscoveryFactory(
378
                        $container,
379
                        HttpClientDiscovery::class,
380
                        'auto_discovered_client',
381
                        $config
382
                    ),
383
                    $this->isConfigEnabled($container, $config['profiling'])
384
                );
385
            }
386
387
            $httpClient = new Reference($httpClient);
388
        }
389
390
        $asyncHttpClient = $config['discovery']['async_client'];
391
392 View Code Duplication
        if (!empty($asyncHttpClient)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
393
            if ($asyncHttpClient === 'auto') {
394
                $asyncHttpClient = $this->registerAutoDiscoverableClient(
395
                    $container,
396
                    'auto_discovered_async',
397
                    $this->configureAutoDiscoveryFactory(
398
                        $container,
399
                        HttpAsyncClientDiscovery::class,
400
                        'auto_discovered_async',
401
                        $config
402
                    ),
403
                    $this->isConfigEnabled($container, $config['profiling'])
404
                );
405
            }
406
407
            $asyncHttpClient = new Reference($asyncHttpClient);
408
        }
409
410
        $container
411
            ->getDefinition('httplug.strategy')
412
            ->addArgument($httpClient)
413
            ->addArgument($asyncHttpClient)
414
        ;
415
    }
416
417
    /**
418
     * Find a client with auto discovery and return a service Reference to it.
419
     *
420
     * @param ContainerBuilder   $container
421
     * @param string             $name
422
     * @param Reference|callable $factory
423
     * @param bool               $profiling
424
     *
425
     * @return string service id
426
     */
427
    private function registerAutoDiscoverableClient(ContainerBuilder $container, $name, $factory, $profiling)
428
    {
429
        $serviceId = 'httplug.auto_discovery.'.$name;
430
431
        $plugins = [];
432
        if ($profiling) {
433
            // Add the newstack plugin
434
            $plugins[] = $this->configureNewStackPlugin($container, $name, $serviceId);
435
436
            $this->decoratePluginWithProfilePlugin($container, 'httplug.plugin.stopwatch');
437
            $plugins[] = 'httplug.plugin.stopwatch';
438
        }
439
440
        $container
441
            ->register($serviceId, DummyClient::class)
442
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
443
            ->setArguments([
444
                array_map(
445
                    function ($id) {
446
                        return new Reference($id);
447
                    },
448
                    $plugins
449
                ),
450
                $factory,
451
                [],
452
            ])
453
        ;
454
455
        if ($profiling) {
456
            $collector = $container->getDefinition('httplug.collector.collector');
457
            $collector->replaceArgument(0, array_merge($collector->getArgument(0), [$name]));
458
        }
459
460
        return $serviceId;
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466
    public function getConfiguration(array $config, ContainerBuilder $container)
467
    {
468
        return new Configuration($container->getParameter('kernel.debug'));
469
    }
470
471
    /**
472
     * Configure a plugin using the parent definition from plugins.xml.
473
     *
474
     * @param ContainerBuilder $container
475
     * @param string           $serviceId
476
     * @param string           $pluginName
477
     * @param array            $pluginConfig
478
     *
479
     * @return string configured service id
480
     */
481
    private function configurePlugin(ContainerBuilder $container, $serviceId, $pluginName, array $pluginConfig)
482
    {
483
        $pluginServiceId = $serviceId.'.plugin.'.$pluginName;
484
485
        $definition = class_exists(ChildDefinition::class)
486
            ? new ChildDefinition('httplug.plugin.'.$pluginName)
487
            : new DefinitionDecorator('httplug.plugin.'.$pluginName);
488
489
        $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
490
        $container->setDefinition($pluginServiceId, $definition);
491
492
        return $pluginServiceId;
493
    }
494
495
    /**
496
     * Decorate the plugin service with a ProfilePlugin service.
497
     *
498
     * @param ContainerBuilder $container
499
     * @param string           $pluginServiceId
500
     */
501
    private function decoratePluginWithProfilePlugin(ContainerBuilder $container, $pluginServiceId)
502
    {
503
        $container->register($pluginServiceId.'.debug', ProfilePlugin::class)
504
            ->setDecoratedService($pluginServiceId)
505
            ->setArguments([
506
                new Reference($pluginServiceId.'.debug.inner'),
507
                new Reference('httplug.collector.collector'),
508
                new Reference('httplug.collector.formatter'),
509
                $pluginServiceId,
510
            ])
511
            ->setPublic(false);
512
    }
513
514
    /**
515
     * Configure a new StackPlugin for a client.
516
     *
517
     * @param ContainerBuilder $container
518
     * @param string           $clientName
519
     * @param string           $serviceId
520
     *
521
     * @return string configured new stack plugin service id
522
     */
523
    private function configureNewStackPlugin(ContainerBuilder $container, $clientName, $serviceId)
524
    {
525
        $pluginServiceId = $serviceId.'.plugin.newstack';
526
527
        $definition = class_exists(ChildDefinition::class)
528
            ? new ChildDefinition('httplug.plugin.stack')
529
            : new DefinitionDecorator('httplug.plugin.stack');
530
531
        $definition->addArgument($clientName);
532
        $container->setDefinition($pluginServiceId, $definition);
533
534
        return $pluginServiceId;
535
    }
536
537
    /**
538
     * Configure the discovery factory when profiling is enabled to get client decorated with a ProfileClient.
539
     *
540
     * @param ContainerBuilder $container
541
     * @param string           $discovery
542
     * @param string           $name
543
     * @param array            $config
544
     *
545
     * @return callable|Reference
546
     */
547
    private function configureAutoDiscoveryFactory(ContainerBuilder $container, $discovery, $name, array $config)
548
    {
549
        $factory = [$discovery, 'find'];
550
        if ($this->isConfigEnabled($container, $config['profiling'])) {
551
            $factoryServiceId = 'httplug.auto_discovery.'.$name.'.factory';
552
            $container->register($factoryServiceId, ProfileClientFactory::class)
553
                ->setPublic(false)
554
                ->setArguments([
555
                    $factory,
556
                    new Reference('httplug.collector.collector'),
557
                    new Reference('httplug.collector.formatter'),
558
                ]);
559
            $factory = new Reference($factoryServiceId);
560
        }
561
562
        return $factory;
563
    }
564
}
565