Completed
Pull Request — master (#112)
by David
20:44 queued 18:33
created

Configuration   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 99.41%

Importance

Changes 15
Bugs 1 Features 6
Metric Value
wmc 33
c 15
b 1
f 6
lcom 1
cbo 5
dl 0
loc 454
ccs 336
cts 338
cp 0.9941
rs 9.3999

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C getConfigTreeBuilder() 0 111 12
B configureClients() 0 43 2
A configurePlugins() 0 9 1
B createExtraPluginsNode() 0 41 4
B configureSharedPluginNodes() 0 133 6
B createAuthenticationPluginNode() 0 44 5
A validateAuthenticationType() 0 18 2
1
<?php
2
3
namespace Http\HttplugBundle\DependencyInjection;
4
5
use Symfony\Component\Config\Definition\ArrayNode;
6
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
7
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
8
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
9
use Symfony\Component\Config\Definition\ConfigurationInterface;
10
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
11
12
/**
13
 * This class contains the configuration information for the bundle.
14
 *
15
 * This information is solely responsible for how the different configuration
16
 * sections are normalized, and merged.
17
 *
18
 * @author David Buchmann <[email protected]>
19
 * @author Tobias Nyholm <[email protected]>
20
 */
21
class Configuration implements ConfigurationInterface
22
{
23
    /**
24
     * Whether to use the debug mode.
25
     *
26
     * @see https://github.com/doctrine/DoctrineBundle/blob/v1.5.2/DependencyInjection/Configuration.php#L31-L41
27
     *
28
     * @var bool
29
     */
30
    private $debug;
31
32
    /**
33
     * @param bool $debug
34
     */
35 15
    public function __construct($debug)
36
    {
37 15
        $this->debug = (bool) $debug;
38 15
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43 15
    public function getConfigTreeBuilder()
44
    {
45 15
        $treeBuilder = new TreeBuilder();
46 15
        $rootNode = $treeBuilder->root('httplug');
47
48 15
        $this->configureClients($rootNode);
49 15
        $this->configurePlugins($rootNode);
50
51
        $rootNode
52 15
            ->validate()
53
                ->ifTrue(function ($v) {
54 13
                    return !empty($v['classes']['client'])
55 13
                        || !empty($v['classes']['message_factory'])
56 10
                        || !empty($v['classes']['uri_factory'])
57 13
                        || !empty($v['classes']['stream_factory']);
58 15
                })
59
                ->then(function ($v) {
60 3
                    foreach ($v['classes'] as $key => $class) {
61 3
                        if (null !== $class && !class_exists($class)) {
62 1
                            throw new InvalidConfigurationException(sprintf(
63 1
                                'Class %s specified for httplug.classes.%s does not exist.',
64 1
                                $class,
65
                                $key
66 1
                            ));
67
                        }
68 2
                    }
69
70 2
                    return $v;
71 15
                })
72 15
            ->end()
73 15
            ->beforeNormalization()
74
                ->ifTrue(function ($v) {
75 15
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
76 15
                })
77
                ->then(function ($v) {
78 4
                    if (array_key_exists('profiling', $v)) {
79 1
                        throw new InvalidConfigurationException('Can\'t configure both "toolbar" and "profiling" section. The "toolbar" config is deprecated as of version 1.3.0, please only use "profiling".');
80
                    }
81
82 3
                    @trigger_error('"httplug.toolbar" config is deprecated since version 1.3 and will be removed in 2.0. Use "httplug.profiling" instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
83
84 3
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
85 1
                        @trigger_error('"auto" value in "httplug.toolbar" config is deprecated since version 1.3 and will be removed in 2.0. Use a boolean value instead.', E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
86 1
                        $v['toolbar']['enabled'] = $this->debug;
87 1
                    }
88
89 3
                    $v['profiling'] = $v['toolbar'];
90
91 3
                    unset($v['toolbar']);
92
93 3
                    return $v;
94 15
                })
95 15
            ->end()
96 15
            ->fixXmlConfig('client')
97 15
            ->children()
98 15
                ->arrayNode('main_alias')
99 15
                    ->addDefaultsIfNotSet()
100 15
                    ->info('Configure which service the main alias point to.')
101 15
                    ->children()
102 15
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
103 15
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
104 15
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
105 15
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
106 15
                    ->end()
107 15
                ->end()
108 15
                ->arrayNode('classes')
109 15
                    ->addDefaultsIfNotSet()
110 15
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
111 15
                    ->children()
112 15
                        ->scalarNode('client')->defaultNull()->end()
113 15
                        ->scalarNode('message_factory')->defaultNull()->end()
114 15
                        ->scalarNode('uri_factory')->defaultNull()->end()
115 15
                        ->scalarNode('stream_factory')->defaultNull()->end()
116 15
                    ->end()
117 15
                ->end()
118 15
                ->arrayNode('profiling')
119 15
                    ->addDefaultsIfNotSet()
120 15
                    ->treatFalseLike(['enabled' => false])
121 15
                    ->treatTrueLike(['enabled' => true])
122 15
                    ->treatNullLike(['enabled' => $this->debug])
123 15
                    ->info('Extend the debug profiler with information about requests.')
124 15
                    ->children()
125 15
                        ->booleanNode('enabled')
126 15
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
127 15
                            ->defaultValue($this->debug)
128 15
                        ->end()
129 15
                        ->scalarNode('formatter')->defaultNull()->end()
130 15
                        ->integerNode('captured_body_length')
131 15
                            ->defaultValue(0)
132 15
                            ->info('Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. Only available with the default formatter (FullHttpMessageFormatter).')
133 15
                        ->end()
134 15
                    ->end()
135 15
                ->end()
136 15
                ->arrayNode('discovery')
137 15
                    ->addDefaultsIfNotSet()
138 15
                    ->info('Control what clients should be found by the discovery.')
139 15
                    ->children()
140 15
                        ->scalarNode('client')
141 15
                            ->defaultValue('auto')
142 15
                            ->info('Set to "auto" to see auto discovered client in the web profiler. If provided a service id for a client then this client will be found by auto discovery.')
143 15
                        ->end()
144 15
                        ->scalarNode('async_client')
145 15
                            ->defaultNull()
146 15
                            ->info('Set to "auto" to see auto discovered client in the web profiler. If provided a service id for a client then this client will be found by auto discovery.')
147 15
                        ->end()
148 15
                    ->end()
149 15
                ->end()
150 15
            ->end();
151
152 15
        return $treeBuilder;
153
    }
154
155 15
    private function configureClients(ArrayNodeDefinition $root)
156
    {
157 15
        $root->children()
158 15
            ->arrayNode('clients')
159 15
                ->validate()
160
                    ->ifTrue(function ($clients) {
161 7
                        foreach ($clients as $name => $config) {
162
                            // Make sure we only allow one of these to be true
163 7
                            return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
164
                        }
165
166
                        return false;
167 15
                    })
168 15
                    ->thenInvalid('A http client can\'t be decorated with both FlexibleHttpClient and HttpMethodsClient. Only one of the following options can be true. ("flexible_client", "http_methods_client")')->end()
169 15
                ->useAttributeAsKey('name')
170 15
                ->prototype('array')
171 15
                ->children()
172 15
                    ->scalarNode('factory')
173 15
                        ->isRequired()
174 15
                        ->cannotBeEmpty()
175 15
                        ->info('The service id of a factory to use when creating the adapter.')
176 15
                    ->end()
177 15
                    ->booleanNode('flexible_client')
178 15
                        ->defaultFalse()
179 15
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
180 15
                    ->end()
181 15
                    ->booleanNode('http_methods_client')
182 15
                        ->defaultFalse()
183 15
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
184 15
                    ->end()
185 15
                    ->booleanNode('batch_client')
186 15
                        ->defaultFalse()
187 15
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
188 15
                    ->end()
189 15
                    ->arrayNode('plugins')
190 15
                        ->info('A list of service ids of plugins. The order is important.')
191 15
                        ->prototype('scalar')->end()
192 15
                    ->end()
193 15
                    ->variableNode('config')->defaultValue([])->end()
194 15
                    ->append($this->createExtraPluginsNode())
195 15
                ->end()
196 15
            ->end();
197 15
    }
198
199
    /**
200
     * @param ArrayNodeDefinition $root
201
     */
202 15
    private function configurePlugins(ArrayNodeDefinition $root)
203
    {
204
        $pluginsNode = $root
205 15
            ->children()
206 15
                ->arrayNode('plugins')
207 15
                ->addDefaultsIfNotSet()
208 15
        ;
209 15
        $this->configureSharedPluginNodes($pluginsNode);
210 15
    }
211
212
    /**
213
     * Create configuration for the extra_plugins node inside the client.
214
     *
215
     * @return NodeDefinition Definition of the extra_plugins node in the client.
216
     */
217 15
    private function createExtraPluginsNode()
218
    {
219 15
        $builder = new TreeBuilder();
220 15
        $node = $builder->root('extra_plugins');
221 15
        $node->validate()
222
            ->always(function ($plugins) {
223 4
                if (!count($plugins['authentication'])) {
224 4
                    unset($plugins['authentication']);
225 4
                }
226 4
                foreach ($plugins as $name => $definition) {
227 4
                    if (!$definition['enabled']) {
228 4
                        unset($plugins[$name]);
229 4
                    }
230 4
                }
231 4
                return $plugins;
232 15
            })
233
        ;
234 15
        $this->configureSharedPluginNodes($node, true);
235
        $node
236 15
            ->children()
237 15
                ->arrayNode('add_host')
238 15
                    ->canBeEnabled()
239 15
                    ->addDefaultsIfNotSet()
240 15
                    ->info('Configure the AddHostPlugin for this client.')
241 15
                    ->children()
242 15
                        ->scalarNode('host')
243 15
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
244 15
                            ->isRequired()
245 15
                            ->cannotBeEmpty()
246 15
                        ->end()
247 15
                        ->scalarNode('replace')
248 15
                            ->info('Whether to replace the host if request already specifies it')
249 15
                            ->defaultValue(false)
250 15
                        ->end()
251 15
                    ->end()
252 15
                ->end()
253 15
            ->end()
254 15
        ->end();
255
256 15
        return $node;
257
    }
258
259
    /**
260
     * @param ArrayNodeDefinition $pluginNode
261
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
262
     */
263 15
    private function configureSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
264
    {
265 15
        $children = $pluginNode->children();
266
267 15
        $children->append($this->createAuthenticationPluginNode());
268
269 15
        $children->arrayNode('cache')
270 15
            ->canBeEnabled()
271 15
            ->addDefaultsIfNotSet()
272 15
                ->children()
273 15
                    ->scalarNode('cache_pool')
274 15
                        ->info('This must be a service id to a service implementing Psr\Cache\CacheItemPoolInterface')
275 15
                        ->isRequired()
276 15
                        ->cannotBeEmpty()
277 15
                    ->end()
278 15
                    ->scalarNode('stream_factory')
279 15
                        ->info('This must be a service id to a service implementing Http\Message\StreamFactory')
280 15
                        ->defaultValue('httplug.stream_factory')
281 15
                        ->cannotBeEmpty()
282 15
                    ->end()
283 15
                    ->arrayNode('config')
284 15
                        ->addDefaultsIfNotSet()
285 15
                        ->children()
286 15
                            ->scalarNode('default_ttl')->defaultNull()->end()
287 15
                            ->scalarNode('respect_cache_headers')->defaultTrue()->end()
288 15
                        ->end()
289 15
                    ->end()
290 15
                ->end()
291 15
            ->end();
292
        // End cache plugin
293
294 15
        $children->arrayNode('cookie')
295 15
            ->canBeEnabled()
296 15
                ->children()
297 15
                    ->scalarNode('cookie_jar')
298 15
                        ->info('This must be a service id to a service implementing Http\Message\CookieJar')
299 15
                        ->isRequired()
300 15
                        ->cannotBeEmpty()
301 15
                    ->end()
302 15
                ->end()
303 15
            ->end();
304
        // End cookie plugin
305
306 15
        $decoder = $children->arrayNode('decoder');
307 15
        if ($disableAll) {
308 15
            $decoder->canBeEnabled();
309 15
        } else {
310 15
            $decoder->canBeDisabled();
311
        }
312 15
        $decoder->addDefaultsIfNotSet()
313 15
            ->children()
314 15
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
315 15
            ->end()
316 15
        ->end();
317
        // End decoder plugin
318
319 15
        $children->arrayNode('history')
320 15
            ->canBeEnabled()
321 15
                ->children()
322 15
                    ->scalarNode('journal')
323 15
                        ->info('This must be a service id to a service implementing Http\Client\Plugin\Journal')
324 15
                        ->isRequired()
325 15
                        ->cannotBeEmpty()
326 15
                    ->end()
327 15
                ->end()
328 15
            ->end();
329
        // End history plugin
330
331 15
        $logger = $children->arrayNode('logger');
332 15
        if ($disableAll) {
333 15
            $logger->canBeEnabled();
334 15
        } else {
335 15
            $logger->canBeDisabled();
336
        }
337 15
        $logger->addDefaultsIfNotSet()
338 15
            ->children()
339 15
                ->scalarNode('logger')
340 15
                    ->info('This must be a service id to a service implementing Psr\Log\LoggerInterface')
341 15
                    ->defaultValue('logger')
342 15
                    ->cannotBeEmpty()
343 15
                ->end()
344 15
                ->scalarNode('formatter')
345 15
                    ->info('This must be a service id to a service implementing Http\Message\Formatter')
346 15
                    ->defaultNull()
347 15
                ->end()
348 15
            ->end()
349 15
        ->end();
350
        // End logger plugin
351
352 15
        $redirect = $children->arrayNode('redirect');
353 15
        if ($disableAll) {
354 15
            $redirect->canBeEnabled();
355 15
        } else {
356 15
            $redirect->canBeDisabled();
357
        }
358 15
        $redirect->addDefaultsIfNotSet()
359 15
            ->children()
360 15
                ->scalarNode('preserve_header')->defaultTrue()->end()
361 15
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
362 15
            ->end()
363 15
        ->end();
364
        // End redirect plugin
365
366 15
        $retry = $children->arrayNode('retry');
367 15
        if ($disableAll) {
368 15
            $retry->canBeEnabled();
369 15
        } else {
370 15
            $retry->canBeDisabled();
371
        }
372 15
        $retry->addDefaultsIfNotSet()
373 15
            ->children()
374 15
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
375 15
            ->end()
376 15
        ->end();
377
        // End retry plugin
378
379 15
        $stopwatch = $children->arrayNode('stopwatch');
380 15
        if ($disableAll) {
381 15
            $stopwatch->canBeEnabled();
382 15
        } else {
383 15
            $stopwatch->canBeDisabled();
384
        }
385 15
        $stopwatch->addDefaultsIfNotSet()
386 15
            ->children()
387 15
                ->scalarNode('stopwatch')
388 15
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
389 15
                    ->defaultValue('debug.stopwatch')
390 15
                    ->cannotBeEmpty()
391 15
                ->end()
392 15
            ->end()
393 15
        ->end();
394
        // End stopwatch plugin
395 15
    }
396
397
    /**
398
     * Create configuration for authentication plugin.
399
     *
400
     * @return NodeDefinition Definition for the authentication node in the plugins list.
401
     */
402 15
    private function createAuthenticationPluginNode()
403
    {
404 15
        $builder = new TreeBuilder();
405 15
        $node = $builder->root('authentication');
406
        $node
407 15
            ->useAttributeAsKey('name')
408 15
            ->prototype('array')
409 15
                ->validate()
410 15
                    ->always()
411 15
                    ->then(function ($config) {
412 2
                        switch ($config['type']) {
413 2
                            case 'basic':
414 1
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
415 1
                                break;
416 2
                            case 'bearer':
417 1
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
418 1
                                break;
419 2
                            case 'service':
420 2
                                $this->validateAuthenticationType(['service'], $config, 'service');
421 1
                                break;
422 1
                            case 'wsse':
423 1
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
424 1
                                break;
425 1
                        }
426
427 1
                        return $config;
428 15
                    })
429 15
                ->end()
430 15
                ->children()
431 15
                    ->enumNode('type')
432 15
                        ->values(['basic', 'bearer', 'wsse', 'service'])
433 15
                        ->isRequired()
434 15
                        ->cannotBeEmpty()
435 15
                    ->end()
436 15
                    ->scalarNode('username')->end()
437 15
                    ->scalarNode('password')->end()
438 15
                    ->scalarNode('token')->end()
439 15
                    ->scalarNode('service')->end()
440 15
                    ->end()
441 15
                ->end()
442 15
            ->end(); // End authentication plugin
443
444 15
        return $node;
445
    }
446
447
    /**
448
     * Validate that the configuration fragment has the specified keys and none other.
449
     *
450
     * @param array  $expected Fields that must exist
451
     * @param array  $actual   Actual configuration hashmap
452
     * @param string $authName Name of authentication method for error messages
453
     *
454
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
455
     */
456 2
    private function validateAuthenticationType(array $expected, array $actual, $authName)
457
    {
458 2
        unset($actual['type']);
459 2
        $actual = array_keys($actual);
460 2
        sort($actual);
461 2
        sort($expected);
462
463 2
        if ($expected === $actual) {
464 1
            return;
465
        }
466
467 1
        throw new InvalidConfigurationException(sprintf(
468 1
            'Authentication "%s" requires %s but got %s',
469 1
            $authName,
470 1
            implode(', ', $expected),
471 1
            implode(', ', $actual)
472 1
        ));
473
    }
474
}
475