Completed
Pull Request — master (#128)
by Fabien
18:17 queued 08:23
created

HttplugExtension::configureClients()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0087

Importance

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