Completed
Pull Request — master (#145)
by Rob
16:44 queued 06:51
created

Configuration   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 547
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 8
dl 0
loc 547
ccs 395
cts 395
cp 1
rs 8.8
c 0
b 0
f 0

8 Methods

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

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

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

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

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
86 1
                        $v['toolbar']['enabled'] = $this->debug;
87
                    }
88 3
89
                    $v['profiling'] = $v['toolbar'];
90 3
91
                    unset($v['toolbar']);
92 3
93 17
                    return $v;
94 17
                })
95 17
            ->end()
96 17
            ->fixXmlConfig('client')
97 17
            ->children()
98 17
                ->arrayNode('main_alias')
99 17
                    ->addDefaultsIfNotSet()
100 17
                    ->info('Configure which service the main alias point to.')
101 17
                    ->children()
102 17
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
103 17
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
104 17
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
105 17
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
106 17
                    ->end()
107 17
                ->end()
108 17
                ->arrayNode('classes')
109 17
                    ->addDefaultsIfNotSet()
110 17
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
111 17
                    ->children()
112 17
                        ->scalarNode('client')->defaultNull()->end()
113 17
                        ->scalarNode('message_factory')->defaultNull()->end()
114 17
                        ->scalarNode('uri_factory')->defaultNull()->end()
115 17
                        ->scalarNode('stream_factory')->defaultNull()->end()
116 17
                    ->end()
117 17
                ->end()
118 17
                ->arrayNode('profiling')
119 17
                    ->addDefaultsIfNotSet()
120 17
                    ->treatFalseLike(['enabled' => false])
121 17
                    ->treatTrueLike(['enabled' => true])
122 17
                    ->treatNullLike(['enabled' => $this->debug])
123 17
                    ->info('Extend the debug profiler with information about requests.')
124 17
                    ->children()
125 17
                        ->booleanNode('enabled')
126 17
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
127 17
                            ->defaultValue($this->debug)
128 17
                        ->end()
129 17
                        ->scalarNode('formatter')->defaultNull()->end()
130 17
                        ->integerNode('captured_body_length')
131 17
                            ->defaultValue(0)
132 17
                            ->info('Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. Only available with the default formatter (FullHttpMessageFormatter).')
133 17
                        ->end()
134 17
                    ->end()
135 17
                ->end()
136 17
                ->arrayNode('discovery')
137 17
                    ->addDefaultsIfNotSet()
138 17
                    ->info('Control what clients should be found by the discovery.')
139 17
                    ->children()
140 17
                        ->scalarNode('client')
141 17
                            ->defaultValue('auto')
142 17
                            ->info('Set to "auto" to see auto discovered client in the web profiler. If provided a service id for a client then this client will be found by auto discovery.')
143 17
                        ->end()
144 17
                        ->scalarNode('async_client')
145 17
                            ->defaultNull()
146 17
                            ->info('Set to "auto" to see auto discovered client in the web profiler. If provided a service id for a client then this client will be found by auto discovery.')
147 17
                        ->end()
148 17
                    ->end()
149 17
                ->end()
150
            ->end();
151 17
152
        return $treeBuilder;
153
    }
154 17
155
    private function configureClients(ArrayNodeDefinition $root)
156 17
    {
157 17
        $root->children()
1 ignored issue
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 fixXmlConfig() 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...
158 17
            ->arrayNode('clients')
159 17
                ->useAttributeAsKey('name')
160 17
                ->prototype('array')
161 17
                ->fixXmlConfig('plugin')
162
                ->validate()
163
                    ->ifTrue(function ($config) {
164 7
                        // Make sure we only allow one of these to be true
165 17
                        return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
166 17
                    })
167 17
                    ->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")')
168 17
                ->end()
169 17
                ->children()
170 17
                    ->scalarNode('factory')
171 17
                        ->isRequired()
172 17
                        ->cannotBeEmpty()
173 17
                        ->info('The service id of a factory to use when creating the adapter.')
174 17
                    ->end()
175 17
                    ->booleanNode('flexible_client')
176 17
                        ->defaultFalse()
177 17
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
178 17
                    ->end()
179 17
                    ->booleanNode('http_methods_client')
180 17
                        ->defaultFalse()
181 17
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
182 17
                    ->end()
183 17
                    ->booleanNode('batch_client')
184 17
                        ->defaultFalse()
185 17
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
186 17
                    ->end()
187 17
                    ->variableNode('config')->defaultValue([])->end()
188 17
                    ->append($this->createClientPluginNode())
189 17
                ->end()
190 17
            ->end()
191 17
        ->end();
192
    }
193
194
    /**
195
     * @param ArrayNodeDefinition $root
196 17
     */
197
    private function configureSharedPlugins(ArrayNodeDefinition $root)
198
    {
199 17
        $pluginsNode = $root
200 17
            ->children()
201 17
                ->arrayNode('plugins')
202 17
                ->info('Global plugin configuration. Plugins need to be explicitly added to clients.')
203
                ->addDefaultsIfNotSet()
204 17
            // don't call end to get the plugins node
205 17
        ;
206 17
        $this->addSharedPluginNodes($pluginsNode);
207
    }
208
209
    /**
210
     * Createplugins node of a client.
211
     *
212
     * @return ArrayNodeDefinition The plugin node
213 17
     */
214
    private function createClientPluginNode()
215 17
    {
216 17
        $builder = new TreeBuilder();
217
        $node = $builder->root('plugins');
218
219
        /** @var ArrayNodeDefinition $pluginList */
220 17
        $pluginList = $node
221 17
            ->info('A list of plugin service ids and client specific plugin definitions. The order is important.')
222 17
            ->prototype('array')
223
        ;
224
        $pluginList
225 17
            // support having just a service id in the list
226
            ->beforeNormalization()
227 8
                ->always(function ($plugin) {
228
                    if (is_string($plugin)) {
229
                        return [
230 7
                            'reference' => [
231 7
                                'enabled' => true,
232 7
                                'id' => $plugin,
233 7
                            ],
234
                        ];
235
                    }
236 5
237 17
                    return $plugin;
238 17
                })
239
            ->end()
240 17
241
            ->validate()
242 7
                ->always(function ($plugins) {
243 7
                    foreach ($plugins as $name => $definition) {
244 7
                        if ('authentication' === $name) {
245 7
                            if (!count($definition)) {
246 7
                                unset($plugins['authentication']);
247 7
                            }
248 7
                        } elseif (!$definition['enabled']) {
249 7
                            unset($plugins[$name]);
250 7
                        }
251
                    }
252 7
253 17
                    return $plugins;
254 17
                })
255
            ->end()
256 17
        ;
257
        $this->addSharedPluginNodes($pluginList, true);
258
259 17
        $pluginList
260 17
            ->children()
261 17
                ->arrayNode('reference')
262 17
                    ->canBeEnabled()
263 17
                    ->info('Reference to a plugin service')
264 17
                    ->children()
265 17
                        ->scalarNode('id')
266 17
                            ->info('Service id of a plugin')
267 17
                            ->isRequired()
268 17
                            ->cannotBeEmpty()
269 17
                        ->end()
270 17
                    ->end()
271 17
                ->end()
272 17
                ->arrayNode('add_host')
273 17
                    ->canBeEnabled()
274 17
                    ->addDefaultsIfNotSet()
275 17
                    ->info('Set scheme, host and port in the request URI.')
276 17
                    ->children()
277 17
                        ->scalarNode('host')
278 17
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
279 17
                            ->isRequired()
280 17
                            ->cannotBeEmpty()
281 17
                        ->end()
282 17
                        ->scalarNode('replace')
283 17
                            ->info('Whether to replace the host if request already specifies one')
284 17
                            ->defaultValue(false)
285 17
                        ->end()
286 17
                    ->end()
287 17
                ->end()
288 17
                ->arrayNode('header_append')
289 17
                    ->canBeEnabled()
290 17
                    ->info('Append headers to the request. If the header already exists the value will be appended to the current value.')
291 17
                    ->fixXmlConfig('header')
292 17
                    ->children()
293 17
                        ->arrayNode('headers')
294 17
                            ->info('Keys are the header names, values the header values')
295 17
                            ->normalizeKeys(false)
296 17
                            ->useAttributeAsKey('name')
297 17
                            ->prototype('scalar')->end()
298 17
                        ->end()
299 17
                    ->end()
300 17
                ->end()
301 17
                ->arrayNode('header_defaults')
302 17
                    ->canBeEnabled()
303 17
                    ->info('Set header to default value if it does not exist.')
304 17
                    ->fixXmlConfig('header')
305 17
                    ->children()
306 17
                        ->arrayNode('headers')
307 17
                            ->info('Keys are the header names, values the header values')
308 17
                            ->normalizeKeys(false)
309 17
                            ->useAttributeAsKey('name')
310 17
                            ->prototype('scalar')->end()
311 17
                        ->end()
312 17
                    ->end()
313 17
                ->end()
314 17
                ->arrayNode('header_set')
315 17
                    ->canBeEnabled()
316 17
                    ->info('Set headers to requests. If the header does not exist it wil be set, if the header already exists it will be replaced.')
317 17
                    ->fixXmlConfig('header')
318 17
                    ->children()
319 17
                        ->arrayNode('headers')
320 17
                            ->info('Keys are the header names, values the header values')
321 17
                            ->normalizeKeys(false)
322 17
                            ->useAttributeAsKey('name')
323 17
                            ->prototype('scalar')->end()
324 17
                        ->end()
325 17
                    ->end()
326 17
                ->end()
327 17
                ->arrayNode('header_remove')
328 17
                    ->canBeEnabled()
329 17
                    ->info('Remove headers from requests.')
330 17
                    ->fixXmlConfig('header')
331 17
                    ->children()
332 17
                        ->arrayNode('headers')
333 17
                            ->info('List of header names to remove')
334 17
                            ->prototype('scalar')->end()
335 17
                        ->end()
336 17
                    ->end()
337 17
                ->end()
338 17
            ->end()
339
        ->end();
340 17
341
        return $node;
342
    }
343
344
    /**
345
     * Add the definitions for shared plugin configurations.
346
     *
347
     * @param ArrayNodeDefinition $pluginNode The node to add to.
348
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
349 17
     */
350
    private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
351 17
    {
352
        $children = $pluginNode->children();
353 17
354
        $children->append($this->createAuthenticationPluginNode());
355 17
356 17
        $children->arrayNode('cache')
357 17
            ->canBeEnabled()
358 17
            ->addDefaultsIfNotSet()
359 17
                ->children()
360 17
                    ->scalarNode('cache_pool')
361 17
                        ->info('This must be a service id to a service implementing Psr\Cache\CacheItemPoolInterface')
362 17
                        ->isRequired()
363 17
                        ->cannotBeEmpty()
364 17
                    ->end()
365 17
                    ->scalarNode('stream_factory')
366 17
                        ->info('This must be a service id to a service implementing Http\Message\StreamFactory')
367 17
                        ->defaultValue('httplug.stream_factory')
368 17
                        ->cannotBeEmpty()
369 17
                    ->end()
370 17
                    ->arrayNode('config')
371 17
                        ->addDefaultsIfNotSet()
372 17
                        ->validate()
373 17
                        ->ifTrue(function ($config) {
374 17
                            // Cannot set both respect_cache_headers and respect_response_cache_directives
375 17
                            return isset($config['respect_cache_headers'], $config['respect_response_cache_directives']);
376 17
                        })
377 17
                        ->thenInvalid('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives" simultaniously. Use "respect_response_cache_directives" instead.')
378
                        ->end()
379
                        ->children()
380 17
                            ->scalarNode('default_ttl')->defaultValue(0)->end()
381 17
                            ->enumNode('respect_cache_headers')
382 17
                                ->values([null, true, false])
383 17
                                ->beforeNormalization()
384 17
                                ->always(function ($v) {
385 17
                                    @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);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
386 17
387 17
                                    return $v;
388 17
                                })
389 17
                                ->end()
390
                            ->end()
391
                            ->variableNode('respect_response_cache_directives')
392 17
                                ->validate()
393 17
                                ->always(function ($v) {
394 17
                                    if (is_null($v) || is_array($v)) {
395 17
                                        return $v;
396 17
                                    }
397 17
                                    throw new InvalidTypeException();
398 17
                                })
399
                                ->end()
400
                            ->end()
401 17
                        ->end()
402 17
                    ->end()
403 17
                ->end()
404 17
            ->end();
405 17
        // End cache plugin
406 17
407 17
        $children->arrayNode('cookie')
408 17
            ->canBeEnabled()
409 17
                ->children()
410 17
                    ->scalarNode('cookie_jar')
411
                        ->info('This must be a service id to a service implementing Http\Message\CookieJar')
412
                        ->isRequired()
413 17
                        ->cannotBeEmpty()
414 17
                    ->end()
415 17
                ->end()
416 17
            ->end();
417 17
        // End cookie plugin
418 17
419 17
        $decoder = $children->arrayNode('decoder');
420 17
        $disableAll ? $decoder->canBeEnabled() : $decoder->canBeDisabled();
421 17
        $decoder->addDefaultsIfNotSet()
422 17
            ->children()
423 17
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
424 17
            ->end()
425 17
        ->end();
426 17
        // End decoder plugin
427 17
428
        $children->arrayNode('history')
429
            ->canBeEnabled()
430 17
                ->children()
431 17
                    ->scalarNode('journal')
432 17
                        ->info('This must be a service id to a service implementing Http\Client\Plugin\Journal')
433 17
                        ->isRequired()
434 17
                        ->cannotBeEmpty()
435 17
                    ->end()
436 17
                ->end()
437 17
            ->end();
438
        // End history plugin
439
440 17
        $logger = $children->arrayNode('logger');
441 17
        $disableAll ? $logger->canBeEnabled() : $logger->canBeDisabled();
442 17
        $logger->addDefaultsIfNotSet()
443 17
            ->children()
444 17
                ->scalarNode('logger')
445 17
                    ->info('This must be a service id to a service implementing Psr\Log\LoggerInterface')
446 17
                    ->defaultValue('logger')
447
                    ->cannotBeEmpty()
448
                ->end()
449 17
                ->scalarNode('formatter')
450 17
                    ->info('This must be a service id to a service implementing Http\Message\Formatter')
451 17
                    ->defaultNull()
452 17
                ->end()
453 17
            ->end()
454 17
        ->end();
455 17
        // End logger plugin
456 17
457 17
        $redirect = $children->arrayNode('redirect');
458 17
        $disableAll ? $redirect->canBeEnabled() : $redirect->canBeDisabled();
459 17
        $redirect->addDefaultsIfNotSet()
460
            ->children()
461 17
                ->scalarNode('preserve_header')->defaultTrue()->end()
462
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
463
            ->end()
464
        ->end();
465
        // End redirect plugin
466
467
        $retry = $children->arrayNode('retry');
468 17
        $disableAll ? $retry->canBeEnabled() : $retry->canBeDisabled();
469
        $retry->addDefaultsIfNotSet()
470 17
            ->children()
471 17
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
472
            ->end()
473 17
        ->end();
474 17
        // End retry plugin
475 17
476 17
        $stopwatch = $children->arrayNode('stopwatch');
477 17
        $disableAll ? $stopwatch->canBeEnabled() : $stopwatch->canBeDisabled();
478 5
        $stopwatch->addDefaultsIfNotSet()
479 5
            ->children()
480 4
                ->scalarNode('stopwatch')
481 4
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
482 2
                    ->defaultValue('debug.stopwatch')
483 1
                    ->cannotBeEmpty()
484 1
                ->end()
485 2
            ->end()
486 2
        ->end();
487 1
        // End stopwatch plugin
488 1
    }
489 1
490 1
    /**
491 4
     * Create configuration for authentication plugin.
492
     *
493 4
     * @return NodeDefinition Definition for the authentication node in the plugins list.
494 17
     */
495 17
    private function createAuthenticationPluginNode()
496 17
    {
497 17
        $builder = new TreeBuilder();
498 17
        $node = $builder->root('authentication');
499 17
        $node
1 ignored issue
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 children() 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...
500 17
            ->useAttributeAsKey('name')
501 17
            ->prototype('array')
502 17
                ->validate()
503 17
                    ->always()
504 17
                    ->then(function ($config) {
505 17
                        switch ($config['type']) {
506 17
                            case 'basic':
507 17
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
508 17
                                break;
509
                            case 'bearer':
510 17
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
511
                                break;
512
                            case 'service':
513
                                $this->validateAuthenticationType(['service'], $config, 'service');
514
                                break;
515
                            case 'wsse':
516
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
517
                                break;
518
                        }
519
520
                        return $config;
521
                    })
522 5
                ->end()
523
                ->children()
524 5
                    ->enumNode('type')
525 5
                        ->values(['basic', 'bearer', 'wsse', 'service'])
526 5
                        ->isRequired()
527 5
                        ->cannotBeEmpty()
528
                    ->end()
529 5
                    ->scalarNode('username')->end()
530 4
                    ->scalarNode('password')->end()
531
                    ->scalarNode('token')->end()
532
                    ->scalarNode('service')->end()
533 1
                    ->end()
534 1
                ->end()
535 1
            ->end(); // End authentication plugin
536 1
537 1
        return $node;
538 1
    }
539
540
    /**
541
     * Validate that the configuration fragment has the specified keys and none other.
542
     *
543
     * @param array  $expected Fields that must exist
544
     * @param array  $actual   Actual configuration hashmap
545
     * @param string $authName Name of authentication method for error messages
546
     *
547
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
548
     */
549
    private function validateAuthenticationType(array $expected, array $actual, $authName)
550
    {
551
        unset($actual['type']);
552
        $actual = array_keys($actual);
553
        sort($actual);
554
        sort($expected);
555
556
        if ($expected === $actual) {
557
            return;
558
        }
559
560
        throw new InvalidConfigurationException(sprintf(
561
            'Authentication "%s" requires %s but got %s',
562
            $authName,
563
            implode(', ', $expected),
564
            implode(', ', $actual)
565
        ));
566
    }
567
}
568