Completed
Push — master ( 0f447d...e86479 )
by Antarès
02:55
created

AutomatedBehaviorTrait::__call()   C

Complexity

Conditions 16
Paths 42

Size

Total Lines 78
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 17.5382

Importance

Changes 4
Bugs 2 Features 0
Metric Value
c 4
b 2
f 0
dl 0
loc 78
ccs 45
cts 55
cp 0.8182
rs 5.142
cc 16
eloc 52
nc 42
nop 2
crap 17.5382

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Accessible;
4
5
use \Accessible\MethodManager\MethodCallManager;
6
use \Accessible\MethodManager\CollectionManager;
7
use \Accessible\MethodManager\ListManager;
8
use \Accessible\MethodManager\MapManager;
9
use \Accessible\MethodManager\SetManager;
10
use \Accessible\Reader\AutoConstructReader;
11
use \Accessible\Reader\AccessReader;
12
use \Accessible\Reader\AssociationReader;
13
use \Accessible\Reader\ConstraintsReader;
14
use \Accessible\Reader\CollectionsReader;
15
16
trait AutomatedBehaviorTrait
17
{
18
    /**
19
     * The list of access rights on each property of the object.
20
     *
21
     * @var array
22
     */
23
    private $_accessProperties;
24
25
    /**
26
     * The list of collection properties and their item names.
27
     * Ex: [
28
     *   "byItemName" => "user" => ["property" => "users", "behavior" => "list", "methods" => ["add", "remove"]],
29
     *   "byProperty" => "users" => ["itemName" => "user", "behavior" => "list", "methods" => ["add", "remove"]]
30
     * ]
31
     *
32
     * @var array
33
     */
34
    private $_collectionsItemNames;
35
36
    /**
37
     * The list of associations for each property
38
     * Ex: ["products" => ["property" => "cart", "association" => "inverted"]]
39
     *
40
     * @var array
41
     */
42
    private $_associationsList;
43
44
    /**
45
     * Indicates wether the constraints validation should be enabled or not.
46
     *
47
     * @var boolean
48
     */
49
    private $_constraintsValidationEnabled;
50
51
    /**
52
     * Indicates wether getPropertiesInfo() has been called or not.
53
     *
54
     * @var boolean
55
     */
56
    private $_automatedBehaviorInitialized = false;
57
58
    /**
59
     * Directly calls the initialization method.
60
     */
61 28
    public function __construct()
62
    {
63 28
        $this->initializeProperties(func_get_args());
64 27
    }
65
66
    /**
67
     * Indicates if the properties constraints validation is enabled.
68
     *
69
     * @return boolean
70
     */
71
    public function isPropertiesConstraintsValidationEnabled()
72
    {
73
        return $this->_constraintsValidationEnabled;
74
    }
75
76
    /**
77
     * Enable (or disable) the properties constraints validation.
78
     *
79
     * @param boolean $enabled
80
     */
81 1
    public function setPropertiesConstraintsValidationEnabled($enabled = true)
82
    {
83 1
        $this->_constraintsValidationEnabled = $enabled;
84 1
    }
85
86
    /**
87
     * Disable (or enable) the properties constraints validation.
88
     *
89
     * @param boolean $disabled
90
     */
91 1
    public function setPropertiesConstraintsValidationDisabled($disabled = true)
92
    {
93 1
        $this->_constraintsValidationEnabled = !$disabled;
94 1
    }
95
96
    /**
97
     * This function will be called each time a getter or a setter that is not
98
     * already defined in the class is called.
99
     *
100
     * @param  string $name The name of the called function.
101
     *                      It must be a getter or a setter.
102
     * @param  array  $args The array of arguments for the called function.
103
     *                      It should be empty for a getter call,
104
     *                      and should have one item for a setter call.
105
     *
106
     * @return mixed    The value that should be returned by the function called if it is a getter,
107
     *                  the object itself if the function called is a setter.
108
     *
109
     * @throws \BadMethodCallException      When the method called is neither a getter nor a setter,
110
     *         						   		or if the access right has not be given for this method,
111
     *         						     	or if the method is a setter called without argument.
112
     * @throws \InvalidArgumentException    When the argument given to the method called (as a setter)
113
     *         								does not satisfy the constraints attached to the property
114
     *         								to modify.
115
     */
116 26
    public function __call($name, array $args)
117
    {
118 26
        $this->getPropertiesInfo();
119
120 26
        $methodCallInfo = $this->getMethodCallInfo($name);
121 24
        $method = $methodCallInfo['method'];
122 24
        $property = $methodCallInfo['property'];
123 24
        $collectionProperties = $methodCallInfo['collectionProperties'];
124
125 24
        $valuesToUpdate = array();
126
127
        switch ($method) {
128 24
            case 'get':
129 24
            case 'is':
130 19
                return $this->$property;
131
132 21
            case 'set':
133
                // a setter should have exactly one argument
134 14
                MethodCallManager::assertArgsNumber(1, $args);
135
                // we set a collection here if there is an association with it
136
                if (
137 14
                    !empty($this->_collectionsItemNames['byProperty'][$property])
138 14
                    && !(empty($this->_associationsList[$property]))
139 14
                ) {
140
                    $itemName = $this->_collectionsItemNames['byProperty'][$property]['itemName'];
141
                    $propertyAddMethod = 'add' . strtoupper(substr($itemName, 0, 1)) . substr($itemName, 1);
142
                    $propertyRemoveMethod = 'remove' . strtoupper(substr($itemName, 0, 1)) . substr($itemName, 1);
143
144
                    foreach ($this->$property as $item) {
145
                        $this->$propertyRemoveMethod($item);
146
                    }
147
                    foreach ($args[0] as $item) {
148
                        $this->$propertyAddMethod($item);
149
                    }
150
                }
151
                // we set a regular property here
152
                else {
153 14
                    $oldValue = $this->$property;
154 14
                    $newValue = $args[0];
155
                    $valuesToUpdate = array(
156 14
                        'oldValue' => $oldValue,
157
                        'newValue' => $newValue
158 14
                    );
159
                    // check that the setter argument respects the property constraints
160 14
                    $this->assertPropertyValue($property, $newValue);
161
162 11
                    if ($oldValue !== $newValue) {
163 11
                        $this->$property = $newValue;
164 11
                    }
165
                }
166 11
                break;
167
168 10
            case 'add':
169 10
            case 'remove':
170 10
                $valueToUpdate = ($method === 'add') ? 'newValue' : 'oldValue';
171 10
                switch ($collectionProperties['behavior']) {
172 10
                    case 'list':
173 4
                        ListManager::$method($this->$property, $args);
174 4
                        $valuesToUpdate[$valueToUpdate] = $args[0];
175 4
                        break;
176 6
                    case 'map':
177 2
                        MapManager::$method($this->$property, $args);
178 1
                        break;
179 4
                    case 'set':
180 4
                        SetManager::$method($this->$property, $args);
181 4
                        $valuesToUpdate[$valueToUpdate] = $args[0];
182 4
                        break;
183 9
                }
184 9
                break;
185
        }
186
187
        // manage associations
188 17
        if (in_array($method, array('set', 'add', 'remove'))) {
189 17
            $this->updatePropertyAssociation($property, $valuesToUpdate);
190 17
        }
191
192 17
        return $this;
193
    }
194
195
    /**
196
     * Validates the given value compared to given property constraints.
197
     * If the value is not valid, an InvalidArgumentException will be thrown.
198
     *
199
     * @param  string $property The name of the reference property.
200
     * @param  mixed  $value    The value to check.
201
     *
202
     * @throws \InvalidArgumentException If the value is not valid.
203
     */
204 29
    protected function assertPropertyValue($property, $value)
205
    {
206 29
        $this->getPropertiesInfo();
207
208 29
        if ($this->_constraintsValidationEnabled) {
209 28
            $constraintsViolations = ConstraintsReader::validatePropertyValue($this, $property, $value);
210 28
            if ($constraintsViolations->count()) {
211 5
                $errorMessage = "Argument given is invalid; its constraints validation failed for property $property with the following messages: \"";
212 5
                $errorMessageList = array();
213 5
                foreach ($constraintsViolations as $violation) {
214 5
                    $errorMessageList[] = $violation->getMessage();
215 5
                }
216 5
                $errorMessage .= implode("\", \n\"", $errorMessageList) . "\".";
217
218 5
                throw new \InvalidArgumentException($errorMessage);
219
            }
220 27
        }
221 29
    }
222
223
    /**
224
     * Update the property associated to the given property.
225
     * You can pass the old or the new value given to the property.
226
     *
227
     * @param  string $property The property of the current class to update
228
     * @param  object $values   An array of old a new value under the following form:
229
     *                          ['oldValue' => $oldvalue, 'newValue' => $newValue]
230
     *                          If one of theses values is not given, it will simply not be updated.
231
     */
232 29
    protected function updatePropertyAssociation($property, array $values)
233
    {
234 29
        $this->getPropertiesInfo();
235
236 29
        if ($this->_associationsList === null) {
237
            $this->_associationsList = AssociationReader::getAssociations($this);
238
        }
239
240 29
        $oldValue = empty($values['oldValue']) ? null : $values['oldValue'];
241 29
        $newValue = empty($values['newValue']) ? null : $values['newValue'];
242
243 29
        $association = $this->_associationsList[$property];
244 29
        if (!empty($association)) {
245 7
            $associatedProperty = $association['property'];
246 7
            switch ($association['association']) {
247 7
                case 'inverted':
248 5
                    $invertedGetMethod = 'get' . strtoupper(substr($associatedProperty, 0, 1)) . substr($associatedProperty, 1);
249 5
                    $invertedSetMethod = 'set' . strtoupper(substr($associatedProperty, 0, 1)) . substr($associatedProperty, 1);
250 5
                    if ($oldValue !== null && $oldValue->$invertedGetMethod() === $this) {
251 4
                        $oldValue->$invertedSetMethod(null);
252 4
                    }
253 5
                    if ($newValue !== null && $newValue->$invertedGetMethod() !== $this) {
254 5
                        $newValue->$invertedSetMethod($this);
255 5
                    }
256 5
                    break;
257
258 3
                case 'mapped':
259 3
                    $itemName = $association['itemName'];
260 3
                    $mappedGetMethod = 'get' . strtoupper(substr($associatedProperty, 0, 1)) . substr($associatedProperty, 1);
261 3
                    $mappedAddMethod = 'add' . strtoupper(substr($itemName, 0, 1)) . substr($itemName, 1);
262 3
                    $mappedRemoveMethod = 'remove' . strtoupper(substr($itemName, 0, 1)) . substr($itemName, 1);
263
264 3
                    if ($oldValue !== null && CollectionManager::collectionContains($this, $oldValue->$mappedGetMethod())) {
265 3
                        $oldValue->$mappedRemoveMethod($this);
266 3
                    }
267 3
                    if ($newValue !== null && !CollectionManager::collectionContains($this, $newValue->$mappedGetMethod())) {
268 3
                        $newValue->$mappedAddMethod($this);
269 3
                    }
270 3
                    break;
271 7
            }
272 7
        }
273 29
    }
274
275
    /**
276
     * Initializes the object according to its class specification and given arguments.
277
     *
278
     * @param array $properties The values to give to the properties.
279
     */
280 29
    protected function initializeProperties($properties = null)
281
    {
282 29
        $this->getPropertiesInfo();
283
284
        // Initialize the properties that were defined using the Initialize / InitializeObject annotations
285 29
        $initializeValueValidationEnabled = Configuration::isInitializeValuesValidationEnabled();
286
287 29
        $initialValues = AutoConstructReader::getPropertiesToInitialize($this);
288 29
        foreach ($initialValues as $propertyName => $value) {
289 29
            if ($initializeValueValidationEnabled) {
290 29
                $this->assertPropertyValue($propertyName, $value);
291 29
            }
292
293 29
            $this->$propertyName = $value;
294
295 29 View Code Duplication
            if (empty($this->_collectionsItemNames['byProperty'][$propertyName])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
296 29
                $this->updatePropertyAssociation($propertyName, array("oldValue" => null, "newValue" => $value));
297 29
            } else {
298 29
                foreach ($value as $newValue) {
299 8
                    $this->updatePropertyAssociation($propertyName, array("oldValue" => null, "newValue" => $newValue));
300 29
                }
301
            }
302 29
        }
303
304
        // Initialize the propeties using given arguments
305 29
        $neededArguments = AutoConstructReader::getConstructArguments($this);
306
307 29
        if ($neededArguments !== null) {
308 3
            $givenArguments = $properties;
309 3
            $numberOfNeededArguments = count($neededArguments);
310
311 3
            MethodCallManager::assertArgsNumber($numberOfNeededArguments, $givenArguments);
0 ignored issues
show
Bug introduced by
It seems like $givenArguments defined by $properties on line 308 can also be of type null; however, Accessible\MethodManager...ger::assertArgsNumber() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
312
313 3
            for ($i = 0; $i < $numberOfNeededArguments; $i++) {
314 3
                $property = $neededArguments[$i];
315 3
                $argument = $givenArguments[$i];
316
317 3
                $this->assertPropertyValue($property, $argument);
318
319 2
                $this->$property = $argument;
320
321
                // Manage associations
322 2 View Code Duplication
                if (empty($this->_collectionsItemNames['byProperty'][$property])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
323 2
                    $this->updatePropertyAssociation($property, array("oldValue" => null, "newValue" => $argument));
324 2
                } else {
325
                    foreach ($argument as $value) {
326
                        $this->updatePropertyAssociation($property, array("oldValue" => null, "newValue" => $value));
327
                    }
328
                }
329 2
            }
330 2
        }
331 28
    }
332
333
    /**
334
     * Get every information needed from this class.
335
     */
336 29
    private function getPropertiesInfo()
337
    {
338 29
        if (!$this->_automatedBehaviorInitialized) {
339 29
            $this->_accessProperties = AccessReader::getAccessProperties($this);
340 29
            $this->_collectionsItemNames = CollectionsReader::getCollectionsItemNames($this);
341 29
            $this->_associationsList = AssociationReader::getAssociations($this);
342 29
            $this->_constraintsValidationEnabled = ConstraintsReader::isConstraintsValidationEnabled($this);
343
344 29
            $this->_automatedBehaviorInitialized = true;
345 29
        }
346 29
    }
347
348 26
    private function getMethodCallInfo($name)
349
    {
350
        // check that the called method is a valid method name
351
        // also get the call type and the property to access
352 26
        $callIsValid = preg_match("/(set|get|is|add|remove)([A-Z].*)/", $name, $pregMatches);
353 26
        if (!$callIsValid) {
354
            throw new \BadMethodCallException("Method $name does not exist.");
355
        }
356
357 26
        $method = $pregMatches[1];
358 26
        $property = strtolower(substr($pregMatches[2], 0, 1)) . substr($pregMatches[2], 1);
359 26
        $collectionProperties = null;
360 26
        if (in_array($method, array('add', 'remove'))) {
361 10
            $collectionProperties = $this->_collectionsItemNames['byItemName'][$property];
362 10
            $property = $collectionProperties['property'];
363 10
        }
364
365
        // check that the method is accepted by the targeted property
366
        if (
367 26
            empty($this->_accessProperties[$property])
368 24
            || !in_array($method, $this->_accessProperties[$property])
369 26
        ) {
370 2
            throw new \BadMethodCallException("Method $name does not exist.");
371
        }
372
373
        return array(
374 24
            'method' => $method,
375 24
            'property' => $property,
376
            'collectionProperties' => $collectionProperties
377 24
        );
378
    }
379
}
380