diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 970f3545b5702..0966b60148769 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +4.3.0 +----- + + * added `ObjectPropertyAccessorInterface` to read and write object properties. + * `PropertyAccessor` now implements the `ObjectPropertyAccessorInterface` interface. + 4.0.0 ----- diff --git a/src/Symfony/Component/PropertyAccess/ObjectPropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/ObjectPropertyAccessorInterface.php new file mode 100644 index 0000000000000..8b46b367c9e8b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/ObjectPropertyAccessorInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Writes and reads values to/from an object. + * + * @author Fabien Bourigault + */ +interface ObjectPropertyAccessorInterface +{ + /** + * Returns the object property value. + * + * @param object $object The object to get the value from + * @param string $property The property to get the value from + * + * @return mixed The object property value + * + * @throws Exception\AccessException If the property does not exist + */ + public function getPropertyValue($object, string $property); + + /** + * Sets the object property value. + * + * @param object $object The object to modify + * @param string $property The property to modify + * @param mixed $value The value to set in the object property + * + * @throws Exception\InvalidArgumentException If the value is incompatible with property type + * @throws Exception\AccessException If the property does not exist + */ + public function setPropertyValue($object, string $property, $value); +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index f063e8f007438..ec7fa20a070cd 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -31,7 +31,7 @@ * @author Kévin Dunglas * @author Nicolas Grekas */ -class PropertyAccessor implements PropertyAccessorInterface +class PropertyAccessor implements PropertyAccessorInterface, ObjectPropertyAccessorInterface { private const VALUE = 0; private const REF = 1; @@ -816,4 +816,35 @@ public static function createCache($namespace, $defaultLifetime, $version, Logge return $apcu; } + + /** + * {@inheritdoc} + */ + public function getPropertyValue($object, string $property) + { + $zval = [ + self::VALUE => $object, + ]; + + return $this->readProperty($zval, $property)[self::VALUE]; + } + + /** + * {@inheritdoc} + */ + public function setPropertyValue($object, string $property, $value) + { + $zval = [ + self::VALUE => $object, + ]; + + try { + $this->writeProperty($zval, $property, $value); + } catch (\TypeError $e) { + self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $property); + + // It wasn't thrown in this class so rethrow it + throw $e; + } + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 5151489eb0603..a8a5186f7704d 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -85,6 +85,7 @@ public function getPathsWithMissingIndex() /** * @dataProvider getValidPropertyPaths + * @dataProvider getValidObjectProperty */ public function testGetValue($objectOrArray, $path, $value) { @@ -204,6 +205,7 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p /** * @dataProvider getValidPropertyPaths + * @dataProvider getValidObjectProperty */ public function testSetValue($objectOrArray, $path) { @@ -310,6 +312,7 @@ public function testGetValueWhenArrayValueIsNull() /** * @dataProvider getValidPropertyPaths + * @dataProvider getValidObjectProperty */ public function testIsReadable($objectOrArray, $path) { @@ -371,6 +374,7 @@ public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $pa /** * @dataProvider getValidPropertyPaths + * @dataProvider getValidObjectProperty */ public function testIsWritable($objectOrArray, $path) { @@ -430,6 +434,42 @@ public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $pa $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path)); } + /** + * @dataProvider getValidObjectProperty + */ + public function testGetPropertyValue($object, $property, $value) + { + $this->assertSame($value, $this->propertyAccessor->getPropertyValue($object, $property)); + } + + /** + * @dataProvider getPathsWithMissingProperty + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testGetPropertyValueThrowsExceptionIfPropertyNotFound($object, $property) + { + $this->propertyAccessor->getPropertyValue($object, $property); + } + + /** + * @dataProvider getValidObjectProperty + */ + public function testSetPropertyValue($object, $property) + { + $this->propertyAccessor->setPropertyValue($object, $property, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getPropertyValue($object, $property)); + } + + /** + * @dataProvider getPathsWithMissingProperty + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetPropertyValueThrowsExceptionIfPropertyNotFound($object, $property) + { + $this->propertyAccessor->setPropertyValue($object, $property, 'Updated'); + } + public function getValidPropertyPaths() { return [ @@ -442,19 +482,6 @@ public function getValidPropertyPaths() [['index' => (object) ['firstName' => 'Bernhard']], '[index].firstName', 'Bernhard'], [(object) ['property' => (object) ['firstName' => 'Bernhard']], 'property.firstName', 'Bernhard'], - // Accessor methods - [new TestClass('Bernhard'), 'publicProperty', 'Bernhard'], - [new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'], - [new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'], - [new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'], - [new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'], - [new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'], - [new TestClass('Bernhard'), 'publicGetSetter', 'Bernhard'], - - // Methods are camelized - [new TestClass('Bernhard'), 'public_accessor', 'Bernhard'], - [new TestClass('Bernhard'), '_public_accessor', 'Bernhard'], - // Missing indices [['index' => []], '[index][firstName]', null], [['root' => ['index' => []]], '[root][index][firstName]', null], @@ -475,6 +502,24 @@ public function getValidPropertyPaths() ]; } + public function getValidObjectProperty() + { + return [ + // Accessor methods + [new TestClass('Bernhard'), 'publicProperty', 'Bernhard'], + [new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'], + [new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'], + [new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'], + [new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'], + [new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'], + [new TestClass('Bernhard'), 'publicGetSetter', 'Bernhard'], + + // Methods are camelized + [new TestClass('Bernhard'), 'public_accessor', 'Bernhard'], + [new TestClass('Bernhard'), '_public_accessor', 'Bernhard'], + ]; + } + public function testTicket5755() { $object = new Ticket5775Object(); diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 530f168c6a7b5..5789a39a9e789 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -12,9 +12,11 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\ObjectPropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; @@ -32,7 +34,7 @@ class ObjectNormalizer extends AbstractObjectNormalizer private $discriminatorCache = []; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) { if (!\class_exists(PropertyAccess::class)) { throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.'); @@ -40,6 +42,14 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext); + if (null !== $propertyAccessor && !$propertyAccessor instanceof ObjectPropertyAccessorInterface && !$propertyAccessor instanceof PropertyAccessorInterface) { + throw new InvalidArgumentException(sprintf('Argument 3 passed to "%s()" must be an instance of "%s" or an instance of "%s" or null, "%s" given.', __METHOD__, ObjectPropertyAccessorInterface::class, PropertyAccessorInterface::class, \gettype($propertyAccessor))); + } + + if (null !== $propertyAccessor && !$propertyAccessor instanceof ObjectPropertyAccessorInterface) { + @trigger_error(sprintf('Passing an instance of "%s" as the 3rd argument to "%s()" is deprecated since Symfony 4.3. Pass a "%s" instance instead.', PropertyAccessorInterface::class, __METHOD__, ObjectPropertyAccessorInterface::class), E_USER_DEPRECATED); + } + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } @@ -121,7 +131,12 @@ protected function getAttributeValue($object, $attribute, $format = null, array } } - return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute); + return $attribute === $this->discriminatorCache[$cacheKey] + ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) + : ($this->propertyAccessor instanceof ObjectPropertyAccessorInterface + ? $this->propertyAccessor->getPropertyValue($object, $attribute) + : $this->propertyAccessor->getValue($object, $attribute) + ); } /** @@ -130,7 +145,11 @@ protected function getAttributeValue($object, $attribute, $format = null, array protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []) { try { - $this->propertyAccessor->setValue($object, $attribute, $value); + if ($this->propertyAccessor instanceof ObjectPropertyAccessorInterface) { + $this->propertyAccessor->setPropertyValue($object, $attribute, $value); + } else { + $this->propertyAccessor->setValue($object, $attribute, $value); + } } catch (NoSuchPropertyException $exception) { // Properties not found are ignored }