Completed
Push — master ( e1f890...18d83b )
by Yann
06:35 queued 02:28
created

Configuration::stringToArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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