Completed
Push — master ( b587fb...d05392 )
by David
20:34
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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\Message\CookieJar;
9
use Http\Message\Formatter;
10
use Http\Message\StreamFactory;
11
use Psr\Cache\CacheItemPoolInterface;
12
use Psr\Log\LoggerInterface;
13
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
14
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
15
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
16
use Symfony\Component\Config\Definition\ConfigurationInterface;
17
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
18
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
19
20
/**
21
 * This class contains the configuration information for the bundle.
22
 *
23
 * This information is solely responsible for how the different configuration
24
 * sections are normalized, and merged.
25
 *
26
 * @author David Buchmann <[email protected]>
27
 * @author Tobias Nyholm <[email protected]>
28
 */
29
class Configuration implements ConfigurationInterface
30
{
31
    /**
32
     * Whether to use the debug mode.
33
     *
34
     * @see https://github.com/doctrine/DoctrineBundle/blob/v1.5.2/DependencyInjection/Configuration.php#L31-L41
35
     *
36
     * @var bool
37
     */
38
    private $debug;
39
40
    /**
41
     * @param bool $debug
42
     */
43 38
    public function __construct($debug)
44
    {
45 38
        $this->debug = (bool) $debug;
46 38
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 38
    public function getConfigTreeBuilder()
52
    {
53 38
        $treeBuilder = new TreeBuilder('httplug');
54
        // Keep compatibility with symfony/config < 4.2
55 38
        if (!method_exists($treeBuilder, 'getRootNode')) {
56
            $rootNode = $treeBuilder->root('httplug');
57
        } else {
58 38
            $rootNode = $treeBuilder->getRootNode();
59
        }
60
61 38
        $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...
62 38
        $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...
63
64
        $rootNode
65 38
            ->validate()
66 38
                ->ifTrue(function ($v) {
67 32
                    return !empty($v['classes']['client'])
68 29
                        || !empty($v['classes']['message_factory'])
69 29
                        || !empty($v['classes']['uri_factory'])
70 32
                        || !empty($v['classes']['stream_factory']);
71 38
                })
72 38
                ->then(function ($v) {
73 3
                    foreach ($v['classes'] as $key => $class) {
74 3
                        if (null !== $class && !class_exists($class)) {
75 1
                            throw new InvalidConfigurationException(sprintf(
76 1
                                'Class %s specified for httplug.classes.%s does not exist.',
77
                                $class,
78
                                $key
79
                            ));
80
                        }
81
                    }
82
83 2
                    return $v;
84 38
                })
85 38
            ->end()
86 38
            ->beforeNormalization()
87 38
                ->ifTrue(function ($v) {
88 38
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
89 38
                })
90 38
                ->then(function ($v) {
91 4
                    if (array_key_exists('profiling', $v)) {
92 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".');
93
                    }
94
95 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);
96
97 3
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
98 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);
99 1
                        $v['toolbar']['enabled'] = $this->debug;
100
                    }
101
102 3
                    $v['profiling'] = $v['toolbar'];
103
104 3
                    unset($v['toolbar']);
105
106 3
                    return $v;
107 38
                })
108 38
            ->end()
109 38
            ->fixXmlConfig('client')
110 38
            ->children()
111 38
                ->arrayNode('main_alias')
112 38
                    ->addDefaultsIfNotSet()
113 38
                    ->info('Configure which service the main alias point to.')
114 38
                    ->children()
115 38
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
116 38
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
117 38
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
118 38
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
119 38
                    ->end()
120 38
                ->end()
121 38
                ->arrayNode('classes')
122 38
                    ->addDefaultsIfNotSet()
123 38
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
124 38
                    ->children()
125 38
                        ->scalarNode('client')->defaultNull()->end()
126 38
                        ->scalarNode('message_factory')->defaultNull()->end()
127 38
                        ->scalarNode('uri_factory')->defaultNull()->end()
128 38
                        ->scalarNode('stream_factory')->defaultNull()->end()
129 38
                    ->end()
130 38
                ->end()
131 38
                ->arrayNode('profiling')
132 38
                    ->addDefaultsIfNotSet()
133 38
                    ->treatFalseLike(['enabled' => false])
134 38
                    ->treatTrueLike(['enabled' => true])
135 38
                    ->treatNullLike(['enabled' => $this->debug])
136 38
                    ->info('Extend the debug profiler with information about requests.')
137 38
                    ->children()
138 38
                        ->booleanNode('enabled')
139 38
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
140 38
                            ->defaultValue($this->debug)
141 38
                        ->end()
142 38
                        ->scalarNode('formatter')->defaultNull()->end()
143 38
                        ->integerNode('captured_body_length')
144 38
                            ->defaultValue(0)
145 38
                            ->info('Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. Only available with the default formatter (FullHttpMessageFormatter).')
146 38
                        ->end()
147 38
                    ->end()
148 38
                ->end()
149 38
                ->arrayNode('discovery')
150 38
                    ->addDefaultsIfNotSet()
151 38
                    ->info('Control what clients should be found by the discovery.')
152 38
                    ->children()
153 38
                        ->scalarNode('client')
154 38
                            ->defaultValue('auto')
155 38
                            ->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.')
156 38
                        ->end()
157 38
                        ->scalarNode('async_client')
158 38
                            ->defaultNull()
159 38
                            ->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.')
160 38
                        ->end()
161 38
                    ->end()
162 38
                ->end()
163 38
            ->end();
164
165 38
        return $treeBuilder;
166
    }
167
168 38
    private function configureClients(ArrayNodeDefinition $root)
169
    {
170 38
        $root->children()
171 38
            ->arrayNode('clients')
172 38
                ->useAttributeAsKey('name')
173 38
                ->prototype('array')
174 38
                ->fixXmlConfig('plugin')
175 38
                ->validate()
176 38
                    ->ifTrue(function ($config) {
177
                        // Make sure we only allow one of these to be true
178 19
                        return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
179 38
                    })
180 38
                    ->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")')
181 38
                ->end()
182 38
                ->validate()
183 38
                    ->ifTrue(function ($config) {
184 19
                        return 'httplug.factory.auto' === $config['factory'] && !empty($config['config']);
185 38
                    })
186 38
                    ->thenInvalid('If you want to use the "config" key you must also specify a valid "factory".')
187 38
                ->end()
188 38
                ->validate()
189 38
                    ->ifTrue(function ($config) {
190 19
                        return !empty($config['service']) && ('httplug.factory.auto' !== $config['factory'] || !empty($config['config']));
191 38
                    })
192 38
                    ->thenInvalid('If you want to use the "service" key you cannot specify "factory" or "config".')
193 38
                ->end()
194 38
                ->children()
195 38
                    ->scalarNode('factory')
196 38
                        ->defaultValue('httplug.factory.auto')
197 38
                        ->cannotBeEmpty()
198 38
                        ->info('The service id of a factory to use when creating the adapter.')
199 38
                    ->end()
200 38
                    ->scalarNode('service')
201 38
                        ->defaultNull()
202 38
                        ->info('The service id of the client to use.')
203 38
                    ->end()
204 38
                    ->booleanNode('public')
205 38
                        ->defaultNull()
206 38
                        ->info('Set to true if you really cannot use dependency injection and need to make the client service public.')
207 38
                    ->end()
208 38
                    ->booleanNode('flexible_client')
209 38
                        ->defaultFalse()
210 38
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
211 38
                    ->end()
212 38
                    ->booleanNode('http_methods_client')
213 38
                        ->defaultFalse()
214 38
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
215 38
                    ->end()
216 38
                    ->booleanNode('batch_client')
217 38
                        ->defaultFalse()
218 38
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
219 38
                    ->end()
220 38
                    ->variableNode('config')->defaultValue([])->end()
221 38
                    ->append($this->createClientPluginNode())
222 38
                ->end()
223 38
            ->end()
224 38
        ->end();
225 38
    }
226
227
    /**
228
     * @param ArrayNodeDefinition $root
229
     */
230 38
    private function configureSharedPlugins(ArrayNodeDefinition $root)
231
    {
232
        $pluginsNode = $root
233 38
            ->children()
234 38
                ->arrayNode('plugins')
235 38
                ->info('Global plugin configuration. Plugins need to be explicitly added to clients.')
236 38
                ->addDefaultsIfNotSet()
237
            // don't call end to get the plugins node
238
        ;
239 38
        $this->addSharedPluginNodes($pluginsNode);
240 38
    }
241
242
    /**
243
     * Createplugins node of a client.
244
     *
245
     * @return ArrayNodeDefinition The plugin node
246
     */
247 38
    private function createClientPluginNode()
248
    {
249 38
        $treeBuilder = new TreeBuilder('plugins');
250
        // Keep compatibility with symfony/config < 4.2
251 38
        if (!method_exists($treeBuilder, 'getRootNode')) {
252
            $node = $treeBuilder->root('plugins');
253
        } else {
254 38
            $node = $treeBuilder->getRootNode();
255
        }
256
257
        /** @var ArrayNodeDefinition $pluginList */
258
        $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...
259 38
            ->info('A list of plugin service ids and client specific plugin definitions. The order is important.')
260 38
            ->prototype('array')
261
        ;
262
        $pluginList
263
            // support having just a service id in the list
264 38
            ->beforeNormalization()
265 38
                ->always(function ($plugin) {
266 12
                    if (is_string($plugin)) {
267
                        return [
268
                            'reference' => [
269 10
                                'enabled' => true,
270 10
                                'id' => $plugin,
271
                            ],
272
                        ];
273
                    }
274
275 9
                    return $plugin;
276 38
                })
277 38
            ->end()
278
279 38
            ->validate()
280 38
                ->always(function ($plugins) {
281 10
                    foreach ($plugins as $name => $definition) {
282 10
                        if ('authentication' === $name) {
283 10
                            if (!count($definition)) {
284 10
                                unset($plugins['authentication']);
285
                            }
286 10
                        } elseif (!$definition['enabled']) {
287 10
                            unset($plugins[$name]);
288
                        }
289
                    }
290
291 10
                    return $plugins;
292 38
                })
293 38
            ->end()
294
        ;
295 38
        $this->addSharedPluginNodes($pluginList, true);
296
297
        $pluginList
298 38
            ->children()
299 38
                ->arrayNode('reference')
300 38
                    ->canBeEnabled()
301 38
                    ->info('Reference to a plugin service')
302 38
                    ->children()
303 38
                        ->scalarNode('id')
304 38
                            ->info('Service id of a plugin')
305 38
                            ->isRequired()
306 38
                            ->cannotBeEmpty()
307 38
                        ->end()
308 38
                    ->end()
309 38
                ->end()
310 38
                ->arrayNode('add_host')
311 38
                    ->canBeEnabled()
312 38
                    ->addDefaultsIfNotSet()
313 38
                    ->info('Set scheme, host and port in the request URI.')
314 38
                    ->children()
315 38
                        ->scalarNode('host')
316 38
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
317 38
                            ->isRequired()
318 38
                            ->cannotBeEmpty()
319 38
                        ->end()
320 38
                        ->scalarNode('replace')
321 38
                            ->info('Whether to replace the host if request already specifies one')
322 38
                            ->defaultValue(false)
323 38
                        ->end()
324 38
                    ->end()
325 38
                ->end()
326 38
                ->arrayNode('add_path')
327 38
                    ->canBeEnabled()
328 38
                    ->addDefaultsIfNotSet()
329 38
                    ->info('Add a base path to the request.')
330 38
                    ->children()
331 38
                        ->scalarNode('path')
332 38
                            ->info('Path to be added, e.g. /api/v1')
333 38
                            ->isRequired()
334 38
                            ->cannotBeEmpty()
335 38
                        ->end()
336 38
                    ->end()
337 38
                ->end()
338 38
                ->arrayNode('base_uri')
339 38
                    ->canBeEnabled()
340 38
                    ->addDefaultsIfNotSet()
341 38
                    ->info('Set a base URI to the request.')
342 38
                    ->children()
343 38
                        ->scalarNode('uri')
344 38
                            ->info('Base Uri including protocol, optionally the port number and prepend path, e.g. https://api.local:8000/api')
345 38
                            ->isRequired()
346 38
                            ->cannotBeEmpty()
347 38
                        ->end()
348 38
                        ->scalarNode('replace')
349 38
                            ->info('Whether to replace the host if request already specifies one')
350 38
                            ->defaultValue(false)
351 38
                        ->end()
352 38
                    ->end()
353 38
                ->end()
354 38
                ->arrayNode('content_type')
355 38
                    ->canBeEnabled()
356 38
                    ->info('Detect the content type of a request body and set the Content-Type header if it is not already set.')
357 38
                    ->children()
358 38
                        ->booleanNode('skip_detection')
359 38
                            ->info('Whether to skip detection when request body is larger than size_limit')
360 38
                            ->defaultFalse()
361 38
                        ->end()
362 38
                        ->scalarNode('size_limit')
363 38
                            ->info('Skip content type detection if request body is larger than size_limit bytes')
364 38
                        ->end()
365 38
                    ->end()
366 38
                ->end()
367 38
                ->arrayNode('header_append')
368 38
                    ->canBeEnabled()
369 38
                    ->info('Append headers to the request. If the header already exists the value will be appended to the current value.')
370 38
                    ->fixXmlConfig('header')
371 38
                    ->children()
372 38
                        ->arrayNode('headers')
373 38
                            ->info('Keys are the header names, values the header values')
374 38
                            ->normalizeKeys(false)
375 38
                            ->useAttributeAsKey('name')
376 38
                            ->prototype('scalar')->end()
377 38
                        ->end()
378 38
                    ->end()
379 38
                ->end()
380 38
                ->arrayNode('header_defaults')
381 38
                    ->canBeEnabled()
382 38
                    ->info('Set header to default value if it does not exist.')
383 38
                    ->fixXmlConfig('header')
384 38
                    ->children()
385 38
                        ->arrayNode('headers')
386 38
                            ->info('Keys are the header names, values the header values')
387 38
                            ->normalizeKeys(false)
388 38
                            ->useAttributeAsKey('name')
389 38
                            ->prototype('scalar')->end()
390 38
                        ->end()
391 38
                    ->end()
392 38
                ->end()
393 38
                ->arrayNode('header_set')
394 38
                    ->canBeEnabled()
395 38
                    ->info('Set headers to requests. If the header does not exist it wil be set, if the header already exists it will be replaced.')
396 38
                    ->fixXmlConfig('header')
397 38
                    ->children()
398 38
                        ->arrayNode('headers')
399 38
                            ->info('Keys are the header names, values the header values')
400 38
                            ->normalizeKeys(false)
401 38
                            ->useAttributeAsKey('name')
402 38
                            ->prototype('scalar')->end()
403 38
                        ->end()
404 38
                    ->end()
405 38
                ->end()
406 38
                ->arrayNode('header_remove')
407 38
                    ->canBeEnabled()
408 38
                    ->info('Remove headers from requests.')
409 38
                    ->fixXmlConfig('header')
410 38
                    ->children()
411 38
                        ->arrayNode('headers')
412 38
                            ->info('List of header names to remove')
413 38
                            ->prototype('scalar')->end()
414 38
                        ->end()
415 38
                    ->end()
416 38
                ->end()
417 38
                ->arrayNode('query_defaults')
418 38
                    ->canBeEnabled()
419 38
                    ->info('Sets query parameters to default value if they are not present in the request.')
420 38
                    ->fixXmlConfig('parameter')
421 38
                    ->children()
422 38
                        ->arrayNode('parameters')
423 38
                            ->info('List of query parameters. Names and values must not be url encoded as the plugin will encode them.')
424 38
                            ->normalizeKeys(false)
425 38
                            ->useAttributeAsKey('name')
426 38
                            ->prototype('scalar')->end()
427 38
                        ->end()
428 38
                    ->end()
429 38
                ->end()
430 38
            ->end()
431 38
        ->end();
432
433 38
        return $node;
434
    }
435
436
    /**
437
     * Add the definitions for shared plugin configurations.
438
     *
439
     * @param ArrayNodeDefinition $pluginNode the node to add to
440
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
441
     */
442 38
    private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
443
    {
444 38
        $children = $pluginNode->children();
445
446 38
        $children->append($this->createAuthenticationPluginNode());
447 38
        $children->append($this->createCachePluginNode());
448
449
        $children
450 38
            ->arrayNode('cookie')
451 38
                ->canBeEnabled()
452 38
                ->children()
453 38
                    ->scalarNode('cookie_jar')
454 38
                        ->info('This must be a service id to a service implementing '.CookieJar::class)
455 38
                        ->isRequired()
456 38
                        ->cannotBeEmpty()
457 38
                    ->end()
458 38
                ->end()
459 38
            ->end();
460
        // End cookie plugin
461
462
        $children
463 38
            ->arrayNode('history')
464 38
                ->canBeEnabled()
465 38
                ->children()
466 38
                    ->scalarNode('journal')
467 38
                        ->info('This must be a service id to a service implementing '.Journal::class)
468 38
                        ->isRequired()
469 38
                        ->cannotBeEmpty()
470 38
                    ->end()
471 38
                ->end()
472 38
            ->end();
473
        // End history plugin
474
475 38
        $decoder = $children->arrayNode('decoder');
476 38
        $disableAll ? $decoder->canBeEnabled() : $decoder->canBeDisabled();
477 38
        $decoder->addDefaultsIfNotSet()
478 38
            ->children()
479 38
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
480 38
            ->end()
481 38
        ->end();
482
        // End decoder plugin
483
484 38
        $logger = $children->arrayNode('logger');
485 38
        $disableAll ? $logger->canBeEnabled() : $logger->canBeDisabled();
486 38
        $logger->addDefaultsIfNotSet()
487 38
            ->children()
488 38
                ->scalarNode('logger')
489 38
                    ->info('This must be a service id to a service implementing '.LoggerInterface::class)
490 38
                    ->defaultValue('logger')
491 38
                    ->cannotBeEmpty()
492 38
                ->end()
493 38
                ->scalarNode('formatter')
494 38
                    ->info('This must be a service id to a service implementing '.Formatter::class)
495 38
                    ->defaultNull()
496 38
                ->end()
497 38
            ->end()
498 38
        ->end();
499
        // End logger plugin
500
501 38
        $redirect = $children->arrayNode('redirect');
502 38
        $disableAll ? $redirect->canBeEnabled() : $redirect->canBeDisabled();
503 38
        $redirect->addDefaultsIfNotSet()
504 38
            ->children()
505 38
                ->scalarNode('preserve_header')->defaultTrue()->end()
506 38
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
507 38
            ->end()
508 38
        ->end();
509
        // End redirect plugin
510
511 38
        $retry = $children->arrayNode('retry');
512 38
        $disableAll ? $retry->canBeEnabled() : $retry->canBeDisabled();
513 38
        $retry->addDefaultsIfNotSet()
514 38
            ->children()
515 38
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
516 38
            ->end()
517 38
        ->end();
518
        // End retry plugin
519
520 38
        $stopwatch = $children->arrayNode('stopwatch');
521 38
        $disableAll ? $stopwatch->canBeEnabled() : $stopwatch->canBeDisabled();
522 38
        $stopwatch->addDefaultsIfNotSet()
523 38
            ->children()
524 38
                ->scalarNode('stopwatch')
525 38
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
526 38
                    ->defaultValue('debug.stopwatch')
527 38
                    ->cannotBeEmpty()
528 38
                ->end()
529 38
            ->end()
530 38
        ->end();
531
        // End stopwatch plugin
532 38
    }
533
534
    /**
535
     * Create configuration for authentication plugin.
536
     *
537
     * @return NodeDefinition definition for the authentication node in the plugins list
538
     */
539 38
    private function createAuthenticationPluginNode()
540
    {
541 38
        $treeBuilder = new TreeBuilder('authentication');
542
        // Keep compatibility with symfony/config < 4.2
543 38
        if (!method_exists($treeBuilder, 'getRootNode')) {
544
            $node = $treeBuilder->root('authentication');
545
        } else {
546 38
            $node = $treeBuilder->getRootNode();
547
        }
548
549
        $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...
550 38
            ->useAttributeAsKey('name')
551 38
            ->prototype('array')
552 38
                ->validate()
553 38
                    ->always()
554 38
                    ->then(function ($config) {
555 8
                        switch ($config['type']) {
556 8
                            case 'basic':
557 7
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
558
559 7
                                break;
560 2
                            case 'bearer':
561 1
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
562
563 1
                                break;
564 2
                            case 'service':
565 2
                                $this->validateAuthenticationType(['service'], $config, 'service');
566
567 1
                                break;
568 1
                            case 'wsse':
569 1
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
570
571 1
                                break;
572
                            case 'query_param':
573
                                $this->validateAuthenticationType(['params'], $config, 'query_param');
574
575
                                break;
576
                        }
577
578 7
                        return $config;
579 38
                    })
580 38
                ->end()
581 38
                ->children()
582 38
                    ->enumNode('type')
583 38
                        ->values(['basic', 'bearer', 'wsse', 'service', 'query_param'])
584 38
                        ->isRequired()
585 38
                        ->cannotBeEmpty()
586 38
                    ->end()
587 38
                    ->scalarNode('username')->end()
588 38
                    ->scalarNode('password')->end()
589 38
                    ->scalarNode('token')->end()
590 38
                    ->scalarNode('service')->end()
591 38
                    ->arrayNode('params')->prototype('scalar')->end()
592 38
                    ->end()
593 38
                ->end()
594 38
            ->end(); // End authentication plugin
595
596 38
        return $node;
597
    }
598
599
    /**
600
     * Validate that the configuration fragment has the specified keys and none other.
601
     *
602
     * @param array  $expected Fields that must exist
603
     * @param array  $actual   Actual configuration hashmap
604
     * @param string $authName Name of authentication method for error messages
605
     *
606
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
607
     */
608 8
    private function validateAuthenticationType(array $expected, array $actual, $authName)
609
    {
610 8
        unset($actual['type']);
611
        // Empty array is always provided, even if the config is not filled.
612 8
        if (empty($actual['params'])) {
613 8
            unset($actual['params']);
614
        }
615 8
        $actual = array_keys($actual);
616 8
        sort($actual);
617 8
        sort($expected);
618
619 8
        if ($expected === $actual) {
620 7
            return;
621
        }
622
623 1
        throw new InvalidConfigurationException(sprintf(
624 1
            'Authentication "%s" requires %s but got %s',
625
            $authName,
626 1
            implode(', ', $expected),
627 1
            implode(', ', $actual)
628
        ));
629
    }
630
631
    /**
632
     * Create configuration for cache plugin.
633
     *
634
     * @return NodeDefinition definition for the cache node in the plugins list
635
     */
636 38
    private function createCachePluginNode()
637
    {
638 38
        $builder = new TreeBuilder('config');
639
        // Keep compatibility with symfony/config < 4.2
640 38
        if (!method_exists($builder, 'getRootNode')) {
641
            $config = $builder->root('config');
642
        } else {
643 38
            $config = $builder->getRootNode();
644
        }
645
646
        $config
647 38
            ->fixXmlConfig('method')
648 38
            ->fixXmlConfig('respect_response_cache_directive')
649 38
            ->addDefaultsIfNotSet()
650 38
            ->validate()
651 38
                ->ifTrue(function ($config) {
652
                    // Cannot set both respect_cache_headers and respect_response_cache_directives
653 5
                    return isset($config['respect_cache_headers'], $config['respect_response_cache_directives']);
654 38
                })
655 38
                ->thenInvalid('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives" simultaniously. Use "respect_response_cache_directives" instead.')
656 38
            ->end()
657 38
            ->children()
658 38
                ->scalarNode('cache_key_generator')
659 38
                    ->info('This must be a service id to a service implementing '.CacheKeyGenerator::class)
660 38
                ->end()
661 38
                ->integerNode('cache_lifetime')
662 38
                    ->info('The minimum time we should store a cache item')
663 38
                ->end()
664 38
                ->integerNode('default_ttl')
665 38
                    ->info('The default max age of a Response')
666 38
                ->end()
667 38
                ->enumNode('hash_algo')
668 38
                    ->info('Hashing algorithm to use')
669 38
                    ->values(hash_algos())
670 38
                    ->cannotBeEmpty()
671 38
                ->end()
672 38
                ->arrayNode('methods')
673 38
                    ->info('Which request methods to cache')
674 38
                    ->defaultValue(['GET', 'HEAD'])
675 38
                    ->prototype('scalar')
676 38
                        ->validate()
677 38
                            ->ifTrue(function ($v) {
678
                                /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */
679 1
                                return preg_match('/[^A-Z0-9!#$%&\'*+\-.^_`|~]+/', $v);
680 38
                            })
681 38
                            ->thenInvalid('Invalid method: %s')
682 38
                        ->end()
683 38
                    ->end()
684 38
                ->end()
685 38
                ->scalarNode('respect_cache_headers')
686 38
                    ->info('Whether we should care about cache headers or not [DEPRECATED]')
687 38
                    ->beforeNormalization()
688 38
                        ->always(function ($v) {
689 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);
690
691 3
                            return $v;
692 38
                        })
693 38
                    ->end()
694 38
                    ->validate()
695 38
                        ->ifNotInArray([null, true, false])
696 38
                        ->thenInvalid('Value for "respect_cache_headers" must be null or boolean')
697 38
                    ->end()
698 38
                ->end()
699 38
                ->variableNode('respect_response_cache_directives')
700 38
                    ->info('A list of cache directives to respect when caching responses')
701 38
                    ->validate()
702 38
                        ->always(function ($v) {
703 2
                            if (is_null($v) || is_array($v)) {
704 2
                                return $v;
705
                            }
706
707
                            throw new InvalidTypeException();
708 38
                        })
709 38
                    ->end()
710 38
                ->end()
711 38
            ->end()
712
        ;
713
714 38
        $cache = $builder->root('cache');
715
        $cache
716 38
            ->canBeEnabled()
717 38
            ->info('Configure HTTP caching, requires the php-http/cache-plugin package')
718 38
            ->addDefaultsIfNotSet()
719 38
            ->validate()
720 38
                ->ifTrue(function ($v) {
721 5
                    return !empty($v['enabled']) && !class_exists(CachePlugin::class);
722 38
                })
723 38
                ->thenInvalid('To use the cache plugin, you need to require php-http/cache-plugin in your project')
724 38
            ->end()
725 38
            ->children()
726 38
                ->scalarNode('cache_pool')
727 38
                    ->info('This must be a service id to a service implementing '.CacheItemPoolInterface::class)
728 38
                    ->isRequired()
729 38
                    ->cannotBeEmpty()
730 38
                ->end()
731 38
                ->scalarNode('stream_factory')
732 38
                    ->info('This must be a service id to a service implementing '.StreamFactory::class)
733 38
                    ->defaultValue('httplug.stream_factory')
734 38
                    ->cannotBeEmpty()
735 38
                ->end()
736 38
            ->end()
737 38
            ->append($config)
738
        ;
739
740 38
        return $cache;
741
    }
742
}
743