Completed
Pull Request — master (#154)
by Fabien
07:00
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\ProfilePlugin;
14
use Http\Message\Authentication\BasicAuth;
15
use Http\Message\Authentication\Bearer;
16
use Http\Message\Authentication\Wsse;
17
use Psr\Http\Message\UriInterface;
18
use Symfony\Component\Config\FileLocator;
19
use Symfony\Component\DependencyInjection\ChildDefinition;
20
use Symfony\Component\DependencyInjection\ContainerBuilder;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22
use Symfony\Component\DependencyInjection\Definition;
23
use Symfony\Component\DependencyInjection\DefinitionDecorator;
24
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
25
use Symfony\Component\DependencyInjection\Reference;
26
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
27
28
/**
29
 * @author David Buchmann <[email protected]>
30
 * @author Tobias Nyholm <[email protected]>
31
 */
32
class HttplugExtension extends Extension
33
{
34
    /**
35
     * {@inheritdoc}
36
     */
37 10
    public function load(array $configs, ContainerBuilder $container)
38
    {
39 10
        $configuration = $this->getConfiguration($configs, $container);
40 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 39 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...
41
42 10
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
43
44 10
        $loader->load('services.xml');
45 10
        $loader->load('plugins.xml');
46
47
        // Register default services
48 10
        foreach ($config['classes'] as $service => $class) {
49 10
            if (!empty($class)) {
50 1
                $container->register(sprintf('httplug.%s.default', $service), $class);
51 1
            }
52 10
        }
53
54
        // Set main aliases
55 10
        foreach ($config['main_alias'] as $type => $id) {
56 10
            $container->setAlias(sprintf('httplug.%s', $type), $id);
57 10
        }
58
59
        // Configure toolbar
60 10
        if ($this->isConfigEnabled($container, $config['profiling'])) {
61 7
            $loader->load('data-collector.xml');
62
63 7
            if (!empty($config['profiling']['formatter'])) {
64
                // Add custom formatter
65
                $container
66 1
                    ->getDefinition('httplug.collector.debug_collector')
67 1
                    ->replaceArgument(0, new Reference($config['profiling']['formatter']))
68
                ;
69 1
            }
70
71
            $container
72 7
                ->getDefinition('httplug.formatter.full_http_message')
73 7
                ->addArgument($config['profiling']['captured_body_length'])
74
            ;
75 7
        }
76
77 10
        $this->configureClients($container, $config, $this->isConfigEnabled($container, $config['profiling']));
78 10
        $this->configureSharedPlugins($container, $config['plugins']); // must be after clients, as clients.X.plugins might use plugins as templates that will be removed
79 10
        $this->configureAutoDiscoveryClients($container, $config);
80 10
    }
81
82
    /**
83
     * Configure client services.
84
     *
85
     * @param ContainerBuilder $container
86
     * @param array            $config
87
     * @param bool             $profiling
88
     */
89 10
    private function configureClients(ContainerBuilder $container, array $config, $profiling)
90
    {
91 10
        $first = null;
92 10
        $clients = [];
93
94 10
        foreach ($config['clients'] as $name => $arguments) {
95 6
            if ($first === null) {
96
                // Save the name of the first configured client.
97 6
                $first = $name;
98 6
            }
99
100 6
            $this->configureClient($container, $name, $arguments, $this->isConfigEnabled($container, $config['profiling']));
101 6
            $clients[] = $name;
102 10
        }
103
104
        // If we have clients configured
105 10
        if ($first !== null) {
106
            // If we do not have a client named 'default'
107 6
            if (!isset($config['clients']['default'])) {
108
                // Alias the first client to httplug.client.default
109 6
                $container->setAlias('httplug.client.default', 'httplug.client.'.$first);
110 6
            }
111 6
        }
112
113 10
        if ($profiling) {
114 7
            $container->getDefinition('httplug.collector.collector')
115 7
                ->setArguments([$clients])
116
            ;
117 7
        }
118 10
    }
119
120
    /**
121
     * @param ContainerBuilder $container
122
     * @param array            $config
123
     */
124 10
    private function configureSharedPlugins(ContainerBuilder $container, array $config)
125
    {
126 10
        if (!empty($config['authentication'])) {
127
            $this->configureAuthentication($container, $config['authentication']);
128
        }
129 10
        unset($config['authentication']);
130
131 10
        foreach ($config as $name => $pluginConfig) {
132 10
            $pluginId = 'httplug.plugin.'.$name;
133
134 10
            if ($this->isConfigEnabled($container, $pluginConfig)) {
135 10
                $def = $container->getDefinition($pluginId);
136 10
                $this->configurePluginByName($name, $def, $pluginConfig, $container, $pluginId);
137 10
            } else {
138 10
                $container->removeDefinition($pluginId);
139
            }
140 10
        }
141 10
    }
142
143
    /**
144
     * @param string           $name
145
     * @param Definition       $definition
146
     * @param array            $config
147
     * @param ContainerBuilder $container  In case we need to add additional services for this plugin
148
     * @param string           $serviceId  Service id of the plugin, in case we need to add additional services for this plugin.
149
     */
150 10
    private function configurePluginByName($name, Definition $definition, array $config, ContainerInterface $container, $serviceId)
151
    {
152
        switch ($name) {
153 10
            case 'cache':
154
                $definition
155
                    ->replaceArgument(0, new Reference($config['cache_pool']))
156
                    ->replaceArgument(1, new Reference($config['stream_factory']))
157
                    ->replaceArgument(2, $config['config']);
158
                break;
159 10
            case 'cookie':
160
                $definition->replaceArgument(0, new Reference($config['cookie_jar']));
161
                break;
162 10
            case 'decoder':
163 10
                $definition->addArgument([
164 10
                    'use_content_encoding' => $config['use_content_encoding'],
165 10
                ]);
166 10
                break;
167 10
            case 'history':
168
                $definition->replaceArgument(0, new Reference($config['journal']));
169
                break;
170 10
            case 'logger':
171 10
                $definition->replaceArgument(0, new Reference($config['logger']));
172 10
                if (!empty($config['formatter'])) {
173
                    $definition->replaceArgument(1, new Reference($config['formatter']));
174
                }
175 10
                break;
176 10
            case 'redirect':
177 10
                $definition->addArgument([
178 10
                    'preserve_header' => $config['preserve_header'],
179 10
                    'use_default_for_multiple' => $config['use_default_for_multiple'],
180 10
                ]);
181 10
                break;
182 10
            case 'retry':
183 10
                $definition->addArgument([
184 10
                    'retries' => $config['retry'],
185 10
                ]);
186 10
                break;
187 10
            case 'stopwatch':
188 10
                $definition->replaceArgument(0, new Reference($config['stopwatch']));
189 10
                break;
190
191
            /* client specific plugins */
192
193 3
            case 'add_host':
194 3
                $uriService = $serviceId.'.host_uri';
195 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...
196 3
                $definition->replaceArgument(0, new Reference($uriService));
197 3
                $definition->replaceArgument(1, [
198 3
                    'replace' => $config['replace'],
199 3
                ]);
200 3
                break;
201 1
            case 'header_append':
202 1
            case 'header_defaults':
203 1
            case 'header_set':
204 1
            case 'header_remove':
205 1
                $definition->replaceArgument(0, $config['headers']);
206 1
                break;
207
208
            default:
209
                throw new \InvalidArgumentException(sprintf('Internal exception: Plugin %s is not handled', $name));
210
        }
211 10
    }
212
213
    /**
214
     * @param ContainerBuilder $container
215
     * @param array            $config
216
     *
217
     * @return array List of service ids for the authentication plugins.
218
     */
219 3
    private function configureAuthentication(ContainerBuilder $container, array $config, $servicePrefix = 'httplug.plugin.authentication')
220
    {
221 3
        $pluginServices = [];
222
223 3
        foreach ($config as $name => $values) {
224 3
            $authServiceKey = sprintf($servicePrefix.'.%s.auth', $name);
225 3
            switch ($values['type']) {
226 3
                case 'bearer':
227
                    $container->register($authServiceKey, Bearer::class)
228
                        ->addArgument($values['token']);
229
                    break;
230 3
                case 'basic':
231
                    $container->register($authServiceKey, BasicAuth::class)
232 3
                        ->addArgument($values['username'])
233 3
                        ->addArgument($values['password']);
234 3
                    break;
235
                case 'wsse':
236
                    $container->register($authServiceKey, Wsse::class)
237
                        ->addArgument($values['username'])
238
                        ->addArgument($values['password']);
239
                    break;
240
                case 'service':
241
                    $authServiceKey = $values['service'];
242
                    break;
243
                default:
244
                    throw new \LogicException(sprintf('Unknown authentication type: "%s"', $values['type']));
245 3
            }
246
247 3
            $pluginServiceKey = $servicePrefix.'.'.$name;
248
            $container->register($pluginServiceKey, AuthenticationPlugin::class)
249
                ->addArgument(new Reference($authServiceKey))
250
            ;
251
            $pluginServices[] = $pluginServiceKey;
252
        }
253
254
        return $pluginServices;
255
    }
256
257
    /**
258
     * @param ContainerBuilder $container
259
     * @param string           $clientName
260
     * @param array            $arguments
261
     * @param bool             $profiling
262
     */
263
    private function configureClient(ContainerBuilder $container, $clientName, array $arguments, $profiling)
264
    {
265
        $serviceId = 'httplug.client.'.$clientName;
266
267
        $plugins = [];
268
        foreach ($arguments['plugins'] as $plugin) {
269
            list($pluginName, $pluginConfig) = each($plugin);
270
            if ('reference' === $pluginName) {
271
                $plugins[] = $pluginConfig['id'];
272
            } elseif ('authentication' === $pluginName) {
273
                $plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication'));
274
            } else {
275
                $plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig);
276
            }
277
        }
278
279
        $pluginClientOptions = [];
280
        if ($profiling) {
281
            // Add the stopwatch plugin
282
            if (!in_array('httplug.plugin.stopwatch', $arguments['plugins'])) {
283
                array_unshift($plugins, $this->configurePlugin($container, $serviceId, 'stopwatch', [
284
                    'stopwatch' => 'debug.stopwatch',
285
                ]));
286
            }
287
288
            //Decorate each plugin with a ProfilePlugin instance.
289
            foreach ($plugins as $pluginServiceId) {
290
                $this->decoratePluginWithProfilePlugin($container, $pluginServiceId);
291
            }
292
293
            # Add the newstack plugin
294
            $newStackPluginId = $this->configureNewStackPlugin($container, $clientName, $serviceId);
295
            array_unshift($plugins, $newStackPluginId);
296
        }
297
298
        $container
299
            ->register($serviceId, DummyClient::class)
300
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
301
            ->addArgument(
302
                array_map(
303
                    function ($id) {
304
                        return new Reference($id);
305
                    },
306
                    $plugins
307
                )
308
            )
309
            ->addArgument(new Reference($arguments['factory']))
310
            ->addArgument($arguments['config'])
311
            ->addArgument($pluginClientOptions)
312
        ;
313
314
        /*
315
         * Decorate the client with clients from client-common
316
         */
317 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...
318
            $container
319
                ->register($serviceId.'.flexible', FlexibleHttpClient::class)
320
                ->addArgument(new Reference($serviceId.'.flexible.inner'))
321
                ->setPublic(false)
322
                ->setDecoratedService($serviceId)
323
            ;
324
        }
325
326 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...
327
            $container
328
                ->register($serviceId.'.http_methods', HttpMethodsClient::class)
329
                ->setArguments([new Reference($serviceId.'.http_methods.inner'), new Reference('httplug.message_factory')])
330
                ->setPublic(false)
331
                ->setDecoratedService($serviceId)
332
            ;
333
        }
334
335 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...
336
            $container
337
                ->register($serviceId.'.batch_client', BatchClient::class)
338
                ->setArguments([new Reference($serviceId.'.batch_client.inner')])
339
                ->setPublic(false)
340
                ->setDecoratedService($serviceId)
341
            ;
342
        }
343
    }
344
345
    /**
346
     * Create a URI object with the default URI factory.
347
     *
348
     * @param ContainerBuilder $container
349
     * @param string           $serviceId Name of the private service to create
350
     * @param string           $uri       String representation of the URI
351
     */
352
    private function createUri(ContainerBuilder $container, $serviceId, $uri)
353
    {
354
        $container
355
            ->register($serviceId, UriInterface::class)
356
            ->setPublic(false)
357
            ->setFactory([new Reference('httplug.uri_factory'), 'createUri'])
358
            ->addArgument($uri)
359
        ;
360
    }
361
362
    /**
363
     * Make the user can select what client is used for auto discovery. If none is provided, a service will be created
364
     * by finding a client using auto discovery.
365
     *
366
     * @param ContainerBuilder $container
367
     * @param array            $config
368
     */
369
    private function configureAutoDiscoveryClients(ContainerBuilder $container, array $config)
370
    {
371
        $httpClient = $config['discovery']['client'];
372
373 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...
374
            if ($httpClient === 'auto') {
375
                $httpClient = $this->registerAutoDiscoverableClient(
376
                    $container,
377
                    'auto_discovered_client',
378
                    [HttpClientDiscovery::class, 'find'],
379
                    $this->isConfigEnabled($container, $config['profiling'])
380
                );
381
            }
382
383
            $httpClient = new Reference($httpClient);
384
        }
385
386
        $asyncHttpClient = $config['discovery']['async_client'];
387
388 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...
389
            if ($asyncHttpClient === 'auto') {
390
                $asyncHttpClient = $this->registerAutoDiscoverableClient(
391
                    $container,
392
                    'auto_discovered_async',
393
                    [HttpAsyncClientDiscovery::class, 'find'],
394
                    $this->isConfigEnabled($container, $config['profiling'])
395
                );
396
            }
397
398
            $asyncHttpClient = new Reference($asyncHttpClient);
399
        }
400
401
        $container
402
            ->getDefinition('httplug.strategy')
403
            ->addArgument($httpClient)
404
            ->addArgument($asyncHttpClient)
405
        ;
406
    }
407
408
    /**
409
     * Find a client with auto discovery and return a service Reference to it.
410
     *
411
     * @param ContainerBuilder $container
412
     * @param string           $name
413
     * @param callable         $factory
414
     * @param bool             $profiling
415
     *
416
     * @return string service id
417
     */
418
    private function registerAutoDiscoverableClient(ContainerBuilder $container, $name, $factory, $profiling)
419
    {
420
        $serviceId = 'httplug.auto_discovery.'.$name;
421
422
        $plugins = [];
423
        if ($profiling) {
424
            # Add the newstack plugin
425
            $plugins[] = $this->configureNewStackPlugin($container, $name, $serviceId);
426
427
            # Add the stopwatch plugin
428
            $stopwatch = $this->configurePlugin($container, $serviceId, 'stopwatch', [
429
                'stopwatch' => 'debug.stopwatch',
430
            ]);
431
            $this->decoratePluginWithProfilePlugin($container, $stopwatch);
432
            $plugins[] = $stopwatch;
433
        }
434
435
        $container
436
            ->register($serviceId, DummyClient::class)
437
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
438
            ->setArguments([
439
                array_map(
440
                    function ($id) {
441
                        return new Reference($id);
442
                    },
443
                    $plugins
444
                ),
445
                $factory,
446
                [],
447
            ])
448
        ;
449
450
        return $serviceId;
451
    }
452
453
    /**
454
     * {@inheritdoc}
455
     */
456
    public function getConfiguration(array $config, ContainerBuilder $container)
457
    {
458
        return new Configuration($container->getParameter('kernel.debug'));
459
    }
460
461
    /**
462
     * @param ContainerBuilder $container
463
     * @param string           $serviceId
464
     * @param string           $pluginName
465
     * @param array            $pluginConfig
466
     *
467
     * @return string
468
     */
469
    private function configurePlugin(ContainerBuilder $container, $serviceId, $pluginName, array $pluginConfig)
470
    {
471
        $pluginServiceId = $serviceId . '.plugin.' . $pluginName;
472
473
        $definition = class_exists(ChildDefinition::class)
474
            ? new ChildDefinition('httplug.plugin' . '.' . $pluginName)
475
            : new DefinitionDecorator('httplug.plugin' . '.' . $pluginName);
476
477
        $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
478
        $container->setDefinition($pluginServiceId, $definition);
479
480
        return $pluginServiceId;
481
    }
482
483
    /**
484
     * @param ContainerBuilder $container
485
     * @param $pluginServiceId
486
     */
487
    private function decoratePluginWithProfilePlugin(ContainerBuilder $container, $pluginServiceId)
488
    {
489
        $container->register($pluginServiceId . '.debug', ProfilePlugin::class)
490
            ->setDecoratedService($pluginServiceId)
491
            ->setArguments([
492
                new Reference($pluginServiceId . '.debug.inner'),
493
                new Reference('httplug.collector.collector'),
494
                new Reference('httplug.collector.formatter'),
495
                $pluginServiceId,
496
            ])
497
            ->setPublic(false);
498
    }
499
500
    /**
501
     * @param ContainerBuilder $container
502
     * @param string           $clientName
503
     * @param string           $serviceId
504
     *
505
     * @return string
506
     */
507
    private function configureNewStackPlugin(ContainerBuilder $container, $clientName, $serviceId)
508
    {
509
        $pluginServiceId = $serviceId . '.plugin.newstack';
510
511
        $definition = class_exists(ChildDefinition::class)
512
            ? new ChildDefinition('httplug.plugin.stack')
513
            : new DefinitionDecorator('httplug.plugin.stack');
514
515
        $definition->addArgument($clientName);
516
        $container->setDefinition($pluginServiceId, $definition);
517
518
        return $pluginServiceId;
519
    }
520
}
521