1515use PHPUnit \Framework \MockObject \MockObject ;
1616use PHPUnit \Framework \TestCase ;
1717use Symfony \Component \PropertyAccess \Exception \InvalidTypeException ;
18+ use Symfony \Component \PropertyAccess \PropertyAccessorBuilder ;
1819use Symfony \Component \PropertyInfo \Extractor \PhpDocExtractor ;
1920use Symfony \Component \PropertyInfo \Extractor \PhpStanExtractor ;
2021use Symfony \Component \PropertyInfo \Extractor \ReflectionExtractor ;
@@ -963,10 +964,26 @@ public function testObjectNormalizerWithAttributeLoaderAndObjectHasStaticPropert
963964 $ this ->assertSame ([], $ normalizer ->normalize ($ class ));
964965 }
965966
966- public function testNormalizeWithMethodNamesSimilarToAccessors ()
967+ // accessors
968+
969+ protected function getNormalizerForAccessors ($ accessorPrefixes = null ): ObjectNormalizer
967970 {
971+ $ accessorPrefixes = $ accessorPrefixes ?? ReflectionExtractor::$ defaultAccessorPrefixes ;
968972 $ classMetadataFactory = new ClassMetadataFactory (new AttributeLoader ());
969- $ normalizer = new ObjectNormalizer ($ classMetadataFactory );
973+ $ propertyAccessorBuilder = (new PropertyAccessorBuilder ())
974+ ->setReadInfoExtractor (
975+ new ReflectionExtractor ([], $ accessorPrefixes , null , false )
976+ );
977+
978+ return new ObjectNormalizer (
979+ $ classMetadataFactory ,
980+ propertyAccessor: $ propertyAccessorBuilder ->getPropertyAccessor (),
981+ );
982+ }
983+
984+ public function testNormalizeWithMethodNamesSimilarToAccessors ()
985+ {
986+ $ normalizer = $ this ->getNormalizerForAccessors ();
970987
971988 $ object = new ObjectWithAccessorishMethods ();
972989 $ normalized = $ normalizer ->normalize ($ object );
@@ -981,19 +998,94 @@ public function testNormalizeWithMethodNamesSimilarToAccessors()
981998 ], $ normalized );
982999 }
9831000
984- public function testNormalizeObjectWithBooleanPropertyAndIsserMethodWithSameName ()
1001+ public function testNormalizeObjectWithPublicPropertyAccessorPrecedence ()
9851002 {
986- $ classMetadataFactory = new ClassMetadataFactory (new AttributeLoader ());
987- $ normalizer = new ObjectNormalizer ($ classMetadataFactory );
1003+ $ normalizer = $ this ->getNormalizerForAccessors ();
9881004
989- $ object = new ObjectWithBooleanPropertyAndIsserWithSameName ();
1005+ $ object = new ObjectWithPropertyAndAllAccessorMethods (
1006+ 'foo ' ,
1007+ );
9901008 $ normalized = $ normalizer ->normalize ($ object );
9911009
1010+ // The getter method should take precedence over all other accessor methods
9921011 $ this ->assertSame ([
9931012 'foo ' => 'foo ' ,
994- 'isFoo ' => true ,
9951013 ], $ normalized );
9961014 }
1015+
1016+ public function testNormalizeObjectWithPropertyAndAccessorMethodsWithSameName ()
1017+ {
1018+ $ normalizer = $ this ->getNormalizerForAccessors ();
1019+
1020+ $ object = new ObjectWithPropertyAndAccessorSameName (
1021+ 'foo ' ,
1022+ 'getFoo ' ,
1023+ 'canFoo ' ,
1024+ 'hasFoo ' ,
1025+ 'isFoo '
1026+ );
1027+ $ normalized = $ normalizer ->normalize ($ object );
1028+
1029+ // Accessor methods with exactly the same name as the property should take precedence
1030+ $ this ->assertSame ([
1031+ 'getFoo ' => 'getFoo ' ,
1032+ 'canFoo ' => 'canFoo ' ,
1033+ 'hasFoo ' => 'hasFoo ' ,
1034+ 'isFoo ' => 'isFoo ' ,
1035+ // The getFoo accessor method is used for foo, thus it's also 'getFoo' instead of 'foo'
1036+ 'foo ' => 'getFoo ' ,
1037+ ], $ normalized );
1038+
1039+ $ denormalized = $ this ->normalizer ->denormalize ($ normalized , ObjectWithPropertyAndAccessorSameName::class);
1040+
1041+ $ this ->assertSame ('getFoo ' , $ denormalized ->getFoo ());
1042+
1043+ // On the initial object the value was 'foo', but the normalizer prefers the accessor method 'getFoo'
1044+ // Thus on the denoramilzed object the value is 'getFoo'
1045+ $ this ->assertSame ('foo ' , $ object ->foo );
1046+ $ this ->assertSame ('getFoo ' , $ denormalized ->foo );
1047+
1048+ $ this ->assertSame ('hasFoo ' , $ denormalized ->hasFoo ());
1049+ $ this ->assertSame ('canFoo ' , $ denormalized ->canFoo ());
1050+ $ this ->assertSame ('isFoo ' , $ denormalized ->isFoo ());
1051+ }
1052+
1053+ /**
1054+ * Priority of accessor methods is defined by the PropertyReadInfoExtractorInterface passed to the PropertyAccessor
1055+ * component. By default ReflectionExtractor::$defaultAccessorPrefixes are used.
1056+ */
1057+ public function testPrecedenceOfAccessorMethods ()
1058+ {
1059+ // by default 'is' comes before 'has'
1060+ $ defaultAccessorPrefixNormalizer = $ this ->getNormalizerForAccessors ();
1061+ $ swappedAccessorPrefixNormalizer = $ this ->getNormalizerForAccessors (['has ' , 'is ' ]);
1062+
1063+ // Nearly equal class, only accessor order is different
1064+ $ isserHasserObject = new ObjectWithPropertyIsserAndHasser ('foo ' );
1065+ $ hasserIsserObject = new ObjectWithPropertyHasserAndIsser ('foo ' );
1066+
1067+ // default precedence (is, has)
1068+ $ normalizedDefaultIsserHasser = $ defaultAccessorPrefixNormalizer ->normalize ($ isserHasserObject );
1069+ $ normalizedDefaultHasserIsser = $ defaultAccessorPrefixNormalizer ->normalize ($ hasserIsserObject );
1070+
1071+ $ this ->assertSame ([
1072+ 'foo ' => 'isFoo ' ,
1073+ ], $ normalizedDefaultIsserHasser );
1074+ $ this ->assertSame ([
1075+ 'foo ' => 'isFoo ' ,
1076+ ], $ normalizedDefaultHasserIsser );
1077+
1078+ // swapped precedence (has, is)
1079+ $ normalizedSwappedIsserHasser = $ swappedAccessorPrefixNormalizer ->normalize ($ isserHasserObject );
1080+ $ normalizedSwappedHasserIsser = $ swappedAccessorPrefixNormalizer ->normalize ($ hasserIsserObject );
1081+
1082+ $ this ->assertSame ([
1083+ 'foo ' => 'hasFoo ' ,
1084+ ], $ normalizedSwappedIsserHasser );
1085+ $ this ->assertSame ([
1086+ 'foo ' => 'hasFoo ' ,
1087+ ], $ normalizedSwappedHasserIsser );
1088+ }
9971089}
9981090
9991091class ProxyObjectDummy extends ObjectDummy
@@ -1337,18 +1429,98 @@ public function isolate()
13371429 }
13381430}
13391431
1340- class ObjectWithBooleanPropertyAndIsserWithSameName
1432+ class ObjectWithPropertyAndAllAccessorMethods
13411433{
1342- private $ foo = 'foo ' ;
1343- private $ isFoo = true ;
1434+ public function __construct (
1435+ private $ foo ,
1436+ ) {
1437+ }
1438+
1439+ public function canFoo ()
1440+ {
1441+ return 'canFoo ' ;
1442+ }
13441443
13451444 public function getFoo ()
13461445 {
13471446 return $ this ->foo ;
13481447 }
13491448
1449+ public function hasFoo ()
1450+ {
1451+ return 'hasFoo ' ;
1452+ }
1453+
1454+ public function isFoo ()
1455+ {
1456+ return 'isFoo ' ;
1457+ }
1458+ }
1459+
1460+ class ObjectWithPropertyAndAccessorSameName
1461+ {
1462+ public function __construct (
1463+ public $ foo ,
1464+ private $ getFoo ,
1465+ private $ canFoo = null ,
1466+ private $ hasFoo = null ,
1467+ private $ isFoo = null ,
1468+ ) {
1469+ }
1470+
1471+ public function getFoo ()
1472+ {
1473+ return $ this ->getFoo ;
1474+ }
1475+
1476+ public function canFoo ()
1477+ {
1478+ return $ this ->canFoo ;
1479+ }
1480+
1481+ public function hasFoo ()
1482+ {
1483+ return $ this ->hasFoo ;
1484+ }
1485+
13501486 public function isFoo ()
13511487 {
13521488 return $ this ->isFoo ;
13531489 }
13541490}
1491+
1492+ class ObjectWithPropertyHasserAndIsser
1493+ {
1494+ public function __construct (
1495+ private $ foo ,
1496+ ) {
1497+ }
1498+
1499+ public function hasFoo ()
1500+ {
1501+ return 'hasFoo ' ;
1502+ }
1503+
1504+ public function isFoo ()
1505+ {
1506+ return 'isFoo ' ;
1507+ }
1508+ }
1509+
1510+ class ObjectWithPropertyIsserAndHasser
1511+ {
1512+ public function __construct (
1513+ private $ foo ,
1514+ ) {
1515+ }
1516+
1517+ public function isFoo ()
1518+ {
1519+ return 'isFoo ' ;
1520+ }
1521+
1522+ public function hasFoo ()
1523+ {
1524+ return 'hasFoo ' ;
1525+ }
1526+ }
0 commit comments