@@ -23,8 +23,21 @@ class PropertyAccessor implements PropertyAccessorInterface
23
23
{
24
24
const VALUE = 0 ;
25
25
const IS_REF = 1 ;
26
+ const ACCESS_HAS_PROPERTY = 0 ;
27
+ const ACCESS_TYPE = 1 ;
28
+ const ACCESS_NAME = 2 ;
29
+ const ACCESS_REF = 3 ;
30
+ const ACCESS_ADDER = 4 ;
31
+ const ACCESS_REMOVER = 5 ;
32
+ const ACCESS_TYPE_METHOD = 0 ;
33
+ const ACCESS_TYPE_PROPERTY = 1 ;
34
+ const ACCESS_TYPE_MAGIC = 2 ;
35
+ const ACCESS_TYPE_ADDER_AND_REMOVER = 3 ;
36
+ const ACCESS_TYPE_NOT_FOUND = 4 ;
26
37
27
38
private $ magicCall ;
39
+ private $ readPropertyCache = array ();
40
+ private $ writePropertyCache = array ();
28
41
29
42
/**
30
43
* Should not be used by application code. Use
@@ -202,48 +215,31 @@ private function &readProperty(&$object, $property)
202
215
throw new NoSuchPropertyException (sprintf ('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead? ' , $ property , $ property ));
203
216
}
204
217
205
- $ camelProp = $ this ->camelize ($ property );
206
- $ reflClass = new \ReflectionClass ($ object );
207
- $ getter = 'get ' .$ camelProp ;
208
- $ isser = 'is ' .$ camelProp ;
209
- $ hasser = 'has ' .$ camelProp ;
210
- $ classHasProperty = $ reflClass ->hasProperty ($ property );
211
-
212
- if ($ reflClass ->hasMethod ($ getter ) && $ reflClass ->getMethod ($ getter )->isPublic ()) {
213
- $ result [self ::VALUE ] = $ object ->$ getter ();
214
- } elseif ($ reflClass ->hasMethod ($ isser ) && $ reflClass ->getMethod ($ isser )->isPublic ()) {
215
- $ result [self ::VALUE ] = $ object ->$ isser ();
216
- } elseif ($ reflClass ->hasMethod ($ hasser ) && $ reflClass ->getMethod ($ hasser )->isPublic ()) {
217
- $ result [self ::VALUE ] = $ object ->$ hasser ();
218
- } elseif ($ reflClass ->hasMethod ('__get ' ) && $ reflClass ->getMethod ('__get ' )->isPublic ()) {
219
- $ result [self ::VALUE ] = $ object ->$ property ;
220
- } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
221
- $ result [self ::VALUE ] = &$ object ->$ property ;
222
- $ result [self ::IS_REF ] = true ;
223
- } elseif (!$ classHasProperty && property_exists ($ object , $ property )) {
218
+ $ access = $ this ->getReadAccessInfo ($ object , $ property );
219
+
220
+ if (self ::ACCESS_TYPE_METHOD === $ access [self ::ACCESS_TYPE ]) {
221
+ $ result [self ::VALUE ] = $ object ->{$ access [self ::ACCESS_NAME ]}();
222
+ } elseif (self ::ACCESS_TYPE_PROPERTY === $ access [self ::ACCESS_TYPE ]) {
223
+ if ($ access [self ::ACCESS_REF ]) {
224
+ $ result [self ::VALUE ] = &$ object ->{$ access [self ::ACCESS_NAME ]};
225
+ $ result [self ::IS_REF ] = true ;
226
+ } else {
227
+ $ result [self ::VALUE ] = $ object ->{$ access [self ::ACCESS_NAME ]};
228
+ }
229
+ } elseif (!$ access [self ::ACCESS_HAS_PROPERTY ] && property_exists ($ object , $ property )) {
224
230
// Needed to support \stdClass instances. We need to explicitly
225
231
// exclude $classHasProperty, otherwise if in the previous clause
226
232
// a *protected* property was found on the class, property_exists()
227
233
// returns true, consequently the following line will result in a
228
234
// fatal error.
235
+
229
236
$ result [self ::VALUE ] = &$ object ->$ property ;
230
237
$ result [self ::IS_REF ] = true ;
231
- } elseif ($ this -> magicCall && $ reflClass -> hasMethod ( ' __call ' ) && $ reflClass -> getMethod ( ' __call ' )-> isPublic () ) {
238
+ } elseif (self :: ACCESS_TYPE_MAGIC === $ access [ self :: ACCESS_TYPE ] ) {
232
239
// we call the getter and hope the __call do the job
233
- $ result [self ::VALUE ] = $ object ->$ getter ();
240
+ $ result [self ::VALUE ] = $ object ->{ $ access [ self :: ACCESS_NAME ]} ();
234
241
} else {
235
- $ methods = array ($ getter , $ isser , $ hasser , '__get ' );
236
- if ($ this ->magicCall ) {
237
- $ methods [] = '__call ' ;
238
- }
239
-
240
- throw new NoSuchPropertyException (sprintf (
241
- 'Neither the property "%s" nor one of the methods "%s()" ' .
242
- 'exist and have public access in class "%s". ' ,
243
- $ property ,
244
- implode ('()", " ' , $ methods ),
245
- $ reflClass ->name
246
- ));
242
+ throw new NoSuchPropertyException ($ access [self ::ACCESS_NAME ]);
247
243
}
248
244
249
245
// Objects are always passed around by reference
@@ -254,6 +250,77 @@ private function &readProperty(&$object, $property)
254
250
return $ result ;
255
251
}
256
252
253
+ /**
254
+ * Guesses how to read the property value.
255
+ *
256
+ * @param string $object
257
+ * @param string $property
258
+ *
259
+ * @return array
260
+ */
261
+ private function getReadAccessInfo ($ object , $ property )
262
+ {
263
+ $ key = get_class ($ object ).':: ' .$ property ;
264
+
265
+ if (isset ($ this ->readPropertyCache [$ key ])) {
266
+ $ access = $ this ->readPropertyCache [$ key ];
267
+ } else {
268
+ $ access = array ();
269
+
270
+ $ reflClass = new \ReflectionClass ($ object );
271
+ $ access [self ::ACCESS_HAS_PROPERTY ] = $ reflClass ->hasProperty ($ property );
272
+ $ camelProp = $ this ->camelize ($ property );
273
+ $ getter = 'get ' .$ camelProp ;
274
+ $ isser = 'is ' .$ camelProp ;
275
+ $ hasser = 'has ' .$ camelProp ;
276
+ $ classHasProperty = $ reflClass ->hasProperty ($ property );
277
+
278
+ if ($ reflClass ->hasMethod ($ getter ) && $ reflClass ->getMethod ($ getter )->isPublic ()) {
279
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
280
+ $ access [self ::ACCESS_NAME ] = $ getter ;
281
+ } elseif ($ reflClass ->hasMethod ($ isser ) && $ reflClass ->getMethod ($ isser )->isPublic ()) {
282
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
283
+ $ access [self ::ACCESS_NAME ] = $ isser ;
284
+ } elseif ($ reflClass ->hasMethod ($ hasser ) && $ reflClass ->getMethod ($ hasser )->isPublic ()) {
285
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
286
+ $ access [self ::ACCESS_NAME ] = $ hasser ;
287
+ } elseif ($ reflClass ->hasMethod ('__get ' ) && $ reflClass ->getMethod ('__get ' )->isPublic ()) {
288
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
289
+ $ access [self ::ACCESS_NAME ] = $ property ;
290
+ $ access [self ::ACCESS_REF ] = false ;
291
+ } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
292
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
293
+ $ access [self ::ACCESS_NAME ] = $ property ;
294
+ $ access [self ::ACCESS_REF ] = true ;
295
+
296
+ $ result [self ::VALUE ] = &$ object ->$ property ;
297
+ $ result [self ::IS_REF ] = true ;
298
+ } elseif ($ this ->magicCall && $ reflClass ->hasMethod ('__call ' ) && $ reflClass ->getMethod ('__call ' )->isPublic ()) {
299
+ // we call the getter and hope the __call do the job
300
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_MAGIC ;
301
+ $ access [self ::ACCESS_NAME ] = $ getter ;
302
+ } else {
303
+ $ methods = array ($ getter , $ isser , $ hasser , '__get ' );
304
+ if ($ this ->magicCall ) {
305
+ $ methods [] = '__call ' ;
306
+ }
307
+
308
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_NOT_FOUND ;
309
+ $ access [self ::ACCESS_NAME ] = sprintf (
310
+ 'Neither the property "%s" nor one of the methods "%s()" ' .
311
+ 'exist and have public access in class "%s". ' ,
312
+ $ property ,
313
+ implode ('()", " ' , $ methods ),
314
+ $ reflClass ->name
315
+ );
316
+ }
317
+
318
+ $ this ->readPropertyCache [$ key ] = $ access ;
319
+ }
320
+
321
+ return $ access ;
322
+ }
323
+
257
324
/**
258
325
* Sets the value of the property at the given index in the path.
259
326
*
@@ -285,96 +352,143 @@ private function writeIndex(&$array, $index, $value)
285
352
*/
286
353
private function writeProperty (&$ object , $ property , $ singular , $ value )
287
354
{
288
- $ guessedAdders = '' ;
289
-
290
355
if (!is_object ($ object )) {
291
356
throw new NoSuchPropertyException (sprintf ('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead? ' , $ property , $ property ));
292
357
}
293
358
294
- $ reflClass = new \ReflectionClass ($ object );
295
- $ plural = $ this ->camelize ($ property );
296
-
297
- // Any of the two methods is required, but not yet known
298
- $ singulars = null !== $ singular ? array ($ singular ) : (array ) StringUtil::singularify ($ plural );
299
-
300
- if (is_array ($ value ) || $ value instanceof \Traversable) {
301
- $ methods = $ this ->findAdderAndRemover ($ reflClass , $ singulars );
302
-
303
- if (null !== $ methods ) {
304
- // At this point the add and remove methods have been found
305
- // Use iterator_to_array() instead of clone in order to prevent side effects
306
- // see https://github.com/symfony/symfony/issues/4670
307
- $ itemsToAdd = is_object ($ value ) ? iterator_to_array ($ value ) : $ value ;
308
- $ itemToRemove = array ();
309
- $ propertyValue = &$ this ->readProperty ($ object , $ property );
310
- $ previousValue = $ propertyValue [self ::VALUE ];
311
- // remove reference to avoid modifications
312
- unset($ propertyValue );
313
-
314
- if (is_array ($ previousValue ) || $ previousValue instanceof \Traversable) {
315
- foreach ($ previousValue as $ previousItem ) {
316
- foreach ($ value as $ key => $ item ) {
317
- if ($ item === $ previousItem ) {
318
- // Item found, don't add
319
- unset($ itemsToAdd [$ key ]);
320
-
321
- // Next $previousItem
322
- continue 2 ;
323
- }
359
+ $ access = $ this ->getWriteAccessInfo ($ object , $ property , $ singular , $ value );
360
+
361
+ if (self ::ACCESS_TYPE_METHOD === $ access [self ::ACCESS_TYPE ]) {
362
+ $ object ->{$ access [self ::ACCESS_NAME ]}($ value );
363
+ } elseif (self ::ACCESS_TYPE_PROPERTY === $ access [self ::ACCESS_TYPE ]) {
364
+ $ object ->{$ access [self ::ACCESS_NAME ]} = $ value ;
365
+ } elseif (self ::ACCESS_TYPE_ADDER_AND_REMOVER === $ access [self ::ACCESS_TYPE ]) {
366
+ // At this point the add and remove methods have been found
367
+ // Use iterator_to_array() instead of clone in order to prevent side effects
368
+ // see https://github.com/symfony/symfony/issues/4670
369
+ $ itemsToAdd = is_object ($ value ) ? iterator_to_array ($ value ) : $ value ;
370
+ $ itemToRemove = array ();
371
+ $ propertyValue = &$ this ->readProperty ($ object , $ property );
372
+ $ previousValue = $ propertyValue [self ::VALUE ];
373
+ // remove reference to avoid modifications
374
+ unset($ propertyValue );
375
+
376
+ if (is_array ($ previousValue ) || $ previousValue instanceof \Traversable) {
377
+ foreach ($ previousValue as $ previousItem ) {
378
+ foreach ($ value as $ key => $ item ) {
379
+ if ($ item === $ previousItem ) {
380
+ // Item found, don't add
381
+ unset($ itemsToAdd [$ key ]);
382
+
383
+ // Next $previousItem
384
+ continue 2 ;
324
385
}
325
-
326
- // Item not found, add to remove list
327
- $ itemToRemove [] = $ previousItem ;
328
386
}
329
- }
330
-
331
- foreach ($ itemToRemove as $ item ) {
332
- call_user_func (array ($ object , $ methods [1 ]), $ item );
333
- }
334
387
335
- foreach ( $ itemsToAdd as $ item ) {
336
- call_user_func ( array ( $ object , $ methods [ 0 ]), $ item ) ;
388
+ // Item not found, add to remove list
389
+ $ itemToRemove [] = $ previousItem ;
337
390
}
338
-
339
- return ;
340
- } else {
341
- // It is sufficient to include only the adders in the error
342
- // message. If the user implements the adder but not the remover,
343
- // an exception will be thrown in findAdderAndRemover() that
344
- // the remover has to be implemented as well.
345
- $ guessedAdders = '"add ' .implode ('()", "add ' , $ singulars ).'()", ' ;
346
391
}
347
- }
348
392
349
- $ setter = 'set ' .$ this ->camelize ($ property );
350
- $ classHasProperty = $ reflClass ->hasProperty ($ property );
393
+ foreach ($ itemToRemove as $ item ) {
394
+ call_user_func (array ($ object , $ access [self ::ACCESS_REMOVER ]), $ item );
395
+ }
351
396
352
- if ($ reflClass ->hasMethod ($ setter ) && $ reflClass ->getMethod ($ setter )->isPublic ()) {
353
- $ object ->$ setter ($ value );
354
- } elseif ($ reflClass ->hasMethod ('__set ' ) && $ reflClass ->getMethod ('__set ' )->isPublic ()) {
355
- $ object ->$ property = $ value ;
356
- } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
357
- $ object ->$ property = $ value ;
358
- } elseif (!$ classHasProperty && property_exists ($ object , $ property )) {
397
+ foreach ($ itemsToAdd as $ item ) {
398
+ call_user_func (array ($ object , $ access [self ::ACCESS_ADDER ]), $ item );
399
+ }
400
+ } elseif (!$ access [self ::ACCESS_HAS_PROPERTY ] && property_exists ($ object , $ property )) {
359
401
// Needed to support \stdClass instances. We need to explicitly
360
402
// exclude $classHasProperty, otherwise if in the previous clause
361
403
// a *protected* property was found on the class, property_exists()
362
404
// returns true, consequently the following line will result in a
363
405
// fatal error.
364
- $ object ->$ property = $ value ;
365
- } elseif ($ this ->magicCall && $ reflClass ->hasMethod ('__call ' ) && $ reflClass ->getMethod ('__call ' )->isPublic ()) {
366
- // we call the getter and hope the __call do the job
367
- $ object ->$ setter ($ value );
406
+
407
+ $ object ->{$ access [self ::ACCESS_NAME ]} = $ value ;
408
+ } elseif (self ::ACCESS_TYPE_MAGIC === $ access [self ::ACCESS_TYPE ]) {
409
+ $ object ->{$ access [self ::ACCESS_NAME ]}($ value );
410
+ } else {
411
+ throw new NoSuchPropertyException ($ access [self ::ACCESS_NAME ]);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Guesses how to write the property value.
417
+ *
418
+ * @param string $object
419
+ * @param string $property
420
+ * @param string|null $singular
421
+ * @param mixed $value
422
+ *
423
+ * @return array
424
+ */
425
+ private function getWriteAccessInfo ($ object , $ property , $ singular , $ value )
426
+ {
427
+ $ key = get_class ($ object ).':: ' .$ property ;
428
+ $ guessedAdders = '' ;
429
+
430
+ if (isset ($ this ->writePropertyCache [$ key ])) {
431
+ $ access = $ this ->writePropertyCache [$ key ];
368
432
} else {
369
- throw new NoSuchPropertyException (sprintf (
370
- 'Neither the property "%s" nor one of the methods %s"%s()", ' .
371
- '"__set()" or "__call()" exist and have public access in class "%s". ' ,
372
- $ property ,
373
- $ guessedAdders ,
374
- $ setter ,
375
- $ reflClass ->name
376
- ));
433
+ $ access = array ();
434
+
435
+ $ reflClass = new \ReflectionClass ($ object );
436
+ $ access [self ::ACCESS_HAS_PROPERTY ] = $ reflClass ->hasProperty ($ property );
437
+ $ plural = $ this ->camelize ($ property );
438
+
439
+ // Any of the two methods is required, but not yet known
440
+ $ singulars = null !== $ singular ? array ($ singular ) : (array ) StringUtil::singularify ($ plural );
441
+
442
+ if (is_array ($ value ) || $ value instanceof \Traversable) {
443
+ $ methods = $ this ->findAdderAndRemover ($ reflClass , $ singulars );
444
+
445
+ if (null === $ methods ) {
446
+ // It is sufficient to include only the adders in the error
447
+ // message. If the user implements the adder but not the remover,
448
+ // an exception will be thrown in findAdderAndRemover() that
449
+ // the remover has to be implemented as well.
450
+ $ guessedAdders = '"add ' .implode ('()", "add ' , $ singulars ).'()", ' ;
451
+ } else {
452
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_ADDER_AND_REMOVER ;
453
+ $ access [self ::ACCESS_ADDER ] = $ methods [0 ];
454
+ $ access [self ::ACCESS_REMOVER ] = $ methods [1 ];
455
+ }
456
+ }
457
+
458
+ if (!isset ($ access [self ::ACCESS_TYPE ])) {
459
+ $ setter = 'set ' .$ this ->camelize ($ property );
460
+ $ classHasProperty = $ reflClass ->hasProperty ($ property );
461
+
462
+ if ($ reflClass ->hasMethod ($ setter ) && $ reflClass ->getMethod ($ setter )->isPublic ()) {
463
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
464
+ $ access [self ::ACCESS_NAME ] = $ setter ;
465
+ } elseif ($ reflClass ->hasMethod ('__set ' ) && $ reflClass ->getMethod ('__set ' )->isPublic ()) {
466
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
467
+ $ access [self ::ACCESS_NAME ] = $ property ;
468
+ } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
469
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
470
+ $ access [self ::ACCESS_NAME ] = $ property ;
471
+ } elseif ($ this ->magicCall && $ reflClass ->hasMethod ('__call ' ) && $ reflClass ->getMethod ('__call ' )->isPublic ()) {
472
+ // we call the getter and hope the __call do the job
473
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_MAGIC ;
474
+ $ access [self ::ACCESS_NAME ] = $ setter ;
475
+ } else {
476
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_NOT_FOUND ;
477
+ $ access [self ::ACCESS_NAME ] = sprintf (
478
+ 'Neither the property "%s" nor one of the methods %s"%s()", ' .
479
+ '"__set()" or "__call()" exist and have public access in class "%s". ' ,
480
+ $ property ,
481
+ $ guessedAdders ,
482
+ $ setter ,
483
+ $ reflClass ->name
484
+ );
485
+ }
486
+ }
487
+
488
+ $ this ->writePropertyCache [$ key ] = $ access ;
377
489
}
490
+
491
+ return $ access ;
378
492
}
379
493
380
494
/**
0 commit comments