Completed
Push — master ( 456ca5...709ceb )
by Yann
05:58
created

Configuration::getConfigTreeBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 12
cts 12
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 13
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
     * @inheritdoc
17
     */
18 13
    public function getConfigTreeBuilder()
19
    {
20 13
        $builder = $this->getBuilder();
21 13
        $root = $builder->root('yokai_messenger');
22
23
        $root
24 13
            ->addDefaultsIfNotSet()
25 13
            ->fixXmlConfig('message')
26 13
            ->children()
27 13
                ->scalarNode('logging_channel')->defaultValue('app')->end()
28 13
                ->append($this->getContentBuilderNode())
29 13
                ->append($this->getChannelsNode())
30 13
                ->append($this->getMessagesNode())
31 13
            ->end()
32
        ;
33
34 13
        return $builder;
35
    }
36
37
    /**
38
     * @return TreeBuilder
39
     */
40 13
    private function getBuilder()
41
    {
42 13
        return new TreeBuilder();
43
    }
44
45
    /**
46
     * @param string $name
47
     *
48
     * @return ArrayNodeDefinition
49
     */
50 13
    private function root($name)
51
    {
52 13
        return $this->getBuilder()->root($name);
53
    }
54
55
    /**
56
     * @return NodeDefinition
57
     */
58 13
    private function getChannelsNode()
59
    {
60 13
        $node = $this->root('channels');
61
62
        $node
63 13
            ->addDefaultsIfNotSet()
64 13
            ->children()
65 13
                ->append($this->getSwiftmailerChannelNode())
66 13
                ->append($this->getDoctrineChannelNode())
67 13
                ->append($this->getMobileChannelNode())
68 13
            ->end()
69
        ;
70
71 13
        return $node;
72
    }
73
74
    /**
75
     * @return NodeDefinition
76
     */
77 13
    private function getMessagesNode()
78
    {
79 13
        $node = $this->root('messages');
80
81
        $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 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...
82 13
            ->defaultValue([])
83 13
            ->prototype('array')
84 13
                ->fixXmlConfig('channel')
85 13
                ->children()
86 13
                    ->scalarNode('id')->isRequired()->end()
87 13
                    ->arrayNode('channels')
88 13
                        ->beforeNormalization()
89 13
                            ->ifString()->then($this->stringToArray())
90 13
                        ->end()
91 13
                        ->requiresAtLeastOneElement()
92 13
                        ->prototype('scalar')->end()
93 13
                    ->end()
94 13
                    ->arrayNode('defaults')
95 13
                        ->defaultValue([])
96 13
                        ->prototype('variable')->end()
97 13
                    ->end()
98 13
                    ->arrayNode('options')
99 13
                        ->defaultValue([])
100 13
                        ->useAttributeAsKey('channel')
101 13
                        ->prototype('variable')
102 13
                            ->validate()
103 13
                                ->ifTrue($this->isNotHash())
104 13
                                    ->thenInvalid('Expected a hash for channel options, got %s.')
105 13
                            ->end()
106 13
                        ->end()
107 13
                    ->end()
108 13
                ->end()
109 13
            ->end()
110
        ;
111
112 13
        return $node;
113
    }
114
115
    /**
116
     * @return NodeDefinition
117
     */
118 13
    private function getSwiftmailerChannelNode()
119
    {
120 13
        $node = $this->root('swiftmailer');
121
122
        $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 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...
123 13
            ->canBeEnabled()
124 13
            ->validate()
125 13
                ->ifTrue($this->nodeRequiredIfEnabled('from'))
126 13
                    ->thenInvalid($this->nodeMustBeConfigured('from', 'channels.swiftmailer'))
127 13
            ->end()
128 13
            ->children()
129 13
                ->arrayNode('from')
130 13
                    ->normalizeKeys(false)
131 13
                    ->beforeNormalization()
132 13
                        ->ifString()->then($this->stringToArray())
133 13
                    ->end()
134 13
                    ->requiresAtLeastOneElement()
135 13
                    ->prototype('scalar')->end()
136 13
                ->end()
137 13
                ->scalarNode('translator_catalog')->defaultValue('notifications')->end()
138 13
            ->end()
139
        ;
140
141 13
        return $node;
142
    }
143
144
    /**
145
     * @return NodeDefinition
146
     */
147 13
    private function getDoctrineChannelNode()
148
    {
149 13
        $node = $this->root('doctrine');
150
151
        $node
152 13
            ->canBeEnabled()
153 13
            ->children()
154 13
                ->scalarNode('manager')->defaultValue('default')->end()
155 13
            ->end()
156
        ;
157
158 13
        return $node;
159
    }
160
161
    /**
162
     * @return NodeDefinition
163
     */
164 13
    private function getMobileChannelNode()
165
    {
166 13
        $node = $this->root('mobile');
167
168
        $node
169 13
            ->canBeEnabled()
170 13
            ->children()
171 13
                ->scalarNode('environment')->defaultValue('dev')->end()
172 13
                ->arrayNode('apns')
173 13
                    ->canBeDisabled()
174 13
                    ->validate()
175 13
                        ->ifTrue($this->nodeRequiredIfEnabled('certificate'))
176 13
                            ->thenInvalid($this->nodeMustBeConfigured('certificate', 'channels.mobile.apns'))
177 13
                        ->ifTrue($this->nodeRequiredIfEnabled('certificate'))
178 13
                            ->thenInvalid($this->nodeMustBeConfigured('pass_phrase', 'channels.mobile.apns'))
179 13
                    ->end()
180 13
                    ->children()
181 13
                        ->scalarNode('certificate')->defaultNull()->end()
182 13
                        ->scalarNode('pass_phrase')->defaultNull()->end()
183 13
                    ->end()
184 13
                ->end()
185 13
                ->arrayNode('gcm')
186 13
                    ->canBeDisabled()
187 13
                    ->validate()
188 13
                        ->ifTrue($this->nodeRequiredIfEnabled('api_key'))
189 13
                            ->thenInvalid($this->nodeMustBeConfigured('api_key', 'channels.mobile.gcm'))
190 13
                    ->end()
191 13
                    ->children()
192 13
                        ->scalarNode('api_key')->defaultNull()->end()
193 13
                    ->end()
194 13
                ->end()
195 13
            ->end()
196
        ;
197
198 13
        return $node;
199
    }
200
201
    /**
202
     * @return NodeDefinition
203
     */
204 13
    private function getContentBuilderNode()
205
    {
206 13
        $node = $this->root('content_builder');
207
208
        $node
0 ignored issues
show
Unused Code introduced by
The call to the method Symfony\Component\Config...r\NodeDefinition::end() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
209 13
            ->defaultValue([])
210 13
            ->useAttributeAsKey('name')
211 13
            ->prototype('variable')
212 13
            ->end()
213
        ;
214
215 13
        return $node;
216
    }
217
218
    /**
219
     * @return callable
220
     */
221 13
    private function stringToArray()
222
    {
223
        return function ($value) {
224 2
            return [$value];
225 13
        };
226
    }
227
228
    /**
229
     * @return callable
230
     */
231 13
    private function isNotHash()
232
    {
233
        return function ($value) {
234
            if (!is_array($value)) {
235
                return true;
236
            }
237
238
            if (array_values($value) === $value) {
239
                return true;
240
            }
241
242
            return false;
243 13
        };
244
    }
245
246
    /**
247
     * @param string $node
248
     *
249
     * @return callable
250
     */
251
    private function nodeRequiredIfEnabled($node)
252
    {
253 13
        return function ($value) use ($node) {
254 6
            if (!$value['enabled']) {
255 1
                return false;
256
            }
257
258 5
            if (isset($value[$node]) && !empty($value[$node])) {
259 5
                return false;
260
            }
261
262
            return true;
263 13
        };
264
    }
265
266
    /**
267
     * @param string $node
268
     * @param string $path
269
     *
270
     * @return string
271
     */
272 13
    private function nodeMustBeConfigured($node, $path)
273
    {
274 13
        return sprintf(
275 13
            'The child node "%s" at path "yokai_messenger.%s" must be configured.',
276 13
            $node,
277
            $path
278 13
        );
279
    }
280
}
281