Completed
Pull Request — master (#154)
by Fabien
04:41
created

HttplugExtension::configurePlugin()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 4
crap 6
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
            // To profile the requests, add a StackPlugin as first plugin in the chain.
293
            $stackPluginId = $this->configureStackPlugin($container, $clientName, $serviceId);
294
            array_unshift($plugins, $stackPluginId);
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
        if (null === $httpClient && null === $asyncHttpClient) {
411
            $container->removeDefinition('httplug.strategy');
412
413
            return;
414
        }
415
416
        $container
417
            ->getDefinition('httplug.strategy')
418
            ->addArgument($httpClient)
419
            ->addArgument($asyncHttpClient)
420
        ;
421
    }
422
423
    /**
424
     * Find a client with auto discovery and return a service Reference to it.
425
     *
426
     * @param ContainerBuilder   $container
427
     * @param string             $name
428
     * @param Reference|callable $factory
429
     * @param bool               $profiling
430
     *
431
     * @return string service id
432
     */
433
    private function registerAutoDiscoverableClient(ContainerBuilder $container, $name, $factory, $profiling)
434
    {
435
        $serviceId = 'httplug.auto_discovery.'.$name;
436
437
        $plugins = [];
438
        if ($profiling) {
439
            // To profile the requests, add a StackPlugin as first plugin in the chain.
440
            $plugins[] = $this->configureStackPlugin($container, $name, $serviceId);
441
442
            $this->decoratePluginWithProfilePlugin($container, 'httplug.plugin.stopwatch');
443
            $plugins[] = 'httplug.plugin.stopwatch';
444
        }
445
446
        $container
447
            ->register($serviceId, DummyClient::class)
448
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
449
            ->setArguments([
450
                array_map(
451
                    function ($id) {
452
                        return new Reference($id);
453
                    },
454
                    $plugins
455
                ),
456
                $factory,
457
                [],
458
            ])
459
        ;
460
461
        if ($profiling) {
462
            $collector = $container->getDefinition('httplug.collector.collector');
463
            $collector->replaceArgument(0, array_merge($collector->getArgument(0), [$name]));
464
        }
465
466
        return $serviceId;
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     */
472
    public function getConfiguration(array $config, ContainerBuilder $container)
473
    {
474
        return new Configuration($container->getParameter('kernel.debug'));
475
    }
476
477
    /**
478
     * Configure a plugin using the parent definition from plugins.xml.
479
     *
480
     * @param ContainerBuilder $container
481
     * @param string           $serviceId
482
     * @param string           $pluginName
483
     * @param array            $pluginConfig
484
     *
485
     * @return string configured service id
486
     */
487
    private function configurePlugin(ContainerBuilder $container, $serviceId, $pluginName, array $pluginConfig)
488
    {
489
        $pluginServiceId = $serviceId.'.plugin.'.$pluginName;
490
491
        $definition = class_exists(ChildDefinition::class)
492
            ? new ChildDefinition('httplug.plugin.'.$pluginName)
493
            : new DefinitionDecorator('httplug.plugin.'.$pluginName);
494
495
        $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
496
        $container->setDefinition($pluginServiceId, $definition);
497
498
        return $pluginServiceId;
499
    }
500
501
    /**
502
     * Decorate the plugin service with a ProfilePlugin service.
503
     *
504
     * @param ContainerBuilder $container
505
     * @param string           $pluginServiceId
506
     */
507
    private function decoratePluginWithProfilePlugin(ContainerBuilder $container, $pluginServiceId)
508
    {
509
        $container->register($pluginServiceId.'.debug', ProfilePlugin::class)
510
            ->setDecoratedService($pluginServiceId)
511
            ->setArguments([
512
                new Reference($pluginServiceId.'.debug.inner'),
513
                new Reference('httplug.collector.collector'),
514
                new Reference('httplug.collector.formatter'),
515
                $pluginServiceId,
516
            ])
517
            ->setPublic(false);
518
    }
519
520
    /**
521
     * Configure a StackPlugin for a client.
522
     *
523
     * @param ContainerBuilder $container
524
     * @param string           $clientName Client name to display in the profiler.
525
     * @param string           $serviceId  Client service id. Used as base for the StackPlugin service id.
526
     *
527
     * @return string configured StackPlugin service id
528
     */
529
    private function configureStackPlugin(ContainerBuilder $container, $clientName, $serviceId)
530
    {
531
        $pluginServiceId = $serviceId.'.plugin.stack';
532
533
        $definition = class_exists(ChildDefinition::class)
534
            ? new ChildDefinition('httplug.plugin.stack')
535
            : new DefinitionDecorator('httplug.plugin.stack');
536
537
        $definition->addArgument($clientName);
538
        $container->setDefinition($pluginServiceId, $definition);
539
540
        return $pluginServiceId;
541
    }
542
543
    /**
544
     * Configure the discovery factory when profiling is enabled to get client decorated with a ProfileClient.
545
     *
546
     * @param ContainerBuilder $container
547
     * @param string           $discovery
548
     * @param string           $name
549
     * @param array            $config
550
     *
551
     * @return callable|Reference
552
     */
553
    private function configureAutoDiscoveryFactory(ContainerBuilder $container, $discovery, $name, array $config)
554
    {
555
        $factory = [$discovery, 'find'];
556
        if ($this->isConfigEnabled($container, $config['profiling'])) {
557
            $factoryServiceId = 'httplug.auto_discovery.'.$name.'.factory';
558
            $container->register($factoryServiceId, ProfileClientFactory::class)
559
                ->setPublic(false)
560
                ->setArguments([
561
                    $factory,
562
                    new Reference('httplug.collector.collector'),
563
                    new Reference('httplug.collector.formatter'),
564
                ]);
565
            $factory = new Reference($factoryServiceId);
566
        }
567
568
        return $factory;
569
    }
570
}
571