Completed
Pull Request — master (#127)
by Pascal
08:48
created

HttplugExtension::load()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 6.0493

Importance

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