20
20
* Default implementation of {@link PropertyAccessorInterface}.
21
21
*
22
22
* @author Bernhard Schussek <bschussek@gmail.com>
23
+ * @author Kévin Dunglas <dunglas@gmail.com>
23
24
*/
24
25
class PropertyAccessor implements PropertyAccessorInterface
25
26
{
26
27
const VALUE = 0 ;
27
28
const IS_REF = 1 ;
28
29
const IS_REF_CHAINED = 2 ;
30
+ const ACCESS_HAS_PROPERTY = 0 ;
31
+ const ACCESS_TYPE = 1 ;
32
+ const ACCESS_NAME = 2 ;
33
+ const ACCESS_REF = 3 ;
34
+ const ACCESS_ADDER = 4 ;
35
+ const ACCESS_REMOVER = 5 ;
36
+ const ACCESS_TYPE_METHOD = 0 ;
37
+ const ACCESS_TYPE_PROPERTY = 1 ;
38
+ const ACCESS_TYPE_MAGIC = 2 ;
39
+ const ACCESS_TYPE_ADDER_AND_REMOVER = 3 ;
40
+ const ACCESS_TYPE_NOT_FOUND = 4 ;
29
41
30
42
/**
31
43
* @var bool
@@ -37,6 +49,16 @@ class PropertyAccessor implements PropertyAccessorInterface
37
49
*/
38
50
private $ ignoreInvalidIndices ;
39
51
52
+ /**
53
+ * @var array
54
+ */
55
+ private $ readPropertyCache = array ();
56
+
57
+ /**
58
+ * @var array
59
+ */
60
+ private $ writePropertyCache = array ();
61
+
40
62
/**
41
63
* Should not be used by application code. Use
42
64
* {@link PropertyAccess::createPropertyAccessor()} instead.
@@ -78,7 +100,7 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
78
100
self ::IS_REF => true ,
79
101
self ::IS_REF_CHAINED => true ,
80
102
));
81
-
103
+
82
104
$ propertyMaxIndex = count ($ propertyValues ) - 1 ;
83
105
84
106
for ($ i = $ propertyMaxIndex ; $ i >= 0 ; --$ i ) {
@@ -330,51 +352,31 @@ private function &readProperty(&$object, $property)
330
352
throw new NoSuchPropertyException (sprintf ('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead. ' , $ property , $ property ));
331
353
}
332
354
333
- $ camelized = $ this ->camelize ($ property );
334
- $ reflClass = new \ReflectionClass ($ object );
335
- $ getter = 'get ' .$ camelized ;
336
- $ getsetter = lcfirst ($ camelized ); // jQuery style, e.g. read: last(), write: last($item)
337
- $ isser = 'is ' .$ camelized ;
338
- $ hasser = 'has ' .$ camelized ;
339
- $ classHasProperty = $ reflClass ->hasProperty ($ property );
355
+ $ access = $ this ->getReadAccessInfo ($ object , $ property );
340
356
341
- if ($ reflClass ->hasMethod ($ getter ) && $ reflClass ->getMethod ($ getter )->isPublic ()) {
342
- $ result [self ::VALUE ] = $ object ->$ getter ();
343
- } elseif ($ this ->isMethodAccessible ($ reflClass , $ getsetter , 0 )) {
344
- $ result [self ::VALUE ] = $ object ->$ getsetter ();
345
- } elseif ($ reflClass ->hasMethod ($ isser ) && $ reflClass ->getMethod ($ isser )->isPublic ()) {
346
- $ result [self ::VALUE ] = $ object ->$ isser ();
347
- } elseif ($ reflClass ->hasMethod ($ hasser ) && $ reflClass ->getMethod ($ hasser )->isPublic ()) {
348
- $ result [self ::VALUE ] = $ object ->$ hasser ();
349
- } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
350
- $ result [self ::VALUE ] = &$ object ->$ property ;
351
- $ result [self ::IS_REF ] = true ;
352
- } elseif ($ reflClass ->hasMethod ('__get ' ) && $ reflClass ->getMethod ('__get ' )->isPublic ()) {
353
- $ result [self ::VALUE ] = $ object ->$ property ;
354
- } elseif (!$ classHasProperty && property_exists ($ object , $ property )) {
357
+ if (self ::ACCESS_TYPE_METHOD === $ access [self ::ACCESS_TYPE ]) {
358
+ $ result [self ::VALUE ] = $ object ->{$ access [self ::ACCESS_NAME ]}();
359
+ } elseif (self ::ACCESS_TYPE_PROPERTY === $ access [self ::ACCESS_TYPE ]) {
360
+ if ($ access [self ::ACCESS_REF ]) {
361
+ $ result [self ::VALUE ] = &$ object ->{$ access [self ::ACCESS_NAME ]};
362
+ $ result [self ::IS_REF ] = true ;
363
+ } else {
364
+ $ result [self ::VALUE ] = $ object ->{$ access [self ::ACCESS_NAME ]};
365
+ }
366
+ } elseif (!$ access [self ::ACCESS_HAS_PROPERTY ] && property_exists ($ object , $ property )) {
355
367
// Needed to support \stdClass instances. We need to explicitly
356
368
// exclude $classHasProperty, otherwise if in the previous clause
357
369
// a *protected* property was found on the class, property_exists()
358
370
// returns true, consequently the following line will result in a
359
371
// fatal error.
372
+
360
373
$ result [self ::VALUE ] = &$ object ->$ property ;
361
374
$ result [self ::IS_REF ] = true ;
362
- } elseif ($ this -> magicCall && $ reflClass -> hasMethod ( ' __call ' ) && $ reflClass -> getMethod ( ' __call ' )-> isPublic () ) {
375
+ } elseif (self :: ACCESS_TYPE_MAGIC === $ access [ self :: ACCESS_TYPE ] ) {
363
376
// we call the getter and hope the __call do the job
364
- $ result [self ::VALUE ] = $ object ->$ getter ();
377
+ $ result [self ::VALUE ] = $ object ->{ $ access [ self :: ACCESS_NAME ]} ();
365
378
} else {
366
- $ methods = array ($ getter , $ getsetter , $ isser , $ hasser , '__get ' );
367
- if ($ this ->magicCall ) {
368
- $ methods [] = '__call ' ;
369
- }
370
-
371
- throw new NoSuchPropertyException (sprintf (
372
- 'Neither the property "%s" nor one of the methods "%s()" ' .
373
- 'exist and have public access in class "%s". ' ,
374
- $ property ,
375
- implode ('()", " ' , $ methods ),
376
- $ reflClass ->name
377
- ));
379
+ throw new NoSuchPropertyException ($ access [self ::ACCESS_NAME ]);
378
380
}
379
381
380
382
// Objects are always passed around by reference
@@ -385,6 +387,81 @@ private function &readProperty(&$object, $property)
385
387
return $ result ;
386
388
}
387
389
390
+ /**
391
+ * Guesses how to read the property value.
392
+ *
393
+ * @param string $object
394
+ * @param string $property
395
+ *
396
+ * @return array
397
+ */
398
+ private function getReadAccessInfo ($ object , $ property )
399
+ {
400
+ $ key = get_class ($ object ).':: ' .$ property ;
401
+
402
+ if (isset ($ this ->readPropertyCache [$ key ])) {
403
+ $ access = $ this ->readPropertyCache [$ key ];
404
+ } else {
405
+ $ access = array ();
406
+
407
+ $ reflClass = new \ReflectionClass ($ object );
408
+ $ access [self ::ACCESS_HAS_PROPERTY ] = $ reflClass ->hasProperty ($ property );
409
+ $ camelProp = $ this ->camelize ($ property );
410
+ $ getter = 'get ' .$ camelProp ;
411
+ $ getsetter = lcfirst ($ camelProp ); // jQuery style, e.g. read: last(), write: last($item)
412
+ $ isser = 'is ' .$ camelProp ;
413
+ $ hasser = 'has ' .$ camelProp ;
414
+ $ classHasProperty = $ reflClass ->hasProperty ($ property );
415
+
416
+ if ($ reflClass ->hasMethod ($ getter ) && $ reflClass ->getMethod ($ getter )->isPublic ()) {
417
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
418
+ $ access [self ::ACCESS_NAME ] = $ getter ;
419
+ } elseif ($ reflClass ->hasMethod ($ getsetter ) && $ reflClass ->getMethod ($ getsetter )->isPublic ()) {
420
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
421
+ $ access [self ::ACCESS_NAME ] = $ getsetter ;
422
+ } elseif ($ reflClass ->hasMethod ($ isser ) && $ reflClass ->getMethod ($ isser )->isPublic ()) {
423
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
424
+ $ access [self ::ACCESS_NAME ] = $ isser ;
425
+ } elseif ($ reflClass ->hasMethod ($ hasser ) && $ reflClass ->getMethod ($ hasser )->isPublic ()) {
426
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
427
+ $ access [self ::ACCESS_NAME ] = $ hasser ;
428
+ } elseif ($ reflClass ->hasMethod ('__get ' ) && $ reflClass ->getMethod ('__get ' )->isPublic ()) {
429
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
430
+ $ access [self ::ACCESS_NAME ] = $ property ;
431
+ $ access [self ::ACCESS_REF ] = false ;
432
+ } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
433
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
434
+ $ access [self ::ACCESS_NAME ] = $ property ;
435
+ $ access [self ::ACCESS_REF ] = true ;
436
+
437
+ $ result [self ::VALUE ] = &$ object ->$ property ;
438
+ $ result [self ::IS_REF ] = true ;
439
+ } elseif ($ this ->magicCall && $ reflClass ->hasMethod ('__call ' ) && $ reflClass ->getMethod ('__call ' )->isPublic ()) {
440
+ // we call the getter and hope the __call do the job
441
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_MAGIC ;
442
+ $ access [self ::ACCESS_NAME ] = $ getter ;
443
+ } else {
444
+ $ methods = array ($ getter , $ getsetter , $ isser , $ hasser , '__get ' );
445
+ if ($ this ->magicCall ) {
446
+ $ methods [] = '__call ' ;
447
+ }
448
+
449
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_NOT_FOUND ;
450
+ $ access [self ::ACCESS_NAME ] = sprintf (
451
+ 'Neither the property "%s" nor one of the methods "%s()" ' .
452
+ 'exist and have public access in class "%s". ' ,
453
+ $ property ,
454
+ implode ('()", " ' , $ methods ),
455
+ $ reflClass ->name
456
+ );
457
+ }
458
+
459
+ $ this ->readPropertyCache [$ key ] = $ access ;
460
+ }
461
+
462
+ return $ access ;
463
+ }
464
+
388
465
/**
389
466
* Sets the value of an index in a given array-accessible value.
390
467
*
@@ -419,55 +496,26 @@ private function writeProperty(&$object, $property, $value)
419
496
throw new NoSuchPropertyException (sprintf ('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead? ' , $ property , $ property ));
420
497
}
421
498
422
- $ reflClass = new \ReflectionClass ($ object );
423
- $ camelized = $ this ->camelize ($ property );
424
- $ singulars = (array ) StringUtil::singularify ($ camelized );
425
-
426
- if (is_array ($ value ) || $ value instanceof \Traversable) {
427
- $ methods = $ this ->findAdderAndRemover ($ reflClass , $ singulars );
428
-
429
- // Use addXxx() and removeXxx() to write the collection
430
- if (null !== $ methods ) {
431
- $ this ->writeCollection ($ object , $ property , $ value , $ methods [0 ], $ methods [1 ]);
499
+ $ access = $ this ->getWriteAccessInfo ($ object , $ property , $ value );
432
500
433
- return ;
434
- }
435
- }
436
-
437
- $ setter = 'set ' .$ camelized ;
438
- $ getsetter = lcfirst ($ camelized ); // jQuery style, e.g. read: last(), write: last($item)
439
- $ classHasProperty = $ reflClass ->hasProperty ($ property );
440
-
441
- if ($ this ->isMethodAccessible ($ reflClass , $ setter , 1 )) {
442
- $ object ->$ setter ($ value );
443
- } elseif ($ this ->isMethodAccessible ($ reflClass , $ getsetter , 1 )) {
444
- $ object ->$ getsetter ($ value );
445
- } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
446
- $ object ->$ property = $ value ;
447
- } elseif ($ this ->isMethodAccessible ($ reflClass , '__set ' , 2 )) {
448
- $ object ->$ property = $ value ;
449
- } elseif (!$ classHasProperty && property_exists ($ object , $ property )) {
501
+ if (self ::ACCESS_TYPE_METHOD === $ access [self ::ACCESS_TYPE ]) {
502
+ $ object ->{$ access [self ::ACCESS_NAME ]}($ value );
503
+ } elseif (self ::ACCESS_TYPE_PROPERTY === $ access [self ::ACCESS_TYPE ]) {
504
+ $ object ->{$ access [self ::ACCESS_NAME ]} = $ value ;
505
+ } elseif (self ::ACCESS_TYPE_ADDER_AND_REMOVER === $ access [self ::ACCESS_TYPE ]) {
506
+ $ this ->writeCollection ($ object , $ property , $ value , $ access [self ::ACCESS_ADDER ], $ access [self ::ACCESS_REMOVER ]);
507
+ } elseif (!$ access [self ::ACCESS_HAS_PROPERTY ] && property_exists ($ object , $ property )) {
450
508
// Needed to support \stdClass instances. We need to explicitly
451
509
// exclude $classHasProperty, otherwise if in the previous clause
452
510
// a *protected* property was found on the class, property_exists()
453
511
// returns true, consequently the following line will result in a
454
512
// fatal error.
513
+
455
514
$ object ->$ property = $ value ;
456
- } elseif ($ this ->magicCall && $ this ->isMethodAccessible ($ reflClass , '__call ' , 2 )) {
457
- // we call the getter and hope the __call do the job
458
- $ object ->$ setter ($ value );
515
+ } elseif (self ::ACCESS_TYPE_MAGIC === $ access [self ::ACCESS_TYPE ]) {
516
+ $ object ->{$ access [self ::ACCESS_NAME ]}($ value );
459
517
} else {
460
- throw new NoSuchPropertyException (sprintf (
461
- 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", ' .
462
- '"__set()" or "__call()" exist and have public access in class "%s". ' ,
463
- $ property ,
464
- implode ('' , array_map (function ($ singular ) {
465
- return '"add ' .$ singular .'()"/"remove ' .$ singular .'()", ' ;
466
- }, $ singulars )),
467
- $ setter ,
468
- $ getsetter ,
469
- $ reflClass ->name
470
- ));
518
+ throw new NoSuchPropertyException ($ access [self ::ACCESS_NAME ]);
471
519
}
472
520
}
473
521
@@ -519,6 +567,90 @@ private function writeCollection($object, $property, $collection, $addMethod, $r
519
567
}
520
568
}
521
569
570
+ /**
571
+ * Guesses how to write the property value.
572
+ *
573
+ * @param string $object
574
+ * @param string $property
575
+ * @param mixed $value
576
+ *
577
+ * @return array
578
+ */
579
+ private function getWriteAccessInfo ($ object , $ property , $ value )
580
+ {
581
+ $ key = get_class ($ object ).':: ' .$ property ;
582
+ $ guessedAdders = '' ;
583
+
584
+ if (isset ($ this ->writePropertyCache [$ key ])) {
585
+ $ access = $ this ->writePropertyCache [$ key ];
586
+ } else {
587
+ $ access = array ();
588
+
589
+ $ reflClass = new \ReflectionClass ($ object );
590
+ $ access [self ::ACCESS_HAS_PROPERTY ] = $ reflClass ->hasProperty ($ property );
591
+ $ camelized = $ this ->camelize ($ property );
592
+ $ singulars = (array ) StringUtil::singularify ($ camelized );
593
+
594
+ if (is_array ($ value ) || $ value instanceof \Traversable) {
595
+ $ methods = $ this ->findAdderAndRemover ($ reflClass , $ singulars );
596
+
597
+ if (null === $ methods ) {
598
+ // It is sufficient to include only the adders in the error
599
+ // message. If the user implements the adder but not the remover,
600
+ // an exception will be thrown in findAdderAndRemover() that
601
+ // the remover has to be implemented as well.
602
+ $ guessedAdders = '"add ' .implode ('()", "add ' , $ singulars ).'()", ' ;
603
+ } else {
604
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_ADDER_AND_REMOVER ;
605
+ $ access [self ::ACCESS_ADDER ] = $ methods [0 ];
606
+ $ access [self ::ACCESS_REMOVER ] = $ methods [1 ];
607
+ }
608
+ }
609
+
610
+ if (!isset ($ access [self ::ACCESS_TYPE ])) {
611
+ $ setter = 'set ' .$ this ->camelize ($ property );
612
+ $ getsetter = lcfirst ($ camelized ); // jQuery style, e.g. read: last(), write: last($item)
613
+
614
+ $ classHasProperty = $ reflClass ->hasProperty ($ property );
615
+
616
+ if ($ this ->isMethodAccessible ($ reflClass , $ setter , 1 )) {
617
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
618
+ $ access [self ::ACCESS_NAME ] = $ setter ;
619
+ } elseif ($ this ->isMethodAccessible ($ reflClass , $ getsetter , 1 )) {
620
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_METHOD ;
621
+ $ access [self ::ACCESS_NAME ] = $ getsetter ;
622
+ } elseif ($ this ->isMethodAccessible ($ reflClass , '__set ' , 2 )) {
623
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
624
+ $ access [self ::ACCESS_NAME ] = $ property ;
625
+ } elseif ($ classHasProperty && $ reflClass ->getProperty ($ property )->isPublic ()) {
626
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_PROPERTY ;
627
+ $ access [self ::ACCESS_NAME ] = $ property ;
628
+ } elseif ($ this ->magicCall && $ this ->isMethodAccessible ($ reflClass , '__call ' , 2 )) {
629
+ // we call the getter and hope the __call do the job
630
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_MAGIC ;
631
+ $ access [self ::ACCESS_NAME ] = $ setter ;
632
+ } else {
633
+ $ access [self ::ACCESS_TYPE ] = self ::ACCESS_TYPE_NOT_FOUND ;
634
+ $ access [self ::ACCESS_NAME ] = sprintf (
635
+ 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", ' .
636
+ '"__set()" or "__call()" exist and have public access in class "%s". ' ,
637
+ $ property ,
638
+ implode ('' , array_map (function ($ singular ) {
639
+ return '"add ' .$ singular .'()"/"remove ' .$ singular .'()", ' ;
640
+ }, $ singulars )),
641
+ $ setter ,
642
+ $ getsetter ,
643
+ $ reflClass ->name
644
+ );
645
+ }
646
+ }
647
+
648
+ $ this ->writePropertyCache [$ key ] = $ access ;
649
+ }
650
+
651
+ return $ access ;
652
+ }
653
+
522
654
/**
523
655
* Returns whether a property is writable in the given object.
524
656
*
0 commit comments