Completed
Push — master ( 78a0a4...eabb68 )
by David
02:07 queued 18s
created

Configuration::createCachePluginNode()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 135

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 105
CRAP Score 6.0007

Importance

Changes 0
Metric Value
dl 0
loc 135
ccs 105
cts 108
cp 0.9722
rs 7.3777
c 0
b 0
f 0
cc 6
nc 4
nop 0
crap 6.0007

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Http\HttplugBundle\DependencyInjection;
6
7
use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator;
8
use Http\Client\Common\Plugin\Cache\Listener\CacheListener;
9
use Http\Client\Common\Plugin\CachePlugin;
10
use Http\Client\Common\Plugin\Journal;
11
use Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface;
12
use Http\Client\Plugin\Vcr\Recorder\PlayerInterface;
13
use Http\Client\Plugin\Vcr\Recorder\RecorderInterface;
14
use Http\Message\CookieJar;
15
use Http\Message\Formatter;
16
use Http\Message\StreamFactory;
17
use Psr\Cache\CacheItemPoolInterface;
18
use Psr\Log\LoggerInterface;
19
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
20
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
21
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
22
use Symfony\Component\Config\Definition\ConfigurationInterface;
23
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
24
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
25
26
/**
27
 * This class contains the configuration information for the bundle.
28
 *
29
 * This information is solely responsible for how the different configuration
30
 * sections are normalized, and merged.
31
 *
32
 * @author David Buchmann <[email protected]>
33
 * @author Tobias Nyholm <[email protected]>
34
 */
35
class Configuration implements ConfigurationInterface
36
{
37
    /**
38
     * Whether to use the debug mode.
39
     *
40
     * @see https://github.com/doctrine/DoctrineBundle/blob/v1.5.2/DependencyInjection/Configuration.php#L31-L41
41
     *
42
     * @var bool
43
     */
44
    private $debug;
45
46
    /**
47
     * @param bool $debug
48
     */
49 51
    public function __construct($debug)
50
    {
51 51
        $this->debug = (bool) $debug;
52 51
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 51
    public function getConfigTreeBuilder()
58
    {
59 51
        $treeBuilder = new TreeBuilder('httplug');
60
        // Keep compatibility with symfony/config < 4.2
61 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
62
            $rootNode = $treeBuilder->root('httplug');
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
63
        } else {
64 51
            $rootNode = $treeBuilder->getRootNode();
65
        }
66
67 51
        $this->configureClients($rootNode);
68 51
        $this->configureSharedPlugins($rootNode);
69
70
        $rootNode
71 51
            ->validate()
72
                ->ifTrue(function ($v) {
73 44
                    return !empty($v['classes']['client'])
74 41
                        || !empty($v['classes']['message_factory'])
75 41
                        || !empty($v['classes']['uri_factory'])
76 44
                        || !empty($v['classes']['stream_factory']);
77 51
                })
78
                ->then(function ($v) {
79 3
                    foreach ($v['classes'] as $key => $class) {
80 3
                        if (null !== $class && !class_exists($class)) {
81 1
                            throw new InvalidConfigurationException(sprintf(
82 1
                                'Class %s specified for httplug.classes.%s does not exist.',
83
                                $class,
84
                                $key
85
                            ));
86
                        }
87
                    }
88
89 2
                    return $v;
90 51
                })
91 51
            ->end()
92 51
            ->beforeNormalization()
93
                ->ifTrue(function ($v) {
94 51
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
95 51
                })
96
                ->then(function ($v) {
97 4
                    if (array_key_exists('profiling', $v)) {
98 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".');
99
                    }
100
101 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);
102
103 3
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
104 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);
105 1
                        $v['toolbar']['enabled'] = $this->debug;
106
                    }
107
108 3
                    $v['profiling'] = $v['toolbar'];
109
110 3
                    unset($v['toolbar']);
111
112 3
                    return $v;
113 51
                })
114 51
            ->end()
115 51
            ->fixXmlConfig('client')
116 51
            ->children()
117 51
                ->booleanNode('default_client_autowiring')
118 51
                    ->defaultTrue()
119 51
                    ->info('Set to false to not autowire HttpClient and HttpAsyncClient.')
120 51
                ->end()
121 51
                ->arrayNode('main_alias')
122 51
                    ->addDefaultsIfNotSet()
123 51
                    ->info('Configure which service the main alias point to.')
124 51
                    ->children()
125 51
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
126 51
                        ->scalarNode('psr18_client')->defaultValue('httplug.psr18_client.default')->end()
127 51
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
128 51
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
129 51
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
130 51
                        ->scalarNode('psr17_request_factory')->defaultValue('httplug.psr17_request_factory.default')->end()
131 51
                        ->scalarNode('psr17_response_factory')->defaultValue('httplug.psr17_response_factory.default')->end()
132 51
                        ->scalarNode('psr17_stream_factory')->defaultValue('httplug.psr17_stream_factory.default')->end()
133 51
                        ->scalarNode('psr17_uri_factory')->defaultValue('httplug.psr17_uri_factory.default')->end()
134 51
                        ->scalarNode('psr17_uploaded_file_factory')->defaultValue('httplug.psr17_uploaded_file_factory.default')->end()
135 51
                        ->scalarNode('psr17_server_request_factory')->defaultValue('httplug.psr17_server_request_factory.default')->end()
136 51
                    ->end()
137 51
                ->end()
138 51
                ->arrayNode('classes')
139 51
                    ->addDefaultsIfNotSet()
140 51
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
141 51
                    ->children()
142 51
                        ->scalarNode('client')->defaultNull()->end()
143 51
                        ->scalarNode('psr18_client')->defaultNull()->end()
144 51
                        ->scalarNode('message_factory')->defaultNull()->end()
145 51
                        ->scalarNode('uri_factory')->defaultNull()->end()
146 51
                        ->scalarNode('stream_factory')->defaultNull()->end()
147 51
                        ->scalarNode('psr17_request_factory')->defaultNull()->end()
148 51
                        ->scalarNode('psr17_response_factory')->defaultNull()->end()
149 51
                        ->scalarNode('psr17_stream_factory')->defaultNull()->end()
150 51
                        ->scalarNode('psr17_uri_factory')->defaultNull()->end()
151 51
                        ->scalarNode('psr17_uploaded_file_factory')->defaultNull()->end()
152 51
                        ->scalarNode('psr17_server_request_factory')->defaultNull()->end()
153 51
                    ->end()
154 51
                ->end()
155 51
                ->arrayNode('profiling')
156 51
                    ->addDefaultsIfNotSet()
157 51
                    ->treatFalseLike(['enabled' => false])
158 51
                    ->treatTrueLike(['enabled' => true])
159 51
                    ->treatNullLike(['enabled' => $this->debug])
160 51
                    ->info('Extend the debug profiler with information about requests.')
161 51
                    ->children()
162 51
                        ->booleanNode('enabled')
163 51
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
164 51
                            ->defaultValue($this->debug)
165 51
                        ->end()
166 51
                        ->scalarNode('formatter')->defaultNull()->end()
167 51
                        ->scalarNode('captured_body_length')
168 51
                            ->validate()
169
                                ->ifTrue(function ($v) {
170 4
                                    return null !== $v && !is_int($v);
171 51
                                })
172 51
                                ->thenInvalid('The child node "captured_body_length" at path "httplug.profiling" must be an integer or null ("%s" given).')
173 51
                            ->end()
174 51
                            ->defaultValue(0)
175 51
                            ->info('Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. If null the body will not be truncated. Only available with the default formatter (FullHttpMessageFormatter).')
176 51
                        ->end()
177 51
                    ->end()
178 51
                ->end()
179 51
                ->arrayNode('discovery')
180 51
                    ->addDefaultsIfNotSet()
181 51
                    ->info('Control what clients should be found by the discovery.')
182 51
                    ->children()
183 51
                        ->scalarNode('client')
184 51
                            ->defaultValue('auto')
185 51
                            ->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.')
186 51
                        ->end()
187 51
                        ->scalarNode('async_client')
188 51
                            ->defaultNull()
189 51
                            ->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.')
190 51
                        ->end()
191 51
                    ->end()
192 51
                ->end()
193 51
            ->end();
194
195 51
        return $treeBuilder;
196
    }
197
198 51
    private function configureClients(ArrayNodeDefinition $root)
199
    {
200 51
        $root->children()
201 51
            ->arrayNode('clients')
202 51
                ->useAttributeAsKey('name')
203 51
                ->prototype('array')
204 51
                ->fixXmlConfig('plugin')
205 51
                ->validate()
206
                    ->ifTrue(function ($config) {
207
                        // Make sure we only allow one of these to be true
208 27
                        return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
209 51
                    })
210 51
                    ->thenInvalid('A http client can\'t be decorated with several of FlexibleHttpClient, HttpMethodsClient and BatchClient. Only one of the following options can be true. ("flexible_client", "http_methods_client", "batch_client")')
211 51
                ->end()
212 51
                ->validate()
213
                    ->ifTrue(function ($config) {
214 27
                        return 'httplug.factory.auto' === $config['factory'] && !empty($config['config']);
215 51
                    })
216 51
                    ->thenInvalid('If you want to use the "config" key you must also specify a valid "factory".')
217 51
                ->end()
218 51
                ->validate()
219
                    ->ifTrue(function ($config) {
220 27
                        return !empty($config['service']) && ('httplug.factory.auto' !== $config['factory'] || !empty($config['config']));
221 51
                    })
222 51
                    ->thenInvalid('If you want to use the "service" key you cannot specify "factory" or "config".')
223 51
                ->end()
224 51
                ->children()
225 51
                    ->scalarNode('factory')
226 51
                        ->defaultValue('httplug.factory.auto')
227 51
                        ->cannotBeEmpty()
228 51
                        ->info('The service id of a factory to use when creating the adapter.')
229 51
                    ->end()
230 51
                    ->scalarNode('service')
231 51
                        ->defaultNull()
232 51
                        ->info('The service id of the client to use.')
233 51
                    ->end()
234 51
                    ->booleanNode('public')
235 51
                        ->defaultNull()
236 51
                        ->info('Set to true if you really cannot use dependency injection and need to make the client service public.')
237 51
                    ->end()
238 51
                    ->booleanNode('flexible_client')
239 51
                        ->defaultFalse()
240 51
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
241 51
                    ->end()
242 51
                    ->booleanNode('http_methods_client')
243 51
                        ->defaultFalse()
244 51
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
245 51
                    ->end()
246 51
                    ->booleanNode('batch_client')
247 51
                        ->defaultFalse()
248 51
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
249 51
                    ->end()
250 51
                    ->variableNode('config')->defaultValue([])->end()
251 51
                    ->append($this->createClientPluginNode())
252 51
                ->end()
253 51
            ->end()
254 51
        ->end();
255 51
    }
256
257
    /**
258
     * @param ArrayNodeDefinition $root
259
     */
260 51
    private function configureSharedPlugins(ArrayNodeDefinition $root)
261
    {
262
        $pluginsNode = $root
263 51
            ->children()
264 51
                ->arrayNode('plugins')
265 51
                ->info('Global plugin configuration. Plugins need to be explicitly added to clients.')
266 51
                ->addDefaultsIfNotSet()
267
            // don't call end to get the plugins node
268
        ;
269 51
        $this->addSharedPluginNodes($pluginsNode);
270 51
    }
271
272
    /**
273
     * Createplugins node of a client.
274
     *
275
     * @return ArrayNodeDefinition The plugin node
276
     */
277 51
    private function createClientPluginNode()
278
    {
279 51
        $treeBuilder = new TreeBuilder('plugins');
280
        // Keep compatibility with symfony/config < 4.2
281 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
282
            $node = $treeBuilder->root('plugins');
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
283
        } else {
284 51
            $node = $treeBuilder->getRootNode();
285
        }
286
287
        /** @var ArrayNodeDefinition $pluginList */
288
        $pluginList = $node
289 51
            ->info('A list of plugin service ids and client specific plugin definitions. The order is important.')
290 51
            ->prototype('array')
291
        ;
292
        $pluginList
293
            // support having just a service id in the list
294 51
            ->beforeNormalization()
295
                ->always(function ($plugin) {
296 18
                    if (is_string($plugin)) {
297
                        return [
298
                            'reference' => [
299 10
                                'enabled' => true,
300 10
                                'id' => $plugin,
301
                            ],
302
                        ];
303
                    }
304
305 15
                    return $plugin;
306 51
                })
307 51
            ->end()
308
309 51
            ->validate()
310
                ->always(function ($plugins) {
311 16
                    foreach ($plugins as $name => $definition) {
312 16
                        if ('authentication' === $name) {
313 16
                            if (!count($definition)) {
314 16
                                unset($plugins['authentication']);
315
                            }
316 16
                        } elseif (!$definition['enabled']) {
317 16
                            unset($plugins[$name]);
318
                        }
319
                    }
320
321 16
                    return $plugins;
322 51
                })
323 51
            ->end()
324
        ;
325 51
        $this->addSharedPluginNodes($pluginList, true);
326
327
        $pluginList
328 51
            ->children()
329 51
                ->arrayNode('reference')
330 51
                    ->canBeEnabled()
331 51
                    ->info('Reference to a plugin service')
332 51
                    ->children()
333 51
                        ->scalarNode('id')
334 51
                            ->info('Service id of a plugin')
335 51
                            ->isRequired()
336 51
                            ->cannotBeEmpty()
337 51
                        ->end()
338 51
                    ->end()
339 51
                ->end()
340 51
                ->arrayNode('add_host')
341 51
                    ->canBeEnabled()
342 51
                    ->addDefaultsIfNotSet()
343 51
                    ->info('Set scheme, host and port in the request URI.')
344 51
                    ->children()
345 51
                        ->scalarNode('host')
346 51
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
347 51
                            ->isRequired()
348 51
                            ->cannotBeEmpty()
349 51
                        ->end()
350 51
                        ->scalarNode('replace')
351 51
                            ->info('Whether to replace the host if request already specifies one')
352 51
                            ->defaultValue(false)
353 51
                        ->end()
354 51
                    ->end()
355 51
                ->end()
356 51
                ->arrayNode('add_path')
357 51
                    ->canBeEnabled()
358 51
                    ->addDefaultsIfNotSet()
359 51
                    ->info('Add a base path to the request.')
360 51
                    ->children()
361 51
                        ->scalarNode('path')
362 51
                            ->info('Path to be added, e.g. /api/v1')
363 51
                            ->isRequired()
364 51
                            ->cannotBeEmpty()
365 51
                        ->end()
366 51
                    ->end()
367 51
                ->end()
368 51
                ->arrayNode('base_uri')
369 51
                    ->canBeEnabled()
370 51
                    ->addDefaultsIfNotSet()
371 51
                    ->info('Set a base URI to the request.')
372 51
                    ->children()
373 51
                        ->scalarNode('uri')
374 51
                            ->info('Base Uri including protocol, optionally the port number and prepend path, e.g. https://api.local:8000/api')
375 51
                            ->isRequired()
376 51
                            ->cannotBeEmpty()
377 51
                        ->end()
378 51
                        ->scalarNode('replace')
379 51
                            ->info('Whether to replace the host if request already specifies one')
380 51
                            ->defaultValue(false)
381 51
                        ->end()
382 51
                    ->end()
383 51
                ->end()
384 51
                ->arrayNode('content_type')
385 51
                    ->canBeEnabled()
386 51
                    ->info('Detect the content type of a request body and set the Content-Type header if it is not already set.')
387 51
                    ->children()
388 51
                        ->booleanNode('skip_detection')
389 51
                            ->info('Whether to skip detection when request body is larger than size_limit')
390 51
                            ->defaultFalse()
391 51
                        ->end()
392 51
                        ->scalarNode('size_limit')
393 51
                            ->info('Skip content type detection if request body is larger than size_limit bytes')
394 51
                        ->end()
395 51
                    ->end()
396 51
                ->end()
397 51
                ->arrayNode('header_append')
398 51
                    ->canBeEnabled()
399 51
                    ->info('Append headers to the request. If the header already exists the value will be appended to the current value.')
400 51
                    ->fixXmlConfig('header')
401 51
                    ->children()
402 51
                        ->arrayNode('headers')
403 51
                            ->info('Keys are the header names, values the header values')
404 51
                            ->normalizeKeys(false)
405 51
                            ->useAttributeAsKey('name')
406 51
                            ->prototype('scalar')->end()
407 51
                        ->end()
408 51
                    ->end()
409 51
                ->end()
410 51
                ->arrayNode('header_defaults')
411 51
                    ->canBeEnabled()
412 51
                    ->info('Set header to default value if it does not exist.')
413 51
                    ->fixXmlConfig('header')
414 51
                    ->children()
415 51
                        ->arrayNode('headers')
416 51
                            ->info('Keys are the header names, values the header values')
417 51
                            ->normalizeKeys(false)
418 51
                            ->useAttributeAsKey('name')
419 51
                            ->prototype('scalar')->end()
420 51
                        ->end()
421 51
                    ->end()
422 51
                ->end()
423 51
                ->arrayNode('header_set')
424 51
                    ->canBeEnabled()
425 51
                    ->info('Set headers to requests. If the header does not exist it wil be set, if the header already exists it will be replaced.')
426 51
                    ->fixXmlConfig('header')
427 51
                    ->children()
428 51
                        ->arrayNode('headers')
429 51
                            ->info('Keys are the header names, values the header values')
430 51
                            ->normalizeKeys(false)
431 51
                            ->useAttributeAsKey('name')
432 51
                            ->prototype('scalar')->end()
433 51
                        ->end()
434 51
                    ->end()
435 51
                ->end()
436 51
                ->arrayNode('header_remove')
437 51
                    ->canBeEnabled()
438 51
                    ->info('Remove headers from requests.')
439 51
                    ->fixXmlConfig('header')
440 51
                    ->children()
441 51
                        ->arrayNode('headers')
442 51
                            ->info('List of header names to remove')
443 51
                            ->prototype('scalar')->end()
444 51
                        ->end()
445 51
                    ->end()
446 51
                ->end()
447 51
                ->arrayNode('query_defaults')
448 51
                    ->canBeEnabled()
449 51
                    ->info('Sets query parameters to default value if they are not present in the request.')
450 51
                    ->fixXmlConfig('parameter')
451 51
                    ->children()
452 51
                        ->arrayNode('parameters')
453 51
                            ->info('List of query parameters. Names and values must not be url encoded as the plugin will encode them.')
454 51
                            ->normalizeKeys(false)
455 51
                            ->useAttributeAsKey('name')
456 51
                            ->prototype('scalar')->end()
457 51
                        ->end()
458 51
                    ->end()
459 51
                ->end()
460 51
                ->arrayNode('vcr')
461 51
                    ->canBeEnabled()
462 51
                    ->addDefaultsIfNotSet()
463 51
                    ->info('Record response to be replayed during tests or development cycle.')
464 51
                    ->validate()
465
                        ->ifTrue(function ($config) {
466 5
                            return 'filesystem' === $config['recorder'] && empty($config['fixtures_directory']);
467 51
                        })
468 51
                        ->thenInvalid('If you want to use the "filesystem" recorder you must also specify a "fixtures_directory".')
469 51
                    ->end()
470 51
                    ->children()
471 51
                        ->enumNode('mode')
472 51
                        ->info('What should be the behavior of the plugin?')
473 51
                        ->values(['record', 'replay', 'replay_or_record'])
474 51
                        ->isRequired()
475 51
                        ->cannotBeEmpty()
476 51
                    ->end()
477 51
                    ->scalarNode('recorder')
478 51
                        ->info(sprintf('Which recorder to use. Can be "in_memory", "filesystem" or the ID of your service implementing %s and %s. When using filesystem, specify "fixtures_directory" as well.', RecorderInterface::class, PlayerInterface::class))
479 51
                        ->defaultValue('filesystem')
480 51
                        ->cannotBeEmpty()
481 51
                    ->end()
482 51
                    ->scalarNode('naming_strategy')
483 51
                        ->info(sprintf('Which naming strategy to use. Add the ID of your service implementing %s to override the default one.', NamingStrategyInterface::class))
484 51
                        ->defaultValue('default')
485 51
                        ->cannotBeEmpty()
486 51
                    ->end()
487 51
                    ->arrayNode('naming_strategy_options')
488 51
                        ->info('See http://docs.php-http.org/en/latest/plugins/vcr.html#the-naming-strategy for more details')
489 51
                        ->children()
490 51
                            ->arrayNode('hash_headers')
491 51
                                ->info('List of header(s) that make the request unique (Ex: ‘Authorization’)')
492 51
                                ->prototype('scalar')->end()
493 51
                            ->end()
494 51
                            ->arrayNode('hash_body_methods')
495 51
                                ->info('for which request methods the body makes requests distinct.')
496 51
                                ->prototype('scalar')->end()
497 51
                            ->end()
498 51
                        ->end()
499 51
                    ->end() // End naming_strategy_options
500 51
                    ->scalarNode('fixtures_directory')
501 51
                        ->info('Where the responses will be stored and replay from when using the filesystem recorder. Should be accessible to your VCS.')
502 51
                    ->end()
503 51
                ->end()
504 51
            ->end()
505 51
        ->end();
506
507 51
        return $node;
508
    }
509
510
    /**
511
     * Add the definitions for shared plugin configurations.
512
     *
513
     * @param ArrayNodeDefinition $pluginNode the node to add to
514
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
515
     */
516 51
    private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
517
    {
518 51
        $children = $pluginNode->children();
519
520 51
        $children->append($this->createAuthenticationPluginNode());
521 51
        $children->append($this->createCachePluginNode());
522
523
        $children
524 51
            ->arrayNode('cookie')
525 51
                ->canBeEnabled()
526 51
                ->children()
527 51
                    ->scalarNode('cookie_jar')
528 51
                        ->info('This must be a service id to a service implementing '.CookieJar::class)
529 51
                        ->isRequired()
530 51
                        ->cannotBeEmpty()
531 51
                    ->end()
532 51
                ->end()
533 51
            ->end();
534
        // End cookie plugin
535
536
        $children
537 51
            ->arrayNode('history')
538 51
                ->canBeEnabled()
539 51
                ->children()
540 51
                    ->scalarNode('journal')
541 51
                        ->info('This must be a service id to a service implementing '.Journal::class)
542 51
                        ->isRequired()
543 51
                        ->cannotBeEmpty()
544 51
                    ->end()
545 51
                ->end()
546 51
            ->end();
547
        // End history plugin
548
549 51
        $decoder = $children->arrayNode('decoder');
550 51
        $disableAll ? $decoder->canBeEnabled() : $decoder->canBeDisabled();
551 51
        $decoder->addDefaultsIfNotSet()
552 51
            ->children()
553 51
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
554 51
            ->end()
555 51
        ->end();
556
        // End decoder plugin
557
558 51
        $logger = $children->arrayNode('logger');
559 51
        $disableAll ? $logger->canBeEnabled() : $logger->canBeDisabled();
560 51
        $logger->addDefaultsIfNotSet()
561 51
            ->children()
562 51
                ->scalarNode('logger')
563 51
                    ->info('This must be a service id to a service implementing '.LoggerInterface::class)
564 51
                    ->defaultValue('logger')
565 51
                    ->cannotBeEmpty()
566 51
                ->end()
567 51
                ->scalarNode('formatter')
568 51
                    ->info('This must be a service id to a service implementing '.Formatter::class)
569 51
                    ->defaultNull()
570 51
                ->end()
571 51
            ->end()
572 51
        ->end();
573
        // End logger plugin
574
575 51
        $redirect = $children->arrayNode('redirect');
576 51
        $disableAll ? $redirect->canBeEnabled() : $redirect->canBeDisabled();
577 51
        $redirect->addDefaultsIfNotSet()
578 51
            ->children()
579 51
                ->scalarNode('preserve_header')->defaultTrue()->end()
580 51
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
581 51
            ->end()
582 51
        ->end();
583
        // End redirect plugin
584
585 51
        $retry = $children->arrayNode('retry');
586 51
        $disableAll ? $retry->canBeEnabled() : $retry->canBeDisabled();
587 51
        $retry->addDefaultsIfNotSet()
588 51
            ->children()
589 51
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
590 51
            ->end()
591 51
        ->end();
592
        // End retry plugin
593
594 51
        $stopwatch = $children->arrayNode('stopwatch');
595 51
        $disableAll ? $stopwatch->canBeEnabled() : $stopwatch->canBeDisabled();
596 51
        $stopwatch->addDefaultsIfNotSet()
597 51
            ->children()
598 51
                ->scalarNode('stopwatch')
599 51
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
600 51
                    ->defaultValue('debug.stopwatch')
601 51
                    ->cannotBeEmpty()
602 51
                ->end()
603 51
            ->end()
604 51
        ->end();
605
        // End stopwatch plugin
606 51
    }
607
608
    /**
609
     * Create configuration for authentication plugin.
610
     *
611
     * @return NodeDefinition definition for the authentication node in the plugins list
612
     */
613 51
    private function createAuthenticationPluginNode()
614
    {
615 51
        $treeBuilder = new TreeBuilder('authentication');
616
        // Keep compatibility with symfony/config < 4.2
617 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
618
            $node = $treeBuilder->root('authentication');
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
619
        } else {
620 51
            $node = $treeBuilder->getRootNode();
621
        }
622
623
        $node
624 51
            ->useAttributeAsKey('name')
625 51
            ->prototype('array')
626 51
                ->validate()
627 51
                    ->always()
628
                    ->then(function ($config) {
629 8
                        switch ($config['type']) {
630 8
                            case 'basic':
631 7
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
632
633 7
                                break;
634 2
                            case 'bearer':
635 1
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
636
637 1
                                break;
638 2
                            case 'service':
639 2
                                $this->validateAuthenticationType(['service'], $config, 'service');
640
641 1
                                break;
642 1
                            case 'wsse':
643 1
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
644
645 1
                                break;
646
                            case 'query_param':
647
                                $this->validateAuthenticationType(['params'], $config, 'query_param');
648
649
                                break;
650
                        }
651
652 7
                        return $config;
653 51
                    })
654 51
                ->end()
655 51
                ->children()
656 51
                    ->enumNode('type')
657 51
                        ->values(['basic', 'bearer', 'wsse', 'service', 'query_param'])
658 51
                        ->isRequired()
659 51
                        ->cannotBeEmpty()
660 51
                    ->end()
661 51
                    ->scalarNode('username')->end()
662 51
                    ->scalarNode('password')->end()
663 51
                    ->scalarNode('token')->end()
664 51
                    ->scalarNode('service')->end()
665 51
                    ->arrayNode('params')->prototype('scalar')->end()
666 51
                    ->end()
667 51
                ->end()
668 51
            ->end(); // End authentication plugin
669
670 51
        return $node;
671
    }
672
673
    /**
674
     * Validate that the configuration fragment has the specified keys and none other.
675
     *
676
     * @param array  $expected Fields that must exist
677
     * @param array  $actual   Actual configuration hashmap
678
     * @param string $authName Name of authentication method for error messages
679
     *
680
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
681
     */
682 8
    private function validateAuthenticationType(array $expected, array $actual, $authName)
683
    {
684 8
        unset($actual['type']);
685
        // Empty array is always provided, even if the config is not filled.
686 8
        if (empty($actual['params'])) {
687 8
            unset($actual['params']);
688
        }
689 8
        $actual = array_keys($actual);
690 8
        sort($actual);
691 8
        sort($expected);
692
693 8
        if ($expected === $actual) {
694 7
            return;
695
        }
696
697 1
        throw new InvalidConfigurationException(sprintf(
698 1
            'Authentication "%s" requires %s but got %s',
699
            $authName,
700 1
            implode(', ', $expected),
701 1
            implode(', ', $actual)
702
        ));
703
    }
704
705
    /**
706
     * Create configuration for cache plugin.
707
     *
708
     * @return NodeDefinition definition for the cache node in the plugins list
709
     */
710 51
    private function createCachePluginNode()
711
    {
712 51
        $builder = new TreeBuilder('config');
713
        // Keep compatibility with symfony/config < 4.2
714 51
        if (!method_exists($builder, 'getRootNode')) {
715
            $config = $builder->root('config');
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
716
        } else {
717 51
            $config = $builder->getRootNode();
718
        }
719
720
        $config
721 51
            ->fixXmlConfig('method')
722 51
            ->fixXmlConfig('respect_response_cache_directive')
723 51
            ->fixXmlConfig('cache_listener')
724 51
            ->addDefaultsIfNotSet()
725 51
            ->validate()
726
                ->ifTrue(function ($config) {
727
                    // Cannot set both respect_cache_headers and respect_response_cache_directives
728 6
                    return isset($config['respect_cache_headers'], $config['respect_response_cache_directives']);
729 51
                })
730 51
                ->thenInvalid('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives" simultaniously. Use "respect_response_cache_directives" instead.')
731 51
            ->end()
732 51
            ->children()
733 51
                ->scalarNode('cache_key_generator')
734 51
                    ->info('This must be a service id to a service implementing '.CacheKeyGenerator::class)
735 51
                ->end()
736 51
                ->integerNode('cache_lifetime')
737 51
                    ->info('The minimum time we should store a cache item')
738 51
                ->end()
739 51
                ->integerNode('default_ttl')
740 51
                    ->info('The default max age of a Response')
741 51
                ->end()
742 51
                ->arrayNode('blacklisted_paths')
743 51
                    ->info('An array of regular expression patterns for paths not to be cached. Defaults to an empty array.')
744 51
                    ->defaultValue([])
745 51
                    ->beforeNormalization()
746 51
                        ->castToArray()
747 51
                    ->end()
748 51
                    ->prototype('scalar')
749 51
                        ->validate()
750
                            ->ifTrue(function ($v) {
751 1
                                return false === @preg_match($v, '');
752 51
                            })
753 51
                            ->thenInvalid('Invalid regular expression for a blacklisted path: %s')
754 51
                        ->end()
755 51
                    ->end()
756 51
                ->end()
757 51
                ->enumNode('hash_algo')
758 51
                    ->info('Hashing algorithm to use')
759 51
                    ->values(hash_algos())
760 51
                    ->cannotBeEmpty()
761 51
                ->end()
762 51
                ->arrayNode('methods')
763 51
                    ->info('Which request methods to cache')
764 51
                    ->defaultValue(['GET', 'HEAD'])
765 51
                    ->prototype('scalar')
766 51
                        ->validate()
767
                            ->ifTrue(function ($v) {
768
                                /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */
769 1
                                return preg_match('/[^A-Z0-9!#$%&\'*+\-.^_`|~]+/', $v);
770 51
                            })
771 51
                            ->thenInvalid('Invalid method: %s')
772 51
                        ->end()
773 51
                    ->end()
774 51
                ->end()
775 51
                ->arrayNode('cache_listeners')
776 51
                    ->info('A list of service ids to act on the response based on the results of the cache check. Must implement '.CacheListener::class.'. Defaults to an empty array.')
777 51
                    ->beforeNormalization()->castToArray()->end()
778 51
                    ->prototype('scalar')
779 51
                    ->end()
780 51
                ->end()
781 51
                ->scalarNode('respect_cache_headers')
782 51
                    ->info('Whether we should care about cache headers or not [DEPRECATED]')
783 51
                    ->beforeNormalization()
784
                        ->always(function ($v) {
785 3
                            @trigger_error('The option "respect_cache_headers" is deprecated since version 1.3 and will be removed in 2.0. Use "respect_response_cache_directives" instead.', E_USER_DEPRECATED);
786
787 3
                            return $v;
788 51
                        })
789 51
                    ->end()
790 51
                    ->validate()
791 51
                        ->ifNotInArray([null, true, false])
792 51
                        ->thenInvalid('Value for "respect_cache_headers" must be null or boolean')
793 51
                    ->end()
794 51
                ->end()
795 51
                ->variableNode('respect_response_cache_directives')
796 51
                    ->info('A list of cache directives to respect when caching responses')
797 51
                    ->validate()
798
                        ->always(function ($v) {
799 2
                            if (is_null($v) || is_array($v)) {
800 2
                                return $v;
801
                            }
802
803
                            throw new InvalidTypeException();
804 51
                        })
805 51
                    ->end()
806 51
                ->end()
807 51
            ->end()
808
        ;
809
810 51
        $treeBuilder = new TreeBuilder('cache');
811
        // Keep compatibility with symfony/config < 4.2
812 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
813
            $cache = $treeBuilder->root('cache');
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
814
        } else {
815 51
            $cache = $treeBuilder->getRootNode();
816
        }
817
818
        $cache
819 51
            ->canBeEnabled()
820 51
            ->info('Configure HTTP caching, requires the php-http/cache-plugin package')
821 51
            ->addDefaultsIfNotSet()
822 51
            ->validate()
823
                ->ifTrue(function ($v) {
824 6
                    return !empty($v['enabled']) && !class_exists(CachePlugin::class);
825 51
                })
826 51
                ->thenInvalid('To use the cache plugin, you need to require php-http/cache-plugin in your project')
827 51
            ->end()
828 51
            ->children()
829 51
                ->scalarNode('cache_pool')
830 51
                    ->info('This must be a service id to a service implementing '.CacheItemPoolInterface::class)
831 51
                    ->isRequired()
832 51
                    ->cannotBeEmpty()
833 51
                ->end()
834 51
                ->scalarNode('stream_factory')
835 51
                    ->info('This must be a service id to a service implementing '.StreamFactory::class)
836 51
                    ->defaultValue('httplug.stream_factory')
837 51
                    ->cannotBeEmpty()
838 51
                ->end()
839 51
            ->end()
840 51
            ->append($config)
841
        ;
842
843 51
        return $cache;
844
    }
845
}
846