Passed
Push — master ( d72858...0074fd )
by Alex
02:03
created

Configuration::addArrayNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1.0005

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 11
cts 12
cp 0.9167
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 12
nc 1
nop 1
crap 1.0005
1
<?php
2
declare(strict_types=1);
3
4
namespace AlexMasterov\PsyshBundle\DependencyInjection;
5
6
use Symfony\Component\Config\Definition\{
7
    Builder\ArrayNodeDefinition,
8
    Builder\TreeBuilder,
9
    ConfigurationInterface,
10
    Exception\InvalidConfigurationException
11
};
12
13
class Configuration implements ConfigurationInterface
14
{
15
    /**
16
     * @inheritDoc
17
     */
18 3
    public function getConfigTreeBuilder()
19
    {
20 3
        $treeBuilder = new TreeBuilder();
21 3
        $rootNode = $treeBuilder->root('psysh');
22
23
        $rootNode
24 3
            ->children()
25 3
                ->append($this->addVariablesNode())
26 3
                ->append($this->addArrayNode('commands'))
27 3
                ->append($this->addErrorLoggingLevelNode())
28 3
                ->scalarNode('config_dir')->end()
29 3
                ->scalarNode('data_dir')->end()
30 3
                ->scalarNode('runtime_dir')
31 3
                    ->info('Set the shell\'s temporary directory location')
32 3
                ->end()
33 3
                ->integerNode('history_size')
34 3
                    ->info('If set to zero (0), the history size is unlimited')
35 3
                ->end()
36 3
                ->scalarNode('history_file')->end()
37 3
                ->scalarNode('manual_db_file')->end()
38 3
                ->booleanNode('tab_completion')->end()
39 3
                ->append($this->addArrayNode('tab_completion_matchers'))
40 3
                ->scalarNode('startup_message')->end()
41 3
                ->booleanNode('require_semicolons')->end()
42 3
                ->booleanNode('erase_duplicates')->end()
43 3
                ->booleanNode('pcntl')->end()
44 3
                ->booleanNode('readline')->end()
45 3
                ->booleanNode('unicode')->end()
46 3
                ->enumNode('color_mode')
47 3
                    ->values(['auto', 'forced', 'disabled'])
48 3
                ->end()
49 3
                ->scalarNode('pager')->treatNullLike('less')->end()
50 3
                ->enumNode('update_check')->defaultValue('never')
51 3
                    ->values(['never', 'always', 'daily', 'weekly', 'monthly'])
52 3
                ->end()
53 3
            ->end()
54
        ;
55
56 3
        $this->normalizeRootNode($rootNode);
57
58 3
        return $treeBuilder;
59
    }
60
61 3
    private function normalizeRootNode(ArrayNodeDefinition $rootNode): void
62
    {
63
        $normalizer = static function (array $config): array {
1 ignored issue
show
Coding Style introduced by
Expected 1 space after closing parenthesis; found 0
Loading history...
64 2
            static $keys = [
65
                'pcntl'    => 'usePcntl',
66
                'readline' => 'useReadline',
67
                'unicode'  => 'useUnicode',
68
            ];
69
70
            // config_dir -> configDir
71
            $camelize = static function (string $value): string {
1 ignored issue
show
Coding Style introduced by
Expected 1 space after closing parenthesis; found 0
Loading history...
72 2
                return \str_replace('_', '', \lcfirst(\ucwords(\strtolower($value), '_')));
73 2
            };
74
75 2
            $normalized = [];
76 2
            foreach ($config as $key => $value) {
77 2
                if (empty($value)) {
78 1
                    continue;
79
                }
80 2
                $key = $keys[$key] ?? $camelize($key);
81 2
                $normalized[$key] = $value;
82
            }
83
84 2
            return $normalized;
85 3
        };
86
87 3
        $rootNode->validate()->always()->then($normalizer)->end();
88 3
    }
89
90 3
    private function addVariablesNode(): ArrayNodeDefinition
91
    {
92 3
        $node = new ArrayNodeDefinition('variables');
93
        $node
94 3
            ->normalizeKeys(false)
95 3
            ->useAttributeAsKey('name')
96 3
            ->prototype('variable')->end()
97 3
            ->validate()
98 3
                ->always()
99
                ->then(static function ($variables) {
100 2
                    return \array_filter($variables, 'is_string');
101 3
                })
102 3
            ->end()
103
        ;
104
105 3
        return $node;
106
    }
107
108 3
    private function addErrorLoggingLevelNode(): ArrayNodeDefinition
109
    {
110 3
        $node = new ArrayNodeDefinition('error_logging_level');
111
        $node
112 3
            ->beforeNormalization()
113 3
                ->ifString()
114
                ->then(static function ($v) {
115 2
                    return \preg_split('/\s*,\s*/', $v, -1, \PREG_SPLIT_NO_EMPTY);
116 3
                })
117 3
            ->end()
118 3
            ->prototype('scalar')->end()
119 3
            ->validate()
120 3
                ->always()
121
                ->then(static function ($errors) {
122 2
                    static $level = null;
123 2
                    static $missingErrors = [];
124 2
                    foreach (\array_unique($errors) as $error) {
125 2
                        $constant = \strtoupper("E_{$error}");
126 2
                        if (\defined($constant)) {
127 1
                            $level |= \constant($constant);
128
                        } else {
129 2
                            $missingErrors[] = $constant;
130
                        }
131
                    }
132
133 2
                    if (empty($missingErrors)) {
134 1
                        return $level;
135
                    }
136
137 1
                    throw new InvalidConfigurationException(\sprintf(
138 1
                        'The errors are not supported: "%s".',
139 1
                        \implode('", "', $missingErrors)
140
                    ));
141 3
                })
142 3
            ->end()
143
        ;
144
145 3
        return $node;
146
    }
147
148 3
    private function addArrayNode(string $name): ArrayNodeDefinition
149
    {
150 3
        $node = new ArrayNodeDefinition($name);
151
        $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 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...
152 3
            ->normalizeKeys(false)
153 3
            ->useAttributeAsKey('name')
154 3
            ->beforeNormalization()
155 3
                ->ifString()
156 3
                ->then(static function ($v) {
157
                    return \preg_split('/\s*,\s*/', $v, -1, \PREG_SPLIT_NO_EMPTY);
158 3
                })
159 3
            ->end()
160 3
            ->prototype('scalar')->end()
161
        ;
162
163 3
        return $node;
164
    }
165
}
166