Completed
Push — master ( 28a6d6...6b5086 )
by Yann
03:15
created

Configuration::nodeRequiredIfEnabled()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.0466

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 6
cts 7
cp 0.8571
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 1
nop 1
crap 4.0466
1
<?php
2
3
namespace Yokai\MessengerBundle\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
10
/**
11
 * @author Yann Eugoné <[email protected]>
12
 */
13
class Configuration implements ConfigurationInterface
14
{
15
    /**
16
     * @var string
17
     */
18
    private $name;
19
20
    /**
21
     * @var TreeBuilder
22
     */
23
    private $builder;
24
25
    /**
26
     * @param string $name
27
     */
28 13
    public function __construct($name)
29
    {
30 13
        $this->name = $name;
31 13
        $this->builder = new TreeBuilder();
32 13
    }
33
34
    /**
35
     * @inheritdoc
36
     */
37 13
    public function getConfigTreeBuilder()
38
    {
39 13
        $builder = $this->getBuilder();
40 13
        $root = $builder->root($this->name);
41
42
        $root
43 13
            ->addDefaultsIfNotSet()
44 13
            ->fixXmlConfig('message')
45 13
            ->children()
46 13
                ->scalarNode('logging_channel')->defaultValue('app')->end()
47 13
                ->append($this->getContentBuilderNode())
48 13
                ->append($this->getChannelsNode())
49 13
                ->append($this->getMessagesNode())
50 13
            ->end()
51
        ;
52
53 13
        return $builder;
54
    }
55
56
    /**
57
     * @return TreeBuilder
58
     */
59 13
    private function getBuilder()
60
    {
61 13
        return clone $this->builder;
62
    }
63
64
    /**
65
     * @param string $name
66
     *
67
     * @return ArrayNodeDefinition
68
     */
69 13
    private function root($name)
70
    {
71 13
        return $this->getBuilder()->root($name);
72
    }
73
74
    /**
75
     * @return NodeDefinition
76
     */
77 13
    private function getChannelsNode()
78
    {
79 13
        $node = $this->root('channels');
80
81
        $node
82 13
            ->addDefaultsIfNotSet()
83 13
            ->children()
84 13
                ->append($this->getSwiftmailerChannelNode())
85 13
                ->append($this->getDoctrineChannelNode())
86 13
                ->append($this->getMobileChannelNode())
87 13
            ->end()
88
        ;
89
90 13
        return $node;
91
    }
92
93
    /**
94
     * @return NodeDefinition
95
     */
96 13
    private function getMessagesNode()
97
    {
98 13
        $node = $this->root('messages');
99
100
        $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...
101 13
            ->defaultValue([])
102 13
            ->prototype('array')
103 13
                ->fixXmlConfig('channel')
104 13
                ->children()
105 13
                    ->scalarNode('id')->isRequired()->end()
106 13
                    ->arrayNode('channels')
107 13
                        ->beforeNormalization()
108 13
                            ->ifString()->then($this->stringToArray())
109 13
                        ->end()
110 13
                        ->requiresAtLeastOneElement()
111 13
                        ->prototype('scalar')->end()
112 13
                    ->end()
113 13
                    ->arrayNode('defaults')
114 13
                        ->defaultValue([])
115 13
                        ->prototype('variable')->end()
116 13
                    ->end()
117 13
                    ->arrayNode('options')
118 13
                        ->defaultValue([])
119 13
                        ->useAttributeAsKey('channel')
120 13
                        ->prototype('variable')
121 13
                            ->validate()
122 13
                                ->ifTrue($this->isNotHash())
123 13
                                    ->thenInvalid('Expected a hash for channel options, got %s.')
124 13
                            ->end()
125 13
                        ->end()
126 13
                    ->end()
127 13
                ->end()
128 13
            ->end()
129
        ;
130
131 13
        return $node;
132
    }
133
134
    /**
135
     * @return NodeDefinition
136
     */
137 13
    private function getSwiftmailerChannelNode()
138
    {
139 13
        $node = $this->root('swiftmailer');
140
141
        $node
142 13
            ->canBeEnabled()
143 13
            ->validate()
144 13
                ->ifTrue($this->nodeRequiredIfEnabled('from'))
145 13
                    ->thenInvalid($this->nodeMustBeConfigured('from', 'channels.swiftmailer'))
146 13
            ->end()
147 13
            ->children()
148 13
                ->arrayNode('from')
149 13
                    ->normalizeKeys(false)
150 13
                    ->beforeNormalization()
151 13
                        ->ifString()->then($this->stringToArray())
152 13
                    ->end()
153 13
                    ->requiresAtLeastOneElement()
154 13
                    ->prototype('scalar')->end()
155 13
                ->end()
156 13
                ->scalarNode('translator_catalog')->defaultValue('notifications')->end()
157 13
            ->end()
158
        ;
159
160 13
        return $node;
161
    }
162
163
    /**
164
     * @return NodeDefinition
165
     */
166 13
    private function getDoctrineChannelNode()
167
    {
168 13
        $node = $this->root('doctrine');
169
170
        $node
171 13
            ->canBeEnabled()
172 13
            ->children()
173 13
                ->scalarNode('manager')->defaultValue('default')->end()
174 13
            ->end()
175
        ;
176
177 13
        return $node;
178
    }
179
180
    /**
181
     * @return NodeDefinition
182
     */
183 13
    private function getMobileChannelNode()
184
    {
185 13
        $node = $this->root('mobile');
186
187
        $node
188 13
            ->canBeEnabled()
189 13
            ->children()
190 13
                ->scalarNode('environment')->defaultValue('dev')->end()
191 13
                ->arrayNode('apns')
192 13
                    ->canBeDisabled()
193 13
                    ->validate()
194 13
                        ->ifTrue($this->nodeRequiredIfEnabled('certificate'))
195 13
                            ->thenInvalid($this->nodeMustBeConfigured('certificate', 'channels.mobile.apns'))
196 13
                        ->ifTrue($this->nodeRequiredIfEnabled('certificate'))
197 13
                            ->thenInvalid($this->nodeMustBeConfigured('pass_phrase', 'channels.mobile.apns'))
198 13
                    ->end()
199 13
                    ->children()
200 13
                        ->scalarNode('certificate')->defaultNull()->end()
201 13
                        ->scalarNode('pass_phrase')->defaultNull()->end()
202 13
                    ->end()
203 13
                ->end()
204 13
                ->arrayNode('gcm')
205 13
                    ->canBeDisabled()
206 13
                    ->validate()
207 13
                        ->ifTrue($this->nodeRequiredIfEnabled('api_key'))
208 13
                            ->thenInvalid($this->nodeMustBeConfigured('api_key', 'channels.mobile.gcm'))
209 13
                    ->end()
210 13
                    ->children()
211 13
                        ->scalarNode('api_key')->defaultNull()->end()
212 13
                    ->end()
213 13
                ->end()
214 13
            ->end()
215
        ;
216
217 13
        return $node;
218
    }
219
220
    /**
221
     * @return NodeDefinition
222
     */
223 13
    private function getContentBuilderNode()
224
    {
225 13
        $node = $this->root('content_builder');
226
227
        $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...
228 13
            ->defaultValue([])
229 13
            ->useAttributeAsKey('name')
230 13
            ->prototype('variable')
231 13
            ->end()
232
        ;
233
234 13
        return $node;
235
    }
236
237
    /**
238
     * @return \Closure
239
     */
240 13
    private function stringToArray()
241
    {
242
        return function ($value) {
243 2
            return [$value];
244 13
        };
245
    }
246
247
    /**
248
     * @return \Closure
249
     */
250 13
    private function isNotHash()
251
    {
252
        return function ($value) {
253
            if (!is_array($value)) {
254
                return true;
255
            }
256
257
            if (array_values($value) === $value) {
258
                return true;
259
            }
260
261
            return false;
262 13
        };
263
    }
264
265
    /**
266
     * @param string $node
267
     *
268
     * @return \Closure
269
     */
270
    private function nodeRequiredIfEnabled($node)
271
    {
272 13
        return function ($value) use ($node) {
273 6
            if (!$value['enabled']) {
274 1
                return false;
275
            }
276
277 5
            if (isset($value[$node]) && !empty($value[$node])) {
278 5
                return false;
279
            }
280
281
            return true;
282 13
        };
283
    }
284
285
    /**
286
     * @param string $node
287
     * @param string $path
288
     *
289
     * @return string
290
     */
291 13
    private function nodeMustBeConfigured($node, $path)
292
    {
293 13
        return sprintf(
294 13
            'The child node "%s" at path "%s.%s" must be configured.',
295 13
            $node,
296 13
            $this->name,
297
            $path
298 13
        );
299
    }
300
}
301