Completed
Pull Request — master (#128)
by Fabien
08:06
created

HttplugExtension::load()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.0553

Importance

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