Completed
Push — master ( dd63ce...bad327 )
by David
14:41
created

Configuration::createClientPluginNode()   C

Complexity

Conditions 8
Paths 2

Size

Total Lines 232

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 205
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 232
ccs 205
cts 206
cp 0.9951
rs 6.7555
c 0
b 0
f 0
cc 8
nc 2
nop 0
crap 8

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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
60
        } else {
61 49
            $rootNode = $treeBuilder->getRootNode();
62
        }
63
64 49
        $this->configureClients($rootNode);
0 ignored issues
show
Compatibility introduced by
$rootNode of type object<Symfony\Component...Builder\NodeDefinition> is not a sub-type of object<Symfony\Component...er\ArrayNodeDefinition>. It seems like you assume a child class of the class Symfony\Component\Config...\Builder\NodeDefinition to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
65 49
        $this->configureSharedPlugins($rootNode);
0 ignored issues
show
Compatibility introduced by
$rootNode of type object<Symfony\Component...Builder\NodeDefinition> is not a sub-type of object<Symfony\Component...er\ArrayNodeDefinition>. It seems like you assume a child class of the class Symfony\Component\Config...\Builder\NodeDefinition to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
266
        } else {
267 49
            $node = $treeBuilder->getRootNode();
268
        }
269
270
        /** @var ArrayNodeDefinition $pluginList */
271
        $pluginList = $node
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Config...\Builder\NodeDefinition as the method prototype() does only exist in the following sub-classes of Symfony\Component\Config...\Builder\NodeDefinition: Symfony\Component\Config...der\ArrayNodeDefinition. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
602
        } else {
603 49
            $node = $treeBuilder->getRootNode();
604
        }
605
606
        $node
0 ignored issues
show
Bug introduced by
The method useAttributeAsKey() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. Did you maybe mean attribute()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
699
        } else {
700 49
            $config = $builder->getRootNode();
701
        }
702
703
        $config
704 49
            ->fixXmlConfig('method')
705 49
            ->fixXmlConfig('respect_response_cache_directive')
706 49
            ->addDefaultsIfNotSet()
707 49
            ->validate()
708 49
                ->ifTrue(function ($config) {
709
                    // Cannot set both respect_cache_headers and respect_response_cache_directives
710 5
                    return isset($config['respect_cache_headers'], $config['respect_response_cache_directives']);
711 49
                })
712 49
                ->thenInvalid('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives" simultaniously. Use "respect_response_cache_directives" instead.')
713 49
            ->end()
714 49
            ->children()
715 49
                ->scalarNode('cache_key_generator')
716 49
                    ->info('This must be a service id to a service implementing '.CacheKeyGenerator::class)
717 49
                ->end()
718 49
                ->integerNode('cache_lifetime')
719 49
                    ->info('The minimum time we should store a cache item')
720 49
                ->end()
721 49
                ->integerNode('default_ttl')
722 49
                    ->info('The default max age of a Response')
723 49
                ->end()
724 49
                ->enumNode('hash_algo')
725 49
                    ->info('Hashing algorithm to use')
726 49
                    ->values(hash_algos())
727 49
                    ->cannotBeEmpty()
728 49
                ->end()
729 49
                ->arrayNode('methods')
730 49
                    ->info('Which request methods to cache')
731 49
                    ->defaultValue(['GET', 'HEAD'])
732 49
                    ->prototype('scalar')
733 49
                        ->validate()
734 49
                            ->ifTrue(function ($v) {
735
                                /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */
736 1
                                return preg_match('/[^A-Z0-9!#$%&\'*+\-.^_`|~]+/', $v);
737 49
                            })
738 49
                            ->thenInvalid('Invalid method: %s')
739 49
                        ->end()
740 49
                    ->end()
741 49
                ->end()
742 49
                ->scalarNode('respect_cache_headers')
743 49
                    ->info('Whether we should care about cache headers or not [DEPRECATED]')
744 49
                    ->beforeNormalization()
745 49
                        ->always(function ($v) {
746 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);
747
748 3
                            return $v;
749 49
                        })
750 49
                    ->end()
751 49
                    ->validate()
752 49
                        ->ifNotInArray([null, true, false])
753 49
                        ->thenInvalid('Value for "respect_cache_headers" must be null or boolean')
754 49
                    ->end()
755 49
                ->end()
756 49
                ->variableNode('respect_response_cache_directives')
757 49
                    ->info('A list of cache directives to respect when caching responses')
758 49
                    ->validate()
759 49
                        ->always(function ($v) {
760 2
                            if (is_null($v) || is_array($v)) {
761 2
                                return $v;
762
                            }
763
764
                            throw new InvalidTypeException();
765 49
                        })
766 49
                    ->end()
767 49
                ->end()
768 49
            ->end()
769
        ;
770
771 49
        $cache = $builder->root('cache');
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\Config...der\TreeBuilder::root() has been deprecated with message: since Symfony 4.3, pass the root name to the constructor instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
772
        $cache
773 49
            ->canBeEnabled()
774 49
            ->info('Configure HTTP caching, requires the php-http/cache-plugin package')
775 49
            ->addDefaultsIfNotSet()
776 49
            ->validate()
777 49
                ->ifTrue(function ($v) {
778 5
                    return !empty($v['enabled']) && !class_exists(CachePlugin::class);
779 49
                })
780 49
                ->thenInvalid('To use the cache plugin, you need to require php-http/cache-plugin in your project')
781 49
            ->end()
782 49
            ->children()
783 49
                ->scalarNode('cache_pool')
784 49
                    ->info('This must be a service id to a service implementing '.CacheItemPoolInterface::class)
785 49
                    ->isRequired()
786 49
                    ->cannotBeEmpty()
787 49
                ->end()
788 49
                ->scalarNode('stream_factory')
789 49
                    ->info('This must be a service id to a service implementing '.StreamFactory::class)
790 49
                    ->defaultValue('httplug.stream_factory')
791 49
                    ->cannotBeEmpty()
792 49
                ->end()
793 49
            ->end()
794 49
            ->append($config)
795
        ;
796
797 49
        return $cache;
798
    }
799
}
800