Completed
Push — master ( 1b3bec...57b308 )
by David
04:13
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

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
eloc 2
nc 1
nop 1
crap 1
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
     */
35 19
    public function __construct($debug)
36
    {
37 19
        $this->debug = (bool) $debug;
38 19
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43 19
    public function getConfigTreeBuilder()
44
    {
45 19
        $treeBuilder = new TreeBuilder();
46 19
        $rootNode = $treeBuilder->root('httplug');
47
48 19
        $this->configureClients($rootNode);
49 19
        $this->configureSharedPlugins($rootNode);
50
51
        $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 19
            ->validate()
53
                ->ifTrue(function ($v) {
54 15
                    return !empty($v['classes']['client'])
55 15
                        || !empty($v['classes']['message_factory'])
56 12
                        || !empty($v['classes']['uri_factory'])
57 15
                        || !empty($v['classes']['stream_factory']);
58 19
                })
59
                ->then(function ($v) {
60 3
                    foreach ($v['classes'] as $key => $class) {
61 3
                        if (null !== $class && !class_exists($class)) {
62 1
                            throw new InvalidConfigurationException(sprintf(
63 1
                                'Class %s specified for httplug.classes.%s does not exist.',
64 1
                                $class,
65
                                $key
66 1
                            ));
67
                        }
68 2
                    }
69
70 2
                    return $v;
71 19
                })
72 19
            ->end()
73 19
            ->beforeNormalization()
74
                ->ifTrue(function ($v) {
75 19
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
76 19
                })
77
                ->then(function ($v) {
78 4
                    if (array_key_exists('profiling', $v)) {
79 1
                        throw new InvalidConfigurationException('Can\'t configure both "toolbar" and "profiling" section. The "toolbar" config is deprecated as of version 1.3.0, please only use "profiling".');
80
                    }
81
82 3
                    @trigger_error('"httplug.toolbar" config is deprecated since version 1.3 and will be removed in 2.0. Use "httplug.profiling" instead.', E_USER_DEPRECATED);
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
84 3
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
85 1
                        @trigger_error('"auto" value in "httplug.toolbar" config is deprecated since version 1.3 and will be removed in 2.0. Use a boolean value instead.', E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
86 1
                        $v['toolbar']['enabled'] = $this->debug;
87 1
                    }
88
89 3
                    $v['profiling'] = $v['toolbar'];
90
91 3
                    unset($v['toolbar']);
92
93 3
                    return $v;
94 19
                })
95 19
            ->end()
96 19
            ->fixXmlConfig('client')
97 19
            ->children()
98 19
                ->arrayNode('main_alias')
99 19
                    ->addDefaultsIfNotSet()
100 19
                    ->info('Configure which service the main alias point to.')
101 19
                    ->children()
102 19
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
103 19
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
104 19
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
105 19
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
106 19
                    ->end()
107 19
                ->end()
108 19
                ->arrayNode('classes')
109 19
                    ->addDefaultsIfNotSet()
110 19
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
111 19
                    ->children()
112 19
                        ->scalarNode('client')->defaultNull()->end()
113 19
                        ->scalarNode('message_factory')->defaultNull()->end()
114 19
                        ->scalarNode('uri_factory')->defaultNull()->end()
115 19
                        ->scalarNode('stream_factory')->defaultNull()->end()
116 19
                    ->end()
117 19
                ->end()
118 19
                ->arrayNode('profiling')
119 19
                    ->addDefaultsIfNotSet()
120 19
                    ->treatFalseLike(['enabled' => false])
121 19
                    ->treatTrueLike(['enabled' => true])
122 19
                    ->treatNullLike(['enabled' => $this->debug])
123 19
                    ->info('Extend the debug profiler with information about requests.')
124 19
                    ->children()
125 19
                        ->booleanNode('enabled')
126 19
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
127 19
                            ->defaultValue($this->debug)
128 19
                        ->end()
129 19
                        ->scalarNode('formatter')->defaultNull()->end()
130 19
                        ->integerNode('captured_body_length')
131 19
                            ->defaultValue(0)
132 19
                            ->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 19
                        ->end()
134 19
                    ->end()
135 19
                ->end()
136 19
                ->arrayNode('discovery')
137 19
                    ->addDefaultsIfNotSet()
138 19
                    ->info('Control what clients should be found by the discovery.')
139 19
                    ->children()
140 19
                        ->scalarNode('client')
141 19
                            ->defaultValue('auto')
142 19
                            ->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 19
                        ->end()
144 19
                        ->scalarNode('async_client')
145 19
                            ->defaultNull()
146 19
                            ->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 19
                        ->end()
148 19
                    ->end()
149 19
                ->end()
150 19
            ->end();
151
152 19
        return $treeBuilder;
153
    }
154
155 19
    private function configureClients(ArrayNodeDefinition $root)
156
    {
157 19
        $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 19
            ->arrayNode('clients')
159 19
                ->useAttributeAsKey('name')
160 19
                ->prototype('array')
161 19
                ->fixXmlConfig('plugin')
162 19
                ->validate()
163
                    ->ifTrue(function ($config) {
164
                        // Make sure we only allow one of these to be true
165 7
                        return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
166 19
                    })
167 19
                    ->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 19
                ->end()
169 19
                ->children()
170 19
                    ->scalarNode('factory')
171 19
                        ->isRequired()
172 19
                        ->cannotBeEmpty()
173 19
                        ->info('The service id of a factory to use when creating the adapter.')
174 19
                    ->end()
175 19
                    ->booleanNode('flexible_client')
176 19
                        ->defaultFalse()
177 19
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
178 19
                    ->end()
179 19
                    ->booleanNode('http_methods_client')
180 19
                        ->defaultFalse()
181 19
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
182 19
                    ->end()
183 19
                    ->booleanNode('batch_client')
184 19
                        ->defaultFalse()
185 19
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
186 19
                    ->end()
187 19
                    ->variableNode('config')->defaultValue([])->end()
188 19
                    ->append($this->createClientPluginNode())
189 19
                ->end()
190 19
            ->end()
191 19
        ->end();
192 19
    }
193
194
    /**
195
     * @param ArrayNodeDefinition $root
196
     */
197 19
    private function configureSharedPlugins(ArrayNodeDefinition $root)
198
    {
199
        $pluginsNode = $root
200 19
            ->children()
201 19
                ->arrayNode('plugins')
202 19
                ->info('Global plugin configuration. Plugins need to be explicitly added to clients.')
203 19
                ->addDefaultsIfNotSet()
204
            // don't call end to get the plugins node
205 19
        ;
206 19
        $this->addSharedPluginNodes($pluginsNode);
207 19
    }
208
209
    /**
210
     * Createplugins node of a client.
211
     *
212
     * @return ArrayNodeDefinition The plugin node
213
     */
214 19
    private function createClientPluginNode()
215
    {
216 19
        $builder = new TreeBuilder();
217 19
        $node = $builder->root('plugins');
218
219
        /** @var ArrayNodeDefinition $pluginList */
220
        $pluginList = $node
221 19
            ->info('A list of plugin service ids and client specific plugin definitions. The order is important.')
222 19
            ->prototype('array')
223 19
        ;
224
        $pluginList
225
            // support having just a service id in the list
226 19
            ->beforeNormalization()
227
                ->always(function ($plugin) {
228 8
                    if (is_string($plugin)) {
229
                        return [
230
                            'reference' => [
231 7
                                'enabled' => true,
232 7
                                'id' => $plugin,
233 7
                            ],
234 7
                        ];
235
                    }
236
237 5
                    return $plugin;
238 19
                })
239 19
            ->end()
240
241 19
            ->validate()
242
                ->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 7
                    }
252
253 7
                    return $plugins;
254 19
                })
255 19
            ->end()
256
        ;
257 19
        $this->addSharedPluginNodes($pluginList, true);
258
259
        $pluginList
260 19
            ->children()
261 19
                ->arrayNode('reference')
262 19
                    ->canBeEnabled()
263 19
                    ->info('Reference to a plugin service')
264 19
                    ->children()
265 19
                        ->scalarNode('id')
266 19
                            ->info('Service id of a plugin')
267 19
                            ->isRequired()
268 19
                            ->cannotBeEmpty()
269 19
                        ->end()
270 19
                    ->end()
271 19
                ->end()
272 19
                ->arrayNode('add_host')
273 19
                    ->canBeEnabled()
274 19
                    ->addDefaultsIfNotSet()
275 19
                    ->info('Set scheme, host and port in the request URI.')
276 19
                    ->children()
277 19
                        ->scalarNode('host')
278 19
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
279 19
                            ->isRequired()
280 19
                            ->cannotBeEmpty()
281 19
                        ->end()
282 19
                        ->scalarNode('replace')
283 19
                            ->info('Whether to replace the host if request already specifies one')
284 19
                            ->defaultValue(false)
285 19
                        ->end()
286 19
                    ->end()
287 19
                ->end()
288 19
                ->arrayNode('header_append')
289 19
                    ->canBeEnabled()
290 19
                    ->info('Append headers to the request. If the header already exists the value will be appended to the current value.')
291 19
                    ->fixXmlConfig('header')
292 19
                    ->children()
293 19
                        ->arrayNode('headers')
294 19
                            ->info('Keys are the header names, values the header values')
295 19
                            ->normalizeKeys(false)
296 19
                            ->useAttributeAsKey('name')
297 19
                            ->prototype('scalar')->end()
298 19
                        ->end()
299 19
                    ->end()
300 19
                ->end()
301 19
                ->arrayNode('header_defaults')
302 19
                    ->canBeEnabled()
303 19
                    ->info('Set header to default value if it does not exist.')
304 19
                    ->fixXmlConfig('header')
305 19
                    ->children()
306 19
                        ->arrayNode('headers')
307 19
                            ->info('Keys are the header names, values the header values')
308 19
                            ->normalizeKeys(false)
309 19
                            ->useAttributeAsKey('name')
310 19
                            ->prototype('scalar')->end()
311 19
                        ->end()
312 19
                    ->end()
313 19
                ->end()
314 19
                ->arrayNode('header_set')
315 19
                    ->canBeEnabled()
316 19
                    ->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 19
                    ->fixXmlConfig('header')
318 19
                    ->children()
319 19
                        ->arrayNode('headers')
320 19
                            ->info('Keys are the header names, values the header values')
321 19
                            ->normalizeKeys(false)
322 19
                            ->useAttributeAsKey('name')
323 19
                            ->prototype('scalar')->end()
324 19
                        ->end()
325 19
                    ->end()
326 19
                ->end()
327 19
                ->arrayNode('header_remove')
328 19
                    ->canBeEnabled()
329 19
                    ->info('Remove headers from requests.')
330 19
                    ->fixXmlConfig('header')
331 19
                    ->children()
332 19
                        ->arrayNode('headers')
333 19
                            ->info('List of header names to remove')
334 19
                            ->prototype('scalar')->end()
335 19
                        ->end()
336 19
                    ->end()
337 19
                ->end()
338 19
            ->end()
339 19
        ->end();
340
341 19
        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
     */
350 19
    private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
351
    {
352 19
        $children = $pluginNode->children();
353
354 19
        $children->append($this->createAuthenticationPluginNode());
355
356 19
        $children->arrayNode('cache')
357 19
            ->canBeEnabled()
358 19
            ->addDefaultsIfNotSet()
359 19
                ->children()
360 19
                    ->scalarNode('cache_pool')
361 19
                        ->info('This must be a service id to a service implementing Psr\Cache\CacheItemPoolInterface')
362 19
                        ->isRequired()
363 19
                        ->cannotBeEmpty()
364 19
                    ->end()
365 19
                    ->scalarNode('stream_factory')
366 19
                        ->info('This must be a service id to a service implementing Http\Message\StreamFactory')
367 19
                        ->defaultValue('httplug.stream_factory')
368 19
                        ->cannotBeEmpty()
369 19
                    ->end()
370 19
                    ->arrayNode('config')
371 19
                        ->addDefaultsIfNotSet()
372 19
                        ->validate()
373
                        ->ifTrue(function ($config) {
374
                            // Cannot set both respect_cache_headers and respect_response_cache_directives
375 3
                            return isset($config['respect_cache_headers'], $config['respect_response_cache_directives']);
376 19
                        })
377 19
                        ->thenInvalid('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives" simultaniously. Use "respect_response_cache_directives" instead.')
378 19
                        ->end()
379 19
                        ->children()
380 19
                            ->scalarNode('default_ttl')->defaultValue(0)->end()
381 19
                            ->enumNode('respect_cache_headers')
382 19
                                ->values([null, true, false])
383 19
                                ->beforeNormalization()
384
                                ->always(function ($v) {
385 2
                                    @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
387 2
                                    return $v;
388 19
                                })
389 19
                                ->end()
390 19
                            ->end()
391 19
                            ->variableNode('respect_response_cache_directives')
392 19
                                ->validate()
393
                                ->always(function ($v) {
394 2
                                    if (is_null($v) || is_array($v)) {
395 2
                                        return $v;
396
                                    }
397
                                    throw new InvalidTypeException();
398 19
                                })
399 19
                                ->end()
400 19
                            ->end()
401 19
                        ->end()
402 19
                    ->end()
403 19
                ->end()
404 19
            ->end();
405
        // End cache plugin
406
407 19
        $children->arrayNode('cookie')
408 19
            ->canBeEnabled()
409 19
                ->children()
410 19
                    ->scalarNode('cookie_jar')
411 19
                        ->info('This must be a service id to a service implementing Http\Message\CookieJar')
412 19
                        ->isRequired()
413 19
                        ->cannotBeEmpty()
414 19
                    ->end()
415 19
                ->end()
416 19
            ->end();
417
        // End cookie plugin
418
419 19
        $decoder = $children->arrayNode('decoder');
420 19
        $disableAll ? $decoder->canBeEnabled() : $decoder->canBeDisabled();
421 19
        $decoder->addDefaultsIfNotSet()
422 19
            ->children()
423 19
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
424 19
            ->end()
425 19
        ->end();
426
        // End decoder plugin
427
428 19
        $children->arrayNode('history')
429 19
            ->canBeEnabled()
430 19
                ->children()
431 19
                    ->scalarNode('journal')
432 19
                        ->info('This must be a service id to a service implementing Http\Client\Plugin\Journal')
433 19
                        ->isRequired()
434 19
                        ->cannotBeEmpty()
435 19
                    ->end()
436 19
                ->end()
437 19
            ->end();
438
        // End history plugin
439
440 19
        $logger = $children->arrayNode('logger');
441 19
        $disableAll ? $logger->canBeEnabled() : $logger->canBeDisabled();
442 19
        $logger->addDefaultsIfNotSet()
443 19
            ->children()
444 19
                ->scalarNode('logger')
445 19
                    ->info('This must be a service id to a service implementing Psr\Log\LoggerInterface')
446 19
                    ->defaultValue('logger')
447 19
                    ->cannotBeEmpty()
448 19
                ->end()
449 19
                ->scalarNode('formatter')
450 19
                    ->info('This must be a service id to a service implementing Http\Message\Formatter')
451 19
                    ->defaultNull()
452 19
                ->end()
453 19
            ->end()
454 19
        ->end();
455
        // End logger plugin
456
457 19
        $redirect = $children->arrayNode('redirect');
458 19
        $disableAll ? $redirect->canBeEnabled() : $redirect->canBeDisabled();
459 19
        $redirect->addDefaultsIfNotSet()
460 19
            ->children()
461 19
                ->scalarNode('preserve_header')->defaultTrue()->end()
462 19
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
463 19
            ->end()
464 19
        ->end();
465
        // End redirect plugin
466
467 19
        $retry = $children->arrayNode('retry');
468 19
        $disableAll ? $retry->canBeEnabled() : $retry->canBeDisabled();
469 19
        $retry->addDefaultsIfNotSet()
470 19
            ->children()
471 19
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
472 19
            ->end()
473 19
        ->end();
474
        // End retry plugin
475
476 19
        $stopwatch = $children->arrayNode('stopwatch');
477 19
        $disableAll ? $stopwatch->canBeEnabled() : $stopwatch->canBeDisabled();
478 19
        $stopwatch->addDefaultsIfNotSet()
479 19
            ->children()
480 19
                ->scalarNode('stopwatch')
481 19
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
482 19
                    ->defaultValue('debug.stopwatch')
483 19
                    ->cannotBeEmpty()
484 19
                ->end()
485 19
            ->end()
486 19
        ->end();
487
        // End stopwatch plugin
488 19
    }
489
490
    /**
491
     * Create configuration for authentication plugin.
492
     *
493
     * @return NodeDefinition Definition for the authentication node in the plugins list.
494
     */
495 19
    private function createAuthenticationPluginNode()
496
    {
497 19
        $builder = new TreeBuilder();
498 19
        $node = $builder->root('authentication');
499
        $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 19
            ->useAttributeAsKey('name')
501 19
            ->prototype('array')
502 19
                ->validate()
503 19
                    ->always()
504 19
                    ->then(function ($config) {
505 5
                        switch ($config['type']) {
506 5
                            case 'basic':
507 4
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
508 4
                                break;
509 2
                            case 'bearer':
510 1
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
511 1
                                break;
512 2
                            case 'service':
513 2
                                $this->validateAuthenticationType(['service'], $config, 'service');
514 1
                                break;
515 1
                            case 'wsse':
516 1
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
517 1
                                break;
518 4
                        }
519
520 4
                        return $config;
521 19
                    })
522 19
                ->end()
523 19
                ->children()
524 19
                    ->enumNode('type')
525 19
                        ->values(['basic', 'bearer', 'wsse', 'service'])
526 19
                        ->isRequired()
527 19
                        ->cannotBeEmpty()
528 19
                    ->end()
529 19
                    ->scalarNode('username')->end()
530 19
                    ->scalarNode('password')->end()
531 19
                    ->scalarNode('token')->end()
532 19
                    ->scalarNode('service')->end()
533 19
                    ->end()
534 19
                ->end()
535 19
            ->end(); // End authentication plugin
536
537 19
        return $node;
538
    }
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 5
    private function validateAuthenticationType(array $expected, array $actual, $authName)
550
    {
551 5
        unset($actual['type']);
552 5
        $actual = array_keys($actual);
553 5
        sort($actual);
554 5
        sort($expected);
555
556 5
        if ($expected === $actual) {
557 4
            return;
558
        }
559
560 1
        throw new InvalidConfigurationException(sprintf(
561 1
            'Authentication "%s" requires %s but got %s',
562 1
            $authName,
563 1
            implode(', ', $expected),
564 1
            implode(', ', $actual)
565 1
        ));
566
    }
567
}
568