Completed
Pull Request — master (#163)
by Tobias
08:03
created

HttplugExtension::configureSharedPlugins()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0466

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 12
cts 14
cp 0.8571
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
nc 6
nop 2
crap 4.0466
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
        $profilingEnabled = $this->isConfigEnabled($container, $config['profiling']);
62 10
        if ($profilingEnabled) {
63 7
            $loader->load('data-collector.xml');
64
65 7
            if (!empty($config['profiling']['formatter'])) {
66
                // Add custom formatter
67
                $container
68 1
                    ->getDefinition('httplug.collector.formatter')
69 1
                    ->replaceArgument(0, new Reference($config['profiling']['formatter']))
70
                ;
71 1
            }
72
73
            $container
74 7
                ->getDefinition('httplug.formatter.full_http_message')
75 7
                ->addArgument($config['profiling']['captured_body_length'])
76
            ;
77 7
        }
78
79 10
        $this->configureClients($container, $config, $profilingEnabled);
80 10
        $this->configurePlugins($container, $config['plugins']); // must be after clients, as clients.X.plugins might use plugins as templates that will be removed
81 10
        $this->configureAutoDiscoveryClients($container, $config);
82 10
    }
83
84
    /**
85
     * Configure client services.
86
     *
87
     * @param ContainerBuilder $container
88
     * @param array            $config
89
     * @param bool             $profiling
90
     */
91 10
    private function configureClients(ContainerBuilder $container, array $config, $profiling)
92
    {
93 10
        $first = null;
94 10
        $clients = [];
95
96 10
        foreach ($config['clients'] as $name => $arguments) {
97 6
            if ($first === null) {
98
                // Save the name of the first configured client.
99 6
                $first = $name;
100 6
            }
101
102 6
            $this->configureClient($container, $name, $arguments, $this->isConfigEnabled($container, $config['profiling']));
103 6
            $clients[] = $name;
104 10
        }
105
106
        // If we have clients configured
107 10
        if ($first !== null) {
108
            // If we do not have a client named 'default'
109 6
            if (!isset($config['clients']['default'])) {
110
                // Alias the first client to httplug.client.default
111 6
                $container->setAlias('httplug.client.default', 'httplug.client.'.$first);
112 6
            }
113 6
        }
114
115 10
        if ($profiling) {
116 7
            $container->getDefinition('httplug.collector.collector')
117 7
                ->setArguments([$clients])
118
            ;
119 7
        }
120 10
    }
121
122
    /**
123
     * Configure all Httplug plugins or remove their service definition.
124
     *
125
     * @param ContainerBuilder $container
126
     * @param array            $config
127
     */
128 10
    private function configurePlugins(ContainerBuilder $container, array $config)
129
    {
130 10
        if (!empty($config['authentication'])) {
131
            $this->configureAuthentication($container, $config['authentication']);
132
        }
133 10
        unset($config['authentication']);
134
135 10
        foreach ($config as $name => $pluginConfig) {
136 10
            $pluginId = 'httplug.plugin.'.$name;
137
138 10
            if ($this->isConfigEnabled($container, $pluginConfig)) {
139 10
                $def = $container->getDefinition($pluginId);
140 10
                $this->configurePluginByName($name, $def, $pluginConfig, $container, $pluginId);
141 10
            } else {
142 10
                $container->removeDefinition($pluginId);
143
            }
144 10
        }
145 10
    }
146
147
    /**
148
     * @param string           $name
149
     * @param Definition       $definition
150
     * @param array            $config
151
     * @param ContainerBuilder $container  In case we need to add additional services for this plugin
152
     * @param string           $serviceId  Service id of the plugin, in case we need to add additional services for this plugin.
153
     */
154 10
    private function configurePluginByName($name, Definition $definition, array $config, ContainerInterface $container, $serviceId)
155
    {
156
        switch ($name) {
157 10
            case 'cache':
158
                $definition
159
                    ->replaceArgument(0, new Reference($config['cache_pool']))
160
                    ->replaceArgument(1, new Reference($config['stream_factory']))
161
                    ->replaceArgument(2, $config['config']);
162
                break;
163 10
            case 'cookie':
164
                $definition->replaceArgument(0, new Reference($config['cookie_jar']));
165
                break;
166 10
            case 'decoder':
167 10
                $definition->addArgument([
168 10
                    'use_content_encoding' => $config['use_content_encoding'],
169 10
                ]);
170 10
                break;
171 10
            case 'history':
172
                $definition->replaceArgument(0, new Reference($config['journal']));
173
                break;
174 10
            case 'logger':
175 10
                $definition->replaceArgument(0, new Reference($config['logger']));
176 10
                if (!empty($config['formatter'])) {
177
                    $definition->replaceArgument(1, new Reference($config['formatter']));
178
                }
179 10
                break;
180 10
            case 'redirect':
181 10
                $definition->addArgument([
182 10
                    'preserve_header' => $config['preserve_header'],
183 10
                    'use_default_for_multiple' => $config['use_default_for_multiple'],
184 10
                ]);
185 10
                break;
186 10
            case 'retry':
187 10
                $definition->addArgument([
188 10
                    'retries' => $config['retry'],
189 10
                ]);
190 10
                break;
191 10
            case 'stopwatch':
192 10
                $definition->replaceArgument(0, new Reference($config['stopwatch']));
193 10
                break;
194
195
            /* client specific plugins */
196
197 3
            case 'add_host':
198 3
                $uriService = $serviceId.'.host_uri';
199 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...
200 3
                $definition->replaceArgument(0, new Reference($uriService));
201 3
                $definition->replaceArgument(1, [
202 3
                    'replace' => $config['replace'],
203 3
                ]);
204 3
                break;
205 1
            case 'header_append':
206 1
            case 'header_defaults':
207 1
            case 'header_set':
208 1
            case 'header_remove':
209 1
                $definition->replaceArgument(0, $config['headers']);
210 1
                break;
211
212
            default:
213
                throw new \InvalidArgumentException(sprintf('Internal exception: Plugin %s is not handled', $name));
214
        }
215 10
    }
216
217
    /**
218
     * @param ContainerBuilder $container
219
     * @param array            $config
220
     *
221
     * @return array List of service ids for the authentication plugins.
222
     */
223 3
    private function configureAuthentication(ContainerBuilder $container, array $config, $servicePrefix = 'httplug.plugin.authentication')
224
    {
225 3
        $pluginServices = [];
226
227 3
        foreach ($config as $name => $values) {
228 3
            $authServiceKey = sprintf($servicePrefix.'.%s.auth', $name);
229 3
            switch ($values['type']) {
230 3
                case 'bearer':
231
                    $container->register($authServiceKey, Bearer::class)
232
                        ->addArgument($values['token']);
233
                    break;
234
                case 'basic':
235
                    $container->register($authServiceKey, BasicAuth::class)
236
                        ->addArgument($values['username'])
237
                        ->addArgument($values['password']);
238
                    break;
239
                case 'wsse':
240
                    $container->register($authServiceKey, Wsse::class)
241
                        ->addArgument($values['username'])
242
                        ->addArgument($values['password']);
243
                    break;
244
                case 'service':
245
                    $authServiceKey = $values['service'];
246
                    break;
247
                default:
248
                    throw new \LogicException(sprintf('Unknown authentication type: "%s"', $values['type']));
249
            }
250
251
            $pluginServiceKey = $servicePrefix.'.'.$name;
252
            $container->register($pluginServiceKey, AuthenticationPlugin::class)
253
                ->addArgument(new Reference($authServiceKey))
254
            ;
255
            $pluginServices[] = $pluginServiceKey;
256
        }
257
258
        return $pluginServices;
259
    }
260
261
    /**
262
     * @param ContainerBuilder $container
263
     * @param string           $clientName
264
     * @param array            $arguments
265
     * @param bool             $profiling
266
     */
267
    private function configureClient(ContainerBuilder $container, $clientName, array $arguments, $profiling)
268
    {
269
        $serviceId = 'httplug.client.'.$clientName;
270
271
        $plugins = [];
272
        foreach ($arguments['plugins'] as $plugin) {
273
            list($pluginName, $pluginConfig) = each($plugin);
274
            if ('reference' === $pluginName) {
275
                $plugins[] = $pluginConfig['id'];
276
            } elseif ('authentication' === $pluginName) {
277
                $plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication'));
278
            } else {
279
                $plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig);
280
            }
281
        }
282
283
        $pluginClientOptions = [];
284
        if ($profiling) {
285
            //Decorate each plugin with a ProfilePlugin instance.
286
            foreach ($plugins as $pluginServiceId) {
287
                $this->decoratePluginWithProfilePlugin($container, $pluginServiceId);
288
            }
289
290
            // To profile the requests, add a StackPlugin as first plugin in the chain.
291
            $stackPluginId = $this->configureStackPlugin($container, $clientName, $serviceId);
292
            array_unshift($plugins, $stackPluginId);
293
        }
294
295
        $container
296
            ->register($serviceId, DummyClient::class)
297
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
298
            ->addArgument(
299
                array_map(
300
                    function ($id) {
301
                        return new Reference($id);
302
                    },
303
                    $plugins
304
                )
305
            )
306
            ->addArgument(new Reference($arguments['factory']))
307
            ->addArgument($arguments['config'])
308
            ->addArgument($pluginClientOptions)
309
        ;
310
311
        /*
312
         * Decorate the client with clients from client-common
313
         */
314 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...
315
            $container
316
                ->register($serviceId.'.flexible', FlexibleHttpClient::class)
317
                ->addArgument(new Reference($serviceId.'.flexible.inner'))
318
                ->setPublic(false)
319
                ->setDecoratedService($serviceId)
320
            ;
321
        }
322
323 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...
324
            $container
325
                ->register($serviceId.'.http_methods', HttpMethodsClient::class)
326
                ->setArguments([new Reference($serviceId.'.http_methods.inner'), new Reference('httplug.message_factory')])
327
                ->setPublic(false)
328
                ->setDecoratedService($serviceId)
329
            ;
330
        }
331
332 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...
333
            $container
334
                ->register($serviceId.'.batch_client', BatchClient::class)
335
                ->setArguments([new Reference($serviceId.'.batch_client.inner')])
336
                ->setPublic(false)
337
                ->setDecoratedService($serviceId)
338
            ;
339
        }
340
    }
341
342
    /**
343
     * Create a URI object with the default URI factory.
344
     *
345
     * @param ContainerBuilder $container
346
     * @param string           $serviceId Name of the private service to create
347
     * @param string           $uri       String representation of the URI
348
     */
349
    private function createUri(ContainerBuilder $container, $serviceId, $uri)
350
    {
351
        $container
352
            ->register($serviceId, UriInterface::class)
353
            ->setPublic(false)
354
            ->setFactory([new Reference('httplug.uri_factory'), 'createUri'])
355
            ->addArgument($uri)
356
        ;
357
    }
358
359
    /**
360
     * Make the user can select what client is used for auto discovery. If none is provided, a service will be created
361
     * by finding a client using auto discovery.
362
     *
363
     * @param ContainerBuilder $container
364
     * @param array            $config
365
     */
366
    private function configureAutoDiscoveryClients(ContainerBuilder $container, array $config)
367
    {
368
        $httpClient = $config['discovery']['client'];
369
370 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...
371
            if ($httpClient === 'auto') {
372
                $httpClient = $this->registerAutoDiscoverableClient(
373
                    $container,
374
                    'auto_discovered_client',
375
                    $this->configureAutoDiscoveryFactory(
376
                        $container,
377
                        HttpClientDiscovery::class,
378
                        'auto_discovered_client',
379
                        $config
380
                    ),
381
                    $this->isConfigEnabled($container, $config['profiling'])
382
                );
383
            }
384
385
            $httpClient = new Reference($httpClient);
386
        }
387
388
        $asyncHttpClient = $config['discovery']['async_client'];
389
390 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...
391
            if ($asyncHttpClient === 'auto') {
392
                $asyncHttpClient = $this->registerAutoDiscoverableClient(
393
                    $container,
394
                    'auto_discovered_async',
395
                    $this->configureAutoDiscoveryFactory(
396
                        $container,
397
                        HttpAsyncClientDiscovery::class,
398
                        'auto_discovered_async',
399
                        $config
400
                    ),
401
                    $this->isConfigEnabled($container, $config['profiling'])
402
                );
403
            }
404
405
            $asyncHttpClient = new Reference($asyncHttpClient);
406
        }
407
408
        if (null === $httpClient && null === $asyncHttpClient) {
409
            $container->removeDefinition('httplug.strategy');
410
411
            return;
412
        }
413
414
        $container
415
            ->getDefinition('httplug.strategy')
416
            ->addArgument($httpClient)
417
            ->addArgument($asyncHttpClient)
418
        ;
419
    }
420
421
    /**
422
     * Find a client with auto discovery and return a service Reference to it.
423
     *
424
     * @param ContainerBuilder   $container
425
     * @param string             $name
426
     * @param Reference|callable $factory
427
     * @param bool               $profiling
428
     *
429
     * @return string service id
430
     */
431
    private function registerAutoDiscoverableClient(ContainerBuilder $container, $name, $factory, $profiling)
432
    {
433
        $serviceId = 'httplug.auto_discovery.'.$name;
434
435
        $plugins = [];
436
        if ($profiling) {
437
            // To profile the requests, add a StackPlugin as first plugin in the chain.
438
            $plugins[] = $this->configureStackPlugin($container, $name, $serviceId);
439
        }
440
441
        $container
442
            ->register($serviceId, DummyClient::class)
443
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
444
            ->setArguments([
445
                array_map(
446
                    function ($id) {
447
                        return new Reference($id);
448
                    },
449
                    $plugins
450
                ),
451
                $factory,
452
                [],
453
            ])
454
        ;
455
456
        if ($profiling) {
457
            $collector = $container->getDefinition('httplug.collector.collector');
458
            $collector->replaceArgument(0, array_merge($collector->getArgument(0), [$name]));
459
        }
460
461
        return $serviceId;
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467
    public function getConfiguration(array $config, ContainerBuilder $container)
468
    {
469
        return new Configuration($container->getParameter('kernel.debug'));
470
    }
471
472
    /**
473
     * Configure a plugin using the parent definition from plugins.xml.
474
     *
475
     * @param ContainerBuilder $container
476
     * @param string           $serviceId
477
     * @param string           $pluginName
478
     * @param array            $pluginConfig
479
     *
480
     * @return string configured service id
481
     */
482
    private function configurePlugin(ContainerBuilder $container, $serviceId, $pluginName, array $pluginConfig)
483
    {
484
        $pluginServiceId = $serviceId.'.plugin.'.$pluginName;
485
486
        $definition = class_exists(ChildDefinition::class)
487
            ? new ChildDefinition('httplug.plugin.'.$pluginName)
488
            : new DefinitionDecorator('httplug.plugin.'.$pluginName);
489
490
        $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
491
        $container->setDefinition($pluginServiceId, $definition);
492
493
        return $pluginServiceId;
494
    }
495
496
    /**
497
     * Decorate the plugin service with a ProfilePlugin service.
498
     *
499
     * @param ContainerBuilder $container
500
     * @param string           $pluginServiceId
501
     */
502
    private function decoratePluginWithProfilePlugin(ContainerBuilder $container, $pluginServiceId)
503
    {
504
        $container->register($pluginServiceId.'.debug', ProfilePlugin::class)
505
            ->setDecoratedService($pluginServiceId)
506
            ->setArguments([
507
                new Reference($pluginServiceId.'.debug.inner'),
508
                new Reference('httplug.collector.collector'),
509
                new Reference('httplug.collector.formatter'),
510
                $pluginServiceId,
511
            ])
512
            ->setPublic(false);
513
    }
514
515
    /**
516
     * Configure a StackPlugin for a client.
517
     *
518
     * @param ContainerBuilder $container
519
     * @param string           $clientName Client name to display in the profiler.
520
     * @param string           $serviceId  Client service id. Used as base for the StackPlugin service id.
521
     *
522
     * @return string configured StackPlugin service id
523
     */
524
    private function configureStackPlugin(ContainerBuilder $container, $clientName, $serviceId)
525
    {
526
        $pluginServiceId = $serviceId.'.plugin.stack';
527
528
        $definition = class_exists(ChildDefinition::class)
529
            ? new ChildDefinition('httplug.plugin.stack')
530
            : new DefinitionDecorator('httplug.plugin.stack');
531
532
        $definition->addArgument($clientName);
533
        $container->setDefinition($pluginServiceId, $definition);
534
535
        return $pluginServiceId;
536
    }
537
538
    /**
539
     * Configure the discovery factory when profiling is enabled to get client decorated with a ProfileClient.
540
     *
541
     * @param ContainerBuilder $container
542
     * @param string           $discovery
543
     * @param string           $name
544
     * @param array            $config
545
     *
546
     * @return callable|Reference
547
     */
548
    private function configureAutoDiscoveryFactory(ContainerBuilder $container, $discovery, $name, array $config)
549
    {
550
        $factory = [$discovery, 'find'];
551
        if ($this->isConfigEnabled($container, $config['profiling'])) {
552
            $factoryServiceId = 'httplug.auto_discovery.'.$name.'.factory';
553
            $container->register($factoryServiceId, ProfileClientFactory::class)
554
                ->setPublic(false)
555
                ->setArguments([
556
                    $factory,
557
                    new Reference('httplug.collector.collector'),
558
                    new Reference('httplug.collector.formatter'),
559
                    new Reference('debug.stopwatch'),
560
                ]);
561
            $factory = new Reference($factoryServiceId);
562
        }
563
564
        return $factory;
565
    }
566
}
567