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])) { |
|
|
|
|
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 && $properties !== null) { |
308
|
3 |
|
$numberOfNeededArguments = count($neededArguments); |
309
|
|
|
|
310
|
3 |
|
MethodCallManager::assertArgsNumber($numberOfNeededArguments, $properties); |
311
|
|
|
|
312
|
3 |
|
for ($i = 0; $i < $numberOfNeededArguments; $i++) { |
313
|
3 |
|
$property = $neededArguments[$i]; |
314
|
3 |
|
$argument = $properties[$i]; |
315
|
|
|
|
316
|
3 |
|
$this->assertPropertyValue($property, $argument); |
317
|
|
|
|
318
|
2 |
|
$this->$property = $argument; |
319
|
|
|
|
320
|
|
|
// Manage associations |
321
|
2 |
View Code Duplication |
if (empty($this->_collectionsItemNames['byProperty'][$property])) { |
|
|
|
|
322
|
2 |
|
$this->updatePropertyAssociation($property, array("oldValue" => null, "newValue" => $argument)); |
323
|
2 |
|
} else { |
324
|
|
|
foreach ($argument as $value) { |
325
|
|
|
$this->updatePropertyAssociation($property, array("oldValue" => null, "newValue" => $value)); |
326
|
|
|
} |
327
|
|
|
} |
328
|
2 |
|
} |
329
|
2 |
|
} |
330
|
28 |
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Get every information needed from this class. |
334
|
|
|
*/ |
335
|
29 |
|
private function getPropertiesInfo() |
336
|
|
|
{ |
337
|
29 |
|
if (!$this->_automatedBehaviorInitialized) { |
338
|
29 |
|
$this->_accessProperties = AccessReader::getAccessProperties($this); |
339
|
29 |
|
$this->_collectionsItemNames = CollectionsReader::getCollectionsItemNames($this); |
340
|
29 |
|
$this->_associationsList = AssociationReader::getAssociations($this); |
341
|
29 |
|
$this->_constraintsValidationEnabled = ConstraintsReader::isConstraintsValidationEnabled($this); |
342
|
|
|
|
343
|
29 |
|
$this->_automatedBehaviorInitialized = true; |
344
|
29 |
|
} |
345
|
29 |
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Extract the info about the method called. |
349
|
|
|
* |
350
|
|
|
* @param string $name |
351
|
|
|
* |
352
|
|
|
* @return array |
353
|
|
|
*/ |
354
|
26 |
|
private function getMethodCallInfo($name) |
355
|
|
|
{ |
356
|
|
|
// check that the called method is a valid method name |
357
|
|
|
// also get the call type and the property to access |
358
|
26 |
|
$callIsValid = preg_match("/(set|get|is|add|remove)([A-Z].*)/", $name, $pregMatches); |
359
|
26 |
|
if (!$callIsValid) { |
360
|
|
|
throw new \BadMethodCallException("Method $name does not exist."); |
361
|
|
|
} |
362
|
|
|
|
363
|
26 |
|
$method = $pregMatches[1]; |
364
|
26 |
|
$property = strtolower(substr($pregMatches[2], 0, 1)) . substr($pregMatches[2], 1); |
365
|
26 |
|
$collectionProperties = null; |
366
|
26 |
|
if (in_array($method, array('add', 'remove'))) { |
367
|
10 |
|
$collectionProperties = $this->_collectionsItemNames['byItemName'][$property]; |
368
|
10 |
|
$property = $collectionProperties['property']; |
369
|
10 |
|
} |
370
|
|
|
|
371
|
|
|
// check that the method is accepted by the targeted property |
372
|
|
|
if ( |
373
|
26 |
|
empty($this->_accessProperties[$property]) |
374
|
24 |
|
|| !in_array($method, $this->_accessProperties[$property]) |
375
|
26 |
|
) { |
376
|
2 |
|
throw new \BadMethodCallException("Method $name does not exist."); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
return array( |
380
|
24 |
|
'method' => $method, |
381
|
24 |
|
'property' => $property, |
382
|
|
|
'collectionProperties' => $collectionProperties |
383
|
24 |
|
); |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
|
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.