Completed
Pull Request — master (#154)
by Fabien
10:26
created

HttplugExtension::configureNewStackPlugin()   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 3
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\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.formatter')
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, 'httplug.plugin.stopwatch');
284
            }
285
286
            //Decorate each plugin with a ProfilePlugin instance.
287
            foreach ($plugins as $pluginServiceId) {
288
                $this->decoratePluginWithProfilePlugin($container, $pluginServiceId);
289
            }
290
291
            // Add the newstack plugin
292
            $newStackPluginId = $this->configureNewStackPlugin($container, $clientName, $serviceId);
293
            array_unshift($plugins, $newStackPluginId);
294
        }
295
296
        $container
297
            ->register($serviceId, DummyClient::class)
298
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
299
            ->addArgument(
300
                array_map(
301
                    function ($id) {
302
                        return new Reference($id);
303
                    },
304
                    $plugins
305
                )
306
            )
307
            ->addArgument(new Reference($arguments['factory']))
308
            ->addArgument($arguments['config'])
309
            ->addArgument($pluginClientOptions)
310
        ;
311
312
        /*
313
         * Decorate the client with clients from client-common
314
         */
315 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...
316
            $container
317
                ->register($serviceId.'.flexible', FlexibleHttpClient::class)
318
                ->addArgument(new Reference($serviceId.'.flexible.inner'))
319
                ->setPublic(false)
320
                ->setDecoratedService($serviceId)
321
            ;
322
        }
323
324 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...
325
            $container
326
                ->register($serviceId.'.http_methods', HttpMethodsClient::class)
327
                ->setArguments([new Reference($serviceId.'.http_methods.inner'), new Reference('httplug.message_factory')])
328
                ->setPublic(false)
329
                ->setDecoratedService($serviceId)
330
            ;
331
        }
332
333 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...
334
            $container
335
                ->register($serviceId.'.batch_client', BatchClient::class)
336
                ->setArguments([new Reference($serviceId.'.batch_client.inner')])
337
                ->setPublic(false)
338
                ->setDecoratedService($serviceId)
339
            ;
340
        }
341
    }
342
343
    /**
344
     * Create a URI object with the default URI factory.
345
     *
346
     * @param ContainerBuilder $container
347
     * @param string           $serviceId Name of the private service to create
348
     * @param string           $uri       String representation of the URI
349
     */
350
    private function createUri(ContainerBuilder $container, $serviceId, $uri)
351
    {
352
        $container
353
            ->register($serviceId, UriInterface::class)
354
            ->setPublic(false)
355
            ->setFactory([new Reference('httplug.uri_factory'), 'createUri'])
356
            ->addArgument($uri)
357
        ;
358
    }
359
360
    /**
361
     * Make the user can select what client is used for auto discovery. If none is provided, a service will be created
362
     * by finding a client using auto discovery.
363
     *
364
     * @param ContainerBuilder $container
365
     * @param array            $config
366
     */
367
    private function configureAutoDiscoveryClients(ContainerBuilder $container, array $config)
368
    {
369
        $httpClient = $config['discovery']['client'];
370
371 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...
372
            if ($httpClient === 'auto') {
373
                $httpClient = $this->registerAutoDiscoverableClient(
374
                    $container,
375
                    'auto_discovered_client',
376
                    [HttpClientDiscovery::class, 'find'],
377
                    $this->isConfigEnabled($container, $config['profiling'])
378
                );
379
            }
380
381
            $httpClient = new Reference($httpClient);
382
        }
383
384
        $asyncHttpClient = $config['discovery']['async_client'];
385
386 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...
387
            if ($asyncHttpClient === 'auto') {
388
                $asyncHttpClient = $this->registerAutoDiscoverableClient(
389
                    $container,
390
                    'auto_discovered_async',
391
                    [HttpAsyncClientDiscovery::class, 'find'],
392
                    $this->isConfigEnabled($container, $config['profiling'])
393
                );
394
            }
395
396
            $asyncHttpClient = new Reference($asyncHttpClient);
397
        }
398
399
        $container
400
            ->getDefinition('httplug.strategy')
401
            ->addArgument($httpClient)
402
            ->addArgument($asyncHttpClient)
403
        ;
404
    }
405
406
    /**
407
     * Find a client with auto discovery and return a service Reference to it.
408
     *
409
     * @param ContainerBuilder $container
410
     * @param string           $name
411
     * @param callable         $factory
412
     * @param bool             $profiling
413
     *
414
     * @return string service id
415
     */
416
    private function registerAutoDiscoverableClient(ContainerBuilder $container, $name, $factory, $profiling)
417
    {
418
        $serviceId = 'httplug.auto_discovery.'.$name;
419
420
        $plugins = [];
421
        if ($profiling) {
422
            // Add the newstack plugin
423
            $plugins[] = $this->configureNewStackPlugin($container, $name, $serviceId);
424
425
            $this->decoratePluginWithProfilePlugin($container, 'httplug.plugin.stopwatch');
426
            $plugins[] = 'httplug.plugin.stopwatch';
427
        }
428
429
        $container
430
            ->register($serviceId, DummyClient::class)
431
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
432
            ->setArguments([
433
                array_map(
434
                    function ($id) {
435
                        return new Reference($id);
436
                    },
437
                    $plugins
438
                ),
439
                $factory,
440
                [],
441
            ])
442
        ;
443
444
        return $serviceId;
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     */
450
    public function getConfiguration(array $config, ContainerBuilder $container)
451
    {
452
        return new Configuration($container->getParameter('kernel.debug'));
453
    }
454
455
    /**
456
     * @param ContainerBuilder $container
457
     * @param string           $serviceId
458
     * @param string           $pluginName
459
     * @param array            $pluginConfig
460
     *
461
     * @return string
462
     */
463
    private function configurePlugin(ContainerBuilder $container, $serviceId, $pluginName, array $pluginConfig)
464
    {
465
        $pluginServiceId = $serviceId.'.plugin.'.$pluginName;
466
467
        $definition = class_exists(ChildDefinition::class)
468
            ? new ChildDefinition('httplug.plugin'.'.'.$pluginName)
469
            : new DefinitionDecorator('httplug.plugin'.'.'.$pluginName);
470
471
        $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
472
        $container->setDefinition($pluginServiceId, $definition);
473
474
        return $pluginServiceId;
475
    }
476
477
    /**
478
     * @param ContainerBuilder $container
479
     * @param $pluginServiceId
480
     */
481
    private function decoratePluginWithProfilePlugin(ContainerBuilder $container, $pluginServiceId)
482
    {
483
        $container->register($pluginServiceId.'.debug', ProfilePlugin::class)
484
            ->setDecoratedService($pluginServiceId)
485
            ->setArguments([
486
                new Reference($pluginServiceId.'.debug.inner'),
487
                new Reference('httplug.collector.collector'),
488
                new Reference('httplug.collector.formatter'),
489
                $pluginServiceId,
490
            ])
491
            ->setPublic(false);
492
    }
493
494
    /**
495
     * @param ContainerBuilder $container
496
     * @param string           $clientName
497
     * @param string           $serviceId
498
     *
499
     * @return string
500
     */
501
    private function configureNewStackPlugin(ContainerBuilder $container, $clientName, $serviceId)
502
    {
503
        $pluginServiceId = $serviceId.'.plugin.newstack';
504
505
        $definition = class_exists(ChildDefinition::class)
506
            ? new ChildDefinition('httplug.plugin.stack')
507
            : new DefinitionDecorator('httplug.plugin.stack');
508
509
        $definition->addArgument($clientName);
510
        $container->setDefinition($pluginServiceId, $definition);
511
512
        return $pluginServiceId;
513
    }
514
}
515