Completed
Push — master ( b059ce...356aff )
by Tobias
10:36 queued 07:08
created

HttplugExtension::configurePlugins()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0466

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 12
cts 14
cp 0.8571
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
nc 6
nop 2
crap 4.0466
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\ProfileClientFactory;
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\ChildDefinition;
21
use Symfony\Component\DependencyInjection\ContainerBuilder;
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);
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
        $profilingEnabled = $this->isConfigEnabled($container, $config['profiling']);
61 10
        if ($profilingEnabled) {
62 7
            $loader->load('data-collector.xml');
63
64 7
            if (!empty($config['profiling']['formatter'])) {
65
                // Add custom formatter
66
                $container
67 1
                    ->getDefinition('httplug.collector.formatter')
68 1
                    ->replaceArgument(0, new Reference($config['profiling']['formatter']))
69
                ;
70 1
            }
71
72
            $container
73 7
                ->getDefinition('httplug.formatter.full_http_message')
74 7
                ->addArgument($config['profiling']['captured_body_length'])
75
            ;
76 7
        }
77
78 10
        $this->configureClients($container, $config, $profilingEnabled);
79 10
        $this->configurePlugins($container, $config['plugins']); // must be after clients, as clients.X.plugins might use plugins as templates that will be removed
80 10
        $this->configureAutoDiscoveryClients($container, $config);
81 10
    }
82
83
    /**
84
     * Configure client services.
85
     *
86
     * @param ContainerBuilder $container
87
     * @param array            $config
88
     * @param bool             $profiling
89
     */
90 10
    private function configureClients(ContainerBuilder $container, array $config, $profiling)
91
    {
92 10
        $first = null;
93 10
        $clients = [];
94
95 10
        foreach ($config['clients'] as $name => $arguments) {
96 6
            if ($first === null) {
97
                // Save the name of the first configured client.
98 6
                $first = $name;
99 6
            }
100
101 6
            $this->configureClient($container, $name, $arguments, $this->isConfigEnabled($container, $config['profiling']));
102 6
            $clients[] = $name;
103 10
        }
104
105
        // If we have clients configured
106 10
        if ($first !== null) {
107
            // If we do not have a client named 'default'
108 6
            if (!isset($config['clients']['default'])) {
109
                // Alias the first client to httplug.client.default
110 6
                $container->setAlias('httplug.client.default', 'httplug.client.'.$first);
111 6
            }
112 6
        }
113
114 10
        if ($profiling) {
115 7
            $container->getDefinition('httplug.collector.collector')
116 7
                ->setArguments([$clients])
117
            ;
118 7
        }
119 10
    }
120
121
    /**
122
     * Configure all Httplug plugins or remove their service definition.
123
     *
124
     * @param ContainerBuilder $container
125
     * @param array            $config
126
     */
127 10
    private function configurePlugins(ContainerBuilder $container, array $config)
128
    {
129 10
        if (!empty($config['authentication'])) {
130
            $this->configureAuthentication($container, $config['authentication']);
131
        }
132 10
        unset($config['authentication']);
133
134 10
        foreach ($config as $name => $pluginConfig) {
135 10
            $pluginId = 'httplug.plugin.'.$name;
136
137 10
            if ($this->isConfigEnabled($container, $pluginConfig)) {
138 10
                $def = $container->getDefinition($pluginId);
139 10
                $this->configurePluginByName($name, $def, $pluginConfig, $container, $pluginId);
140 10
            } else {
141 10
                $container->removeDefinition($pluginId);
142
            }
143 10
        }
144 10
    }
145
146
    /**
147
     * @param string           $name
148
     * @param Definition       $definition
149
     * @param array            $config
150
     * @param ContainerBuilder $container  In case we need to add additional services for this plugin
151
     * @param string           $serviceId  Service id of the plugin, in case we need to add additional services for this plugin.
152
     */
153 10
    private function configurePluginByName($name, Definition $definition, array $config, ContainerBuilder $container, $serviceId)
154
    {
155
        switch ($name) {
156 10
            case 'cache':
157
                $definition
158
                    ->replaceArgument(0, new Reference($config['cache_pool']))
159
                    ->replaceArgument(1, new Reference($config['stream_factory']))
160
                    ->replaceArgument(2, $config['config']);
161
                break;
162 10
            case 'cookie':
163
                $definition->replaceArgument(0, new Reference($config['cookie_jar']));
164
                break;
165 10
            case 'decoder':
166 10
                $definition->addArgument([
167 10
                    'use_content_encoding' => $config['use_content_encoding'],
168 10
                ]);
169 10
                break;
170 10
            case 'history':
171
                $definition->replaceArgument(0, new Reference($config['journal']));
172
                break;
173 10
            case 'logger':
174 10
                $definition->replaceArgument(0, new Reference($config['logger']));
175 10
                if (!empty($config['formatter'])) {
176
                    $definition->replaceArgument(1, new Reference($config['formatter']));
177
                }
178 10
                break;
179 10
            case 'redirect':
180 10
                $definition->addArgument([
181 10
                    'preserve_header' => $config['preserve_header'],
182 10
                    'use_default_for_multiple' => $config['use_default_for_multiple'],
183 10
                ]);
184 10
                break;
185 10
            case 'retry':
186 10
                $definition->addArgument([
187 10
                    'retries' => $config['retry'],
188 10
                ]);
189 10
                break;
190 10
            case 'stopwatch':
191 10
                $definition->replaceArgument(0, new Reference($config['stopwatch']));
192 10
                break;
193
194
            /* client specific plugins */
195
196 3
            case 'add_host':
197 3
                $uriService = $serviceId.'.host_uri';
198 3
                $this->createUri($container, $uriService, $config['host']);
199 3
                $definition->replaceArgument(0, new Reference($uriService));
200 3
                $definition->replaceArgument(1, [
201 3
                    'replace' => $config['replace'],
202 3
                ]);
203 3
                break;
204 1
            case 'header_append':
205 1
            case 'header_defaults':
206 1
            case 'header_set':
207 1
            case 'header_remove':
208 1
                $definition->replaceArgument(0, $config['headers']);
209 1
                break;
210
211
            default:
212
                throw new \InvalidArgumentException(sprintf('Internal exception: Plugin %s is not handled', $name));
213
        }
214 10
    }
215
216
    /**
217
     * @param ContainerBuilder $container
218
     * @param array            $config
219
     *
220
     * @return array List of service ids for the authentication plugins.
221
     */
222 3
    private function configureAuthentication(ContainerBuilder $container, array $config, $servicePrefix = 'httplug.plugin.authentication')
223
    {
224 3
        $pluginServices = [];
225
226 3
        foreach ($config as $name => $values) {
227 3
            $authServiceKey = sprintf($servicePrefix.'.%s.auth', $name);
228 3
            switch ($values['type']) {
229 3
                case 'bearer':
230
                    $container->register($authServiceKey, Bearer::class)
231
                        ->addArgument($values['token']);
232
                    break;
233
                case 'basic':
234
                    $container->register($authServiceKey, BasicAuth::class)
235
                        ->addArgument($values['username'])
236
                        ->addArgument($values['password']);
237
                    break;
238
                case 'wsse':
239
                    $container->register($authServiceKey, Wsse::class)
240
                        ->addArgument($values['username'])
241
                        ->addArgument($values['password']);
242
                    break;
243
                case 'service':
244
                    $authServiceKey = $values['service'];
245
                    break;
246
                default:
247
                    throw new \LogicException(sprintf('Unknown authentication type: "%s"', $values['type']));
248
            }
249
250
            $pluginServiceKey = $servicePrefix.'.'.$name;
251
            $container->register($pluginServiceKey, AuthenticationPlugin::class)
252
                ->addArgument(new Reference($authServiceKey))
253
            ;
254
            $pluginServices[] = $pluginServiceKey;
255
        }
256
257
        return $pluginServices;
258
    }
259
260
    /**
261
     * @param ContainerBuilder $container
262
     * @param string           $clientName
263
     * @param array            $arguments
264
     * @param bool             $profiling
265
     */
266
    private function configureClient(ContainerBuilder $container, $clientName, array $arguments, $profiling)
267
    {
268
        $serviceId = 'httplug.client.'.$clientName;
269
270
        $plugins = [];
271
        foreach ($arguments['plugins'] as $plugin) {
272
            list($pluginName, $pluginConfig) = each($plugin);
273
            if ('reference' === $pluginName) {
274
                $plugins[] = $pluginConfig['id'];
275
            } elseif ('authentication' === $pluginName) {
276
                $plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication'));
277
            } else {
278
                $plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig);
279
            }
280
        }
281
282
        $pluginClientOptions = [];
283
        if ($profiling) {
284
            //Decorate each plugin with a ProfilePlugin instance.
285
            foreach ($plugins as $pluginServiceId) {
286
                $this->decoratePluginWithProfilePlugin($container, $pluginServiceId);
287
            }
288
289
            // To profile the requests, add a StackPlugin as first plugin in the chain.
290
            $stackPluginId = $this->configureStackPlugin($container, $clientName, $serviceId);
291
            array_unshift($plugins, $stackPluginId);
292
        }
293
294
        $container
295
            ->register($serviceId, DummyClient::class)
296
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
297
            ->addArgument(
298
                array_map(
299
                    function ($id) {
300
                        return new Reference($id);
301
                    },
302
                    $plugins
303
                )
304
            )
305
            ->addArgument(new Reference($arguments['factory']))
306
            ->addArgument($arguments['config'])
307
            ->addArgument($pluginClientOptions)
308
        ;
309
310
        /*
311
         * Decorate the client with clients from client-common
312
         */
313 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...
314
            $container
315
                ->register($serviceId.'.flexible', FlexibleHttpClient::class)
316
                ->addArgument(new Reference($serviceId.'.flexible.inner'))
317
                ->setPublic(false)
318
                ->setDecoratedService($serviceId)
319
            ;
320
        }
321
322 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...
323
            $container
324
                ->register($serviceId.'.http_methods', HttpMethodsClient::class)
325
                ->setArguments([new Reference($serviceId.'.http_methods.inner'), new Reference('httplug.message_factory')])
326
                ->setPublic(false)
327
                ->setDecoratedService($serviceId)
328
            ;
329
        }
330
331 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...
332
            $container
333
                ->register($serviceId.'.batch_client', BatchClient::class)
334
                ->setArguments([new Reference($serviceId.'.batch_client.inner')])
335
                ->setPublic(false)
336
                ->setDecoratedService($serviceId)
337
            ;
338
        }
339
    }
340
341
    /**
342
     * Create a URI object with the default URI factory.
343
     *
344
     * @param ContainerBuilder $container
345
     * @param string           $serviceId Name of the private service to create
346
     * @param string           $uri       String representation of the URI
347
     */
348
    private function createUri(ContainerBuilder $container, $serviceId, $uri)
349
    {
350
        $container
351
            ->register($serviceId, UriInterface::class)
352
            ->setPublic(false)
353
            ->setFactory([new Reference('httplug.uri_factory'), 'createUri'])
354
            ->addArgument($uri)
355
        ;
356
    }
357
358
    /**
359
     * Make the user can select what client is used for auto discovery. If none is provided, a service will be created
360
     * by finding a client using auto discovery.
361
     *
362
     * @param ContainerBuilder $container
363
     * @param array            $config
364
     */
365
    private function configureAutoDiscoveryClients(ContainerBuilder $container, array $config)
366
    {
367
        $httpClient = $config['discovery']['client'];
368
369 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...
370
            if ($httpClient === 'auto') {
371
                $httpClient = $this->registerAutoDiscoverableClient(
372
                    $container,
373
                    'auto_discovered_client',
374
                    $this->configureAutoDiscoveryFactory(
375
                        $container,
376
                        HttpClientDiscovery::class,
377
                        'auto_discovered_client',
378
                        $config
379
                    ),
380
                    $this->isConfigEnabled($container, $config['profiling'])
381
                );
382
            }
383
384
            $httpClient = new Reference($httpClient);
385
        }
386
387
        $asyncHttpClient = $config['discovery']['async_client'];
388
389 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...
390
            if ($asyncHttpClient === 'auto') {
391
                $asyncHttpClient = $this->registerAutoDiscoverableClient(
392
                    $container,
393
                    'auto_discovered_async',
394
                    $this->configureAutoDiscoveryFactory(
395
                        $container,
396
                        HttpAsyncClientDiscovery::class,
397
                        'auto_discovered_async',
398
                        $config
399
                    ),
400
                    $this->isConfigEnabled($container, $config['profiling'])
401
                );
402
            }
403
404
            $asyncHttpClient = new Reference($asyncHttpClient);
405
        }
406
407
        if (null === $httpClient && null === $asyncHttpClient) {
408
            $container->removeDefinition('httplug.strategy');
409
410
            return;
411
        }
412
413
        $container
414
            ->getDefinition('httplug.strategy')
415
            ->addArgument($httpClient)
416
            ->addArgument($asyncHttpClient)
417
        ;
418
    }
419
420
    /**
421
     * Find a client with auto discovery and return a service Reference to it.
422
     *
423
     * @param ContainerBuilder   $container
424
     * @param string             $name
425
     * @param Reference|callable $factory
426
     * @param bool               $profiling
427
     *
428
     * @return string service id
429
     */
430
    private function registerAutoDiscoverableClient(ContainerBuilder $container, $name, $factory, $profiling)
431
    {
432
        $serviceId = 'httplug.auto_discovery.'.$name;
433
434
        $plugins = [];
435
        if ($profiling) {
436
            // To profile the requests, add a StackPlugin as first plugin in the chain.
437
            $plugins[] = $this->configureStackPlugin($container, $name, $serviceId);
438
        }
439
440
        $container
441
            ->register($serviceId, DummyClient::class)
442
            ->setFactory([PluginClientFactory::class, 'createPluginClient'])
443
            ->setArguments([
444
                array_map(
445
                    function ($id) {
446
                        return new Reference($id);
447
                    },
448
                    $plugins
449
                ),
450
                $factory,
451
                [],
452
            ])
453
        ;
454
455
        if ($profiling) {
456
            $collector = $container->getDefinition('httplug.collector.collector');
457
            $collector->replaceArgument(0, array_merge($collector->getArgument(0), [$name]));
458
        }
459
460
        return $serviceId;
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466
    public function getConfiguration(array $config, ContainerBuilder $container)
467
    {
468
        return new Configuration($container->getParameter('kernel.debug'));
469
    }
470
471
    /**
472
     * Configure a plugin using the parent definition from plugins.xml.
473
     *
474
     * @param ContainerBuilder $container
475
     * @param string           $serviceId
476
     * @param string           $pluginName
477
     * @param array            $pluginConfig
478
     *
479
     * @return string configured service id
480
     */
481
    private function configurePlugin(ContainerBuilder $container, $serviceId, $pluginName, array $pluginConfig)
482
    {
483
        $pluginServiceId = $serviceId.'.plugin.'.$pluginName;
484
485
        $definition = class_exists(ChildDefinition::class)
486
            ? new ChildDefinition('httplug.plugin.'.$pluginName)
487
            : new DefinitionDecorator('httplug.plugin.'.$pluginName);
488
489
        $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
490
        $container->setDefinition($pluginServiceId, $definition);
491
492
        return $pluginServiceId;
493
    }
494
495
    /**
496
     * Decorate the plugin service with a ProfilePlugin service.
497
     *
498
     * @param ContainerBuilder $container
499
     * @param string           $pluginServiceId
500
     */
501
    private function decoratePluginWithProfilePlugin(ContainerBuilder $container, $pluginServiceId)
502
    {
503
        $container->register($pluginServiceId.'.debug', ProfilePlugin::class)
504
            ->setDecoratedService($pluginServiceId)
505
            ->setArguments([
506
                new Reference($pluginServiceId.'.debug.inner'),
507
                new Reference('httplug.collector.collector'),
508
                new Reference('httplug.collector.formatter'),
509
                $pluginServiceId,
510
            ])
511
            ->setPublic(false);
512
    }
513
514
    /**
515
     * Configure a StackPlugin for a client.
516
     *
517
     * @param ContainerBuilder $container
518
     * @param string           $clientName Client name to display in the profiler.
519
     * @param string           $serviceId  Client service id. Used as base for the StackPlugin service id.
520
     *
521
     * @return string configured StackPlugin service id
522
     */
523
    private function configureStackPlugin(ContainerBuilder $container, $clientName, $serviceId)
524
    {
525
        $pluginServiceId = $serviceId.'.plugin.stack';
526
527
        $definition = class_exists(ChildDefinition::class)
528
            ? new ChildDefinition('httplug.plugin.stack')
529
            : new DefinitionDecorator('httplug.plugin.stack');
530
531
        $definition->addArgument($clientName);
532
        $container->setDefinition($pluginServiceId, $definition);
533
534
        return $pluginServiceId;
535
    }
536
537
    /**
538
     * Configure the discovery factory when profiling is enabled to get client decorated with a ProfileClient.
539
     *
540
     * @param ContainerBuilder $container
541
     * @param string           $discovery
542
     * @param string           $name
543
     * @param array            $config
544
     *
545
     * @return callable|Reference
546
     */
547
    private function configureAutoDiscoveryFactory(ContainerBuilder $container, $discovery, $name, array $config)
548
    {
549
        $factory = [$discovery, 'find'];
550
        if ($this->isConfigEnabled($container, $config['profiling'])) {
551
            $factoryServiceId = 'httplug.auto_discovery.'.$name.'.factory';
552
            $container->register($factoryServiceId, ProfileClientFactory::class)
553
                ->setPublic(false)
554
                ->setArguments([
555
                    $factory,
556
                    new Reference('httplug.collector.collector'),
557
                    new Reference('httplug.collector.formatter'),
558
                    new Reference('debug.stopwatch'),
559
                ]);
560
            $factory = new Reference($factoryServiceId);
561
        }
562
563
        return $factory;
564
    }
565
}
566