11
11
12
12
namespace Symfony \Component \PropertyAccess ;
13
13
14
+ use Symfony \Component \PropertyAccess \Exception \InvalidArgumentException ;
14
15
use Symfony \Component \PropertyAccess \Exception \NoSuchPropertyException ;
15
16
use Symfony \Component \PropertyAccess \Exception \UnexpectedTypeException ;
16
17
@@ -32,6 +33,11 @@ class PropertyAccessor implements PropertyAccessorInterface
32
33
*/
33
34
const REF = 1 ;
34
35
36
+ /**
37
+ * @internal
38
+ */
39
+ const IS_REF_CHAINED = 2 ;
40
+
35
41
/**
36
42
* @internal
37
43
*/
@@ -87,16 +93,29 @@ class PropertyAccessor implements PropertyAccessorInterface
87
93
*/
88
94
const ACCESS_TYPE_NOT_FOUND = 4 ;
89
95
96
+ /**
97
+ * @var bool
98
+ */
90
99
private $ magicCall ;
100
+
101
+ /**
102
+ * @var array
103
+ */
91
104
private $ readPropertyCache = array ();
105
+
106
+ /**
107
+ * @var array
108
+ */
92
109
private $ writePropertyCache = array ();
93
- private static $ previousErrorHandler ;
110
+ private static $ previousErrorHandler = false ;
94
111
private static $ errorHandler = array (__CLASS__ , 'handleError ' );
95
112
private static $ resultProto = array (self ::VALUE => null );
96
113
97
114
/**
98
115
* Should not be used by application code. Use
99
116
* {@link PropertyAccess::createPropertyAccessor()} instead.
117
+ *
118
+ * @param bool $magicCall
100
119
*/
101
120
public function __construct ($ magicCall = false )
102
121
{
@@ -137,14 +156,25 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
137
156
$ overwrite = true ;
138
157
139
158
try {
140
- if (PHP_VERSION_ID < 70000 ) {
159
+ if (PHP_VERSION_ID < 70000 && false === self :: $ previousErrorHandler ) {
141
160
self ::$ previousErrorHandler = set_error_handler (self ::$ errorHandler );
142
161
}
143
162
144
163
for ($ i = count ($ propertyValues ) - 1 ; 0 <= $ i ; --$ i ) {
145
164
$ zval = $ propertyValues [$ i ];
146
165
unset($ propertyValues [$ i ]);
147
166
167
+ // You only need set value for current element if:
168
+ // 1. it's the parent of the last index element
169
+ // OR
170
+ // 2. its child is not passed by reference
171
+ //
172
+ // This may avoid uncessary value setting process for array elements.
173
+ // For example:
174
+ // '[a][b][c]' => 'old-value'
175
+ // If you want to change its value to 'new-value',
176
+ // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
177
+ //
148
178
if ($ overwrite ) {
149
179
$ property = $ propertyPath ->getElement ($ i );
150
180
@@ -159,22 +189,31 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
159
189
} else {
160
190
$ this ->writeProperty ($ zval , $ property , $ value );
161
191
}
192
+
193
+ // if current element is an object
194
+ // OR
195
+ // if current element's reference chain is not broken - current element
196
+ // as well as all its ancients in the property path are all passed by reference,
197
+ // then there is no need to continue the value setting process
198
+ if (is_object ($ zval [self ::VALUE ]) || isset ($ zval [self ::IS_REF_CHAINED ])) {
199
+ return ;
200
+ }
162
201
}
163
202
164
203
$ value = $ zval [self ::VALUE ];
165
204
}
166
205
} catch (\TypeError $ e ) {
167
206
try {
168
- self ::throwUnexpectedTypeException ($ e ->getMessage (), $ e ->getTrace (), 0 );
169
- } catch (UnexpectedTypeException $ e ) {
207
+ self ::throwInvalidArgumentException ($ e ->getMessage (), $ e ->getTrace (), 0 );
208
+ } catch (InvalidArgumentException $ e ) {
170
209
}
171
210
} catch (\Exception $ e ) {
172
211
} catch (\Throwable $ e ) {
173
212
}
174
213
175
- if (PHP_VERSION_ID < 70000 ) {
214
+ if (PHP_VERSION_ID < 70000 && false !== self :: $ previousErrorHandler ) {
176
215
restore_error_handler ();
177
- self ::$ previousErrorHandler = null ;
216
+ self ::$ previousErrorHandler = false ;
178
217
}
179
218
if (isset ($ e )) {
180
219
throw $ e ;
@@ -187,19 +226,21 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
187
226
public static function handleError ($ type , $ message , $ file , $ line , $ context )
188
227
{
189
228
if (E_RECOVERABLE_ERROR === $ type ) {
190
- self ::throwUnexpectedTypeException ($ message , debug_backtrace (false ), 1 );
229
+ self ::throwInvalidArgumentException ($ message , debug_backtrace (false ), 1 );
191
230
}
192
231
193
232
return null !== self ::$ previousErrorHandler && false !== call_user_func (self ::$ previousErrorHandler , $ type , $ message , $ file , $ line , $ context );
194
233
}
195
234
196
- private static function throwUnexpectedTypeException ($ message , $ trace , $ i )
235
+ private static function throwInvalidArgumentException ($ message , $ trace , $ i )
197
236
{
198
237
if (isset ($ trace [$ i ]['file ' ]) && __FILE__ === $ trace [$ i ]['file ' ]) {
199
238
$ pos = strpos ($ message , $ delim = 'must be of the type ' ) ?: strpos ($ message , $ delim = 'must be an instance of ' );
200
239
$ pos += strlen ($ delim );
240
+ $ type = $ trace [$ i ]['args ' ][0 ];
241
+ $ type = is_object ($ type ) ? get_class ($ type ) : gettype ($ type );
201
242
202
- throw new UnexpectedTypeException ( $ trace [ $ i ][ ' args ' ][ 0 ] , substr ($ message , $ pos , strpos ($ message , ', ' , $ pos ) - $ pos ));
243
+ throw new InvalidArgumentException ( sprintf ( ' Expected argument of type "%s", "%s" given ' , substr ($ message , $ pos , strpos ($ message , ', ' , $ pos ) - $ pos), $ type ));
203
244
}
204
245
}
205
246
@@ -229,14 +270,15 @@ private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath,
229
270
230
271
if ($ isIndex ) {
231
272
// Create missing nested arrays on demand
232
- if ($ i + 1 < $ propertyPath ->getLength () && (
233
- ($ zval [self ::VALUE ] instanceof \ArrayAccess && !$ zval [self ::VALUE ]->offsetExists ($ property )) ||
273
+ if (($ zval [self ::VALUE ] instanceof \ArrayAccess && !$ zval [self ::VALUE ]->offsetExists ($ property )) ||
234
274
(is_array ($ zval [self ::VALUE ]) && !isset ($ zval [self ::VALUE ][$ property ]) && !array_key_exists ($ property , $ zval [self ::VALUE ]))
235
- )) {
236
- $ zval [self ::VALUE ][$ property ] = array ();
275
+ ) {
276
+ if ($ i + 1 < $ propertyPath ->getLength ()) {
277
+ $ zval [self ::VALUE ][$ property ] = array ();
237
278
238
- if (isset ($ zval [self ::REF ])) {
239
- $ zval [self ::REF ] = $ zval [self ::VALUE ];
279
+ if (isset ($ zval [self ::REF ])) {
280
+ $ zval [self ::REF ] = $ zval [self ::VALUE ];
281
+ }
240
282
}
241
283
}
242
284
@@ -250,6 +292,15 @@ private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath,
250
292
throw new UnexpectedTypeException ($ zval [self ::VALUE ], 'object or array ' );
251
293
}
252
294
295
+ if (isset ($ zval [self ::REF ]) && (0 === $ i || isset ($ propertyValues [$ i - 1 ][self ::IS_REF_CHAINED ]))) {
296
+ // Set the IS_REF_CHAINED flag to true if:
297
+ // current property is passed by reference and
298
+ // it is the first element in the property path or
299
+ // the IS_REF_CHAINED flag of its parent element is true
300
+ // Basically, this flag is true only when the reference chain from the top element to current element is not broken
301
+ $ zval [self ::IS_REF_CHAINED ] = true ;
302
+ }
303
+
253
304
$ propertyValues [] = $ zval ;
254
305
}
255
306
@@ -412,7 +463,7 @@ private function getReadAccessInfo($class, $property)
412
463
}
413
464
414
465
/**
415
- * Sets the value of the property at the given index in the path .
466
+ * Sets the value of an index in a given array-accessible value .
416
467
*
417
468
* @param array $zval The array containing the array or \ArrayAccess object to write to
418
469
* @param string|int $index The index to write at
@@ -430,7 +481,7 @@ private function writeIndex($zval, $index, $value)
430
481
}
431
482
432
483
/**
433
- * Sets the value of the property at the given index in the path .
484
+ * Sets the value of a property in the given object .
434
485
*
435
486
* @param array $zval The array containing the object to write to
436
487
* @param string $property The property to write
@@ -452,32 +503,7 @@ private function writeProperty($zval, $property, $value)
452
503
} elseif (self ::ACCESS_TYPE_PROPERTY === $ access [self ::ACCESS_TYPE ]) {
453
504
$ object ->{$ access [self ::ACCESS_NAME ]} = $ value ;
454
505
} elseif (self ::ACCESS_TYPE_ADDER_AND_REMOVER === $ access [self ::ACCESS_TYPE ]) {
455
- // At this point the add and remove methods have been found
456
- $ previousValue = $ this ->readProperty ($ zval , $ property );
457
- $ previousValue = $ previousValue [self ::VALUE ];
458
-
459
- if ($ previousValue instanceof \Traversable) {
460
- $ previousValue = iterator_to_array ($ previousValue );
461
- }
462
- if ($ previousValue && is_array ($ previousValue )) {
463
- if (is_object ($ value )) {
464
- $ value = iterator_to_array ($ value );
465
- }
466
- foreach ($ previousValue as $ key => $ item ) {
467
- if (!in_array ($ item , $ value , true )) {
468
- unset($ previousValue [$ key ]);
469
- $ object ->{$ access [self ::ACCESS_REMOVER ]}($ item );
470
- }
471
- }
472
- } else {
473
- $ previousValue = false ;
474
- }
475
-
476
- foreach ($ value as $ item ) {
477
- if (!$ previousValue || !in_array ($ item , $ previousValue , true )) {
478
- $ object ->{$ access [self ::ACCESS_ADDER ]}($ item );
479
- }
480
- }
506
+ $ this ->writeCollection ($ zval , $ property , $ value , $ access [self ::ACCESS_ADDER ], $ access [self ::ACCESS_REMOVER ]);
481
507
} elseif (!$ access [self ::ACCESS_HAS_PROPERTY ] && property_exists ($ object , $ property )) {
482
508
// Needed to support \stdClass instances. We need to explicitly
483
509
// exclude $classHasProperty, otherwise if in the previous clause
@@ -493,6 +519,45 @@ private function writeProperty($zval, $property, $value)
493
519
}
494
520
}
495
521
522
+ /**
523
+ * Adjusts a collection-valued property by calling add*() and remove*() methods.
524
+ *
525
+ * @param array $zval The array containing the object to write to
526
+ * @param string $property The property to write
527
+ * @param array|\Traversable $collection The collection to write
528
+ * @param string $addMethod The add*() method
529
+ * @param string $removeMethod The remove*() method
530
+ */
531
+ private function writeCollection ($ zval , $ property , $ collection , $ addMethod , $ removeMethod )
532
+ {
533
+ // At this point the add and remove methods have been found
534
+ $ previousValue = $ this ->readProperty ($ zval , $ property );
535
+ $ previousValue = $ previousValue [self ::VALUE ];
536
+
537
+ if ($ previousValue instanceof \Traversable) {
538
+ $ previousValue = iterator_to_array ($ previousValue );
539
+ }
540
+ if ($ previousValue && is_array ($ previousValue )) {
541
+ if (is_object ($ collection )) {
542
+ $ collection = iterator_to_array ($ collection );
543
+ }
544
+ foreach ($ previousValue as $ key => $ item ) {
545
+ if (!in_array ($ item , $ collection , true )) {
546
+ unset($ previousValue [$ key ]);
547
+ $ zval [self ::VALUE ]->{$ removeMethod }($ item );
548
+ }
549
+ }
550
+ } else {
551
+ $ previousValue = false ;
552
+ }
553
+
554
+ foreach ($ collection as $ item ) {
555
+ if (!$ previousValue || !in_array ($ item , $ previousValue , true )) {
556
+ $ zval [self ::VALUE ]->{$ addMethod }($ item );
557
+ }
558
+ }
559
+ }
560
+
496
561
/**
497
562
* Guesses how to write the property value.
498
563
*
@@ -618,7 +683,7 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula
618
683
}
619
684
620
685
/**
621
- * Returns whether a method is public and has a specific number of required parameters.
686
+ * Returns whether a method is public and has the number of required parameters.
622
687
*
623
688
* @param \ReflectionClass $class The class of the method
624
689
* @param string $methodName The method name
0 commit comments