1414use PHPStan \PhpDocParser \Parser \PhpDocParser ;
1515use PHPUnit \Framework \MockObject \MockObject ;
1616use PHPUnit \Framework \TestCase ;
17+ use Symfony \Component \PropertyAccess \PropertyAccessorBuilder ;
1718use Symfony \Component \PropertyInfo \Extractor \PhpDocExtractor ;
1819use Symfony \Component \PropertyInfo \Extractor \PhpStanExtractor ;
1920use Symfony \Component \PropertyInfo \Extractor \ReflectionExtractor ;
@@ -938,10 +939,26 @@ public function testObjectNormalizerWithAttributeLoaderAndObjectHasStaticPropert
938939 $ this ->assertSame ([], $ normalizer ->normalize ($ class ));
939940 }
940941
941- public function testNormalizeWithMethodNamesSimilarToAccessors ()
942+ // accessors
943+
944+ protected function getNormalizerForAccessors ($ accessorPrefixes = null ): ObjectNormalizer
942945 {
946+ $ accessorPrefixes = $ accessorPrefixes ?? ReflectionExtractor::$ defaultAccessorPrefixes ;
943947 $ classMetadataFactory = new ClassMetadataFactory (new AttributeLoader ());
944- $ normalizer = new ObjectNormalizer ($ classMetadataFactory );
948+ $ propertyAccessorBuilder = (new PropertyAccessorBuilder ())
949+ ->setReadInfoExtractor (
950+ new ReflectionExtractor ([], $ accessorPrefixes , null , false )
951+ );
952+
953+ return new ObjectNormalizer (
954+ $ classMetadataFactory ,
955+ propertyAccessor: $ propertyAccessorBuilder ->getPropertyAccessor (),
956+ );
957+ }
958+
959+ public function testNormalizeWithMethodNamesSimilarToAccessors ()
960+ {
961+ $ normalizer = $ this ->getNormalizerForAccessors ();
945962
946963 $ object = new ObjectWithAccessorishMethods ();
947964 $ normalized = $ normalizer ->normalize ($ object );
@@ -956,19 +973,94 @@ public function testNormalizeWithMethodNamesSimilarToAccessors()
956973 ], $ normalized );
957974 }
958975
959- public function testNormalizeObjectWithBooleanPropertyAndIsserMethodWithSameName ()
976+ public function testNormalizeObjectWithPublicPropertyAccessorPrecedence ()
960977 {
961- $ classMetadataFactory = new ClassMetadataFactory (new AttributeLoader ());
962- $ normalizer = new ObjectNormalizer ($ classMetadataFactory );
978+ $ normalizer = $ this ->getNormalizerForAccessors ();
963979
964- $ object = new ObjectWithBooleanPropertyAndIsserWithSameName ();
980+ $ object = new ObjectWithPropertyAndAllAccessorMethods (
981+ 'foo ' ,
982+ );
965983 $ normalized = $ normalizer ->normalize ($ object );
966984
985+ // The getter method should take precedence over all other accessor methods
967986 $ this ->assertSame ([
968987 'foo ' => 'foo ' ,
969- 'isFoo ' => true ,
970988 ], $ normalized );
971989 }
990+
991+ public function testNormalizeObjectWithPropertyAndAccessorMethodsWithSameName ()
992+ {
993+ $ normalizer = $ this ->getNormalizerForAccessors ();
994+
995+ $ object = new ObjectWithPropertyAndAccessorSameName (
996+ 'foo ' ,
997+ 'getFoo ' ,
998+ 'canFoo ' ,
999+ 'hasFoo ' ,
1000+ 'isFoo '
1001+ );
1002+ $ normalized = $ normalizer ->normalize ($ object );
1003+
1004+ // Accessor methods with exactly the same name as the property should take precedence
1005+ $ this ->assertSame ([
1006+ 'getFoo ' => 'getFoo ' ,
1007+ 'canFoo ' => 'canFoo ' ,
1008+ 'hasFoo ' => 'hasFoo ' ,
1009+ 'isFoo ' => 'isFoo ' ,
1010+ // The getFoo accessor method is used for foo, thus it's also 'getFoo' instead of 'foo'
1011+ 'foo ' => 'getFoo ' ,
1012+ ], $ normalized );
1013+
1014+ $ denormalized = $ this ->normalizer ->denormalize ($ normalized , ObjectWithPropertyAndAccessorSameName::class);
1015+
1016+ $ this ->assertSame ('getFoo ' , $ denormalized ->getFoo ());
1017+
1018+ // On the initial object the value was 'foo', but the normalizer prefers the accessor method 'getFoo'
1019+ // Thus on the denoramilzed object the value is 'getFoo'
1020+ $ this ->assertSame ('foo ' , $ object ->foo );
1021+ $ this ->assertSame ('getFoo ' , $ denormalized ->foo );
1022+
1023+ $ this ->assertSame ('hasFoo ' , $ denormalized ->hasFoo ());
1024+ $ this ->assertSame ('canFoo ' , $ denormalized ->canFoo ());
1025+ $ this ->assertSame ('isFoo ' , $ denormalized ->isFoo ());
1026+ }
1027+
1028+ /**
1029+ * Priority of accessor methods is defined by the PropertyReadInfoExtractorInterface passed to the PropertyAccessor
1030+ * component. By default ReflectionExtractor::$defaultAccessorPrefixes are used.
1031+ */
1032+ public function testPrecedenceOfAccessorMethods ()
1033+ {
1034+ // by default 'is' comes before 'has'
1035+ $ defaultAccessorPrefixNormalizer = $ this ->getNormalizerForAccessors ();
1036+ $ swappedAccessorPrefixNormalizer = $ this ->getNormalizerForAccessors (['has ' , 'is ' ]);
1037+
1038+ // Nearly equal class, only accessor order is different
1039+ $ isserHasserObject = new ObjectWithPropertyIsserAndHasser ('foo ' );
1040+ $ hasserIsserObject = new ObjectWithPropertyHasserAndIsser ('foo ' );
1041+
1042+ // default precedence (is, has)
1043+ $ normalizedDefaultIsserHasser = $ defaultAccessorPrefixNormalizer ->normalize ($ isserHasserObject );
1044+ $ normalizedDefaultHasserIsser = $ defaultAccessorPrefixNormalizer ->normalize ($ hasserIsserObject );
1045+
1046+ $ this ->assertSame ([
1047+ 'foo ' => 'isFoo ' ,
1048+ ], $ normalizedDefaultIsserHasser );
1049+ $ this ->assertSame ([
1050+ 'foo ' => 'isFoo ' ,
1051+ ], $ normalizedDefaultHasserIsser );
1052+
1053+ // swapped precedence (has, is)
1054+ $ normalizedSwappedIsserHasser = $ swappedAccessorPrefixNormalizer ->normalize ($ isserHasserObject );
1055+ $ normalizedSwappedHasserIsser = $ swappedAccessorPrefixNormalizer ->normalize ($ hasserIsserObject );
1056+
1057+ $ this ->assertSame ([
1058+ 'foo ' => 'hasFoo ' ,
1059+ ], $ normalizedSwappedIsserHasser );
1060+ $ this ->assertSame ([
1061+ 'foo ' => 'hasFoo ' ,
1062+ ], $ normalizedSwappedHasserIsser );
1063+ }
9721064}
9731065
9741066class ProxyObjectDummy extends ObjectDummy
@@ -1312,18 +1404,98 @@ public function isolate()
13121404 }
13131405}
13141406
1315- class ObjectWithBooleanPropertyAndIsserWithSameName
1407+ class ObjectWithPropertyAndAllAccessorMethods
13161408{
1317- private $ foo = 'foo ' ;
1318- private $ isFoo = true ;
1409+ public function __construct (
1410+ private $ foo ,
1411+ ) {
1412+ }
1413+
1414+ public function canFoo ()
1415+ {
1416+ return 'canFoo ' ;
1417+ }
13191418
13201419 public function getFoo ()
13211420 {
13221421 return $ this ->foo ;
13231422 }
13241423
1424+ public function hasFoo ()
1425+ {
1426+ return 'hasFoo ' ;
1427+ }
1428+
1429+ public function isFoo ()
1430+ {
1431+ return 'isFoo ' ;
1432+ }
1433+ }
1434+
1435+ class ObjectWithPropertyAndAccessorSameName
1436+ {
1437+ public function __construct (
1438+ public $ foo ,
1439+ private $ getFoo ,
1440+ private $ canFoo = null ,
1441+ private $ hasFoo = null ,
1442+ private $ isFoo = null ,
1443+ ) {
1444+ }
1445+
1446+ public function getFoo ()
1447+ {
1448+ return $ this ->getFoo ;
1449+ }
1450+
1451+ public function canFoo ()
1452+ {
1453+ return $ this ->canFoo ;
1454+ }
1455+
1456+ public function hasFoo ()
1457+ {
1458+ return $ this ->hasFoo ;
1459+ }
1460+
13251461 public function isFoo ()
13261462 {
13271463 return $ this ->isFoo ;
13281464 }
13291465}
1466+
1467+ class ObjectWithPropertyHasserAndIsser
1468+ {
1469+ public function __construct (
1470+ private $ foo ,
1471+ ) {
1472+ }
1473+
1474+ public function hasFoo ()
1475+ {
1476+ return 'hasFoo ' ;
1477+ }
1478+
1479+ public function isFoo ()
1480+ {
1481+ return 'isFoo ' ;
1482+ }
1483+ }
1484+
1485+ class ObjectWithPropertyIsserAndHasser
1486+ {
1487+ public function __construct (
1488+ private $ foo ,
1489+ ) {
1490+ }
1491+
1492+ public function isFoo ()
1493+ {
1494+ return 'isFoo ' ;
1495+ }
1496+
1497+ public function hasFoo ()
1498+ {
1499+ return 'hasFoo ' ;
1500+ }
1501+ }
0 commit comments