From c336696a0681214238c6196f71fce81cac42d6f1 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 13 Mar 2019 13:58:13 +0200 Subject: [PATCH] [PropertyAccess] Allow to disable exception on invalid property path when using PropertyAccess::getValue() --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 1 + .../FrameworkExtension.php | 1 + .../Resources/config/property_access.xml | 1 + .../Resources/config/schema/symfony-1.0.xsd | 1 + .../DependencyInjection/ConfigurationTest.php | 1 + .../Fixtures/php/property_accessor.php | 1 + .../Fixtures/xml/property_accessor.xml | 2 +- .../Fixtures/yml/property_accessor.yml | 1 + .../FrameworkExtensionTest.php | 2 + .../Component/PropertyAccess/CHANGELOG.md | 7 ++++ .../PropertyAccess/PropertyAccessor.php | 19 +++++---- .../PropertyAccessorBuilder.php | 40 ++++++++++++++++++- .../Tests/PropertyAccessorTest.php | 30 ++++++++++++++ 14 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 981823fd07f10..79aded380fe3c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -30,6 +30,7 @@ CHANGELOG * Added support for boolean container parameters within routes. * Added the `messenger:setup-transports` command to setup messenger transports * Added a `InMemoryTransport` to Messenger. Use it with a DSN starting with `in-memory://`. + * Added `framework.property_access.throw_exception_on_invalid_property_path` config option. 4.2.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 076a70c818768..698ef88e021f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -903,6 +903,7 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) ->children() ->booleanNode('magic_call')->defaultFalse()->end() ->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end() + ->booleanNode('throw_exception_on_invalid_property_path')->defaultTrue()->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 5ef5ed7b5d269..afca88af956d5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1361,6 +1361,7 @@ private function registerPropertyAccessConfiguration(array $config, ContainerBui ->getDefinition('property_accessor') ->replaceArgument(0, $config['magic_call']) ->replaceArgument(1, $config['throw_exception_on_invalid_index']) + ->replaceArgument(3, $config['throw_exception_on_invalid_property_path']) ; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml index 4fb4f2ff98abc..424f9f682d796 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml @@ -11,6 +11,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 38e60f6516846..5a807403a01ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -233,6 +233,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index aa0a2fc921853..a9d7d0a63bf6c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -249,6 +249,7 @@ protected static function getBundleDefaultConfig() 'property_access' => [ 'magic_call' => false, 'throw_exception_on_invalid_index' => false, + 'throw_exception_on_invalid_property_path' => true, ], 'property_info' => [ 'enabled' => !class_exists(FullStack::class), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php index b5b060c1baa43..8f431f8735d89 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php @@ -4,5 +4,6 @@ 'property_access' => [ 'magic_call' => true, 'throw_exception_on_invalid_index' => true, + 'throw_exception_on_invalid_property_path' => false, ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml index 95ddef8288e38..07e33ae3e8d96 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml @@ -7,6 +7,6 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml index b5fd2718ab112..ea527c9821116 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml @@ -2,3 +2,4 @@ framework: property_access: magic_call: true throw_exception_on_invalid_index: true + throw_exception_on_invalid_property_path: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index acc7fbad156e7..1b8a785a303db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -80,6 +80,7 @@ public function testPropertyAccessWithDefaultValue() $def = $container->getDefinition('property_accessor'); $this->assertFalse($def->getArgument(0)); $this->assertFalse($def->getArgument(1)); + $this->assertTrue($def->getArgument(3)); } public function testPropertyAccessWithOverriddenValues() @@ -88,6 +89,7 @@ public function testPropertyAccessWithOverriddenValues() $def = $container->getDefinition('property_accessor'); $this->assertTrue($def->getArgument(0)); $this->assertTrue($def->getArgument(1)); + $this->assertFalse($def->getArgument(3)); } public function testPropertyAccessCache() diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 970f3545b5702..0a012bb47620d 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +4.3.0 +----- + +* added a `$throwExceptionOnInvalidPropertyPath` argument to the PropertyAccessor constructor. +* added `enableExceptionOnInvalidPropertyPath()`, `disableExceptionOnInvalidPropertyPath()` and + `isExceptionOnInvalidPropertyPath()` methods to `PropertyAccessorBuilder` + 4.0.0 ----- diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index db9c6b84652ea..891cc5e75ea46 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -56,6 +56,7 @@ class PropertyAccessor implements PropertyAccessorInterface */ private $magicCall; private $ignoreInvalidIndices; + private $ignoreInvalidProperty; /** * @var CacheItemPoolInterface @@ -70,11 +71,12 @@ class PropertyAccessor implements PropertyAccessorInterface * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. */ - public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null) + public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true) { $this->magicCall = $magicCall; $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value + $this->ignoreInvalidProperty = !$throwExceptionOnInvalidPropertyPath; } /** @@ -87,7 +89,7 @@ public function getValue($objectOrArray, $propertyPath) ]; if (\is_object($objectOrArray) && false === strpbrk((string) $propertyPath, '.[')) { - return $this->readProperty($zval, $propertyPath)[self::VALUE]; + return $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty)[self::VALUE]; } $propertyPath = $this->getPropertyPath($propertyPath); @@ -313,7 +315,7 @@ private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $zval = $this->readIndex($zval, $property); } else { - $zval = $this->readProperty($zval, $property); + $zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty); } // the final value of the path must not be validated @@ -372,14 +374,15 @@ private function readIndex($zval, $index) /** * Reads the a property from an object. * - * @param array $zval The array containing the object to read from - * @param string $property The property to read + * @param array $zval The array containing the object to read from + * @param string $property The property to read + * @param bool $ignoreInvalidProperty Whether to ignore invalid property or throw an exception * * @return array The array containing the value of the property * - * @throws NoSuchPropertyException if the property does not exist or is not public + * @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public */ - private function readProperty($zval, $property) + private function readProperty($zval, $property, bool $ignoreInvalidProperty = false) { if (!\is_object($zval[self::VALUE])) { throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property)); @@ -411,7 +414,7 @@ private function readProperty($zval, $property) } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { // we call the getter and hope the __call do the job $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); - } else { + } elseif (!$ignoreInvalidProperty) { throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php index 1db6a1dba23ed..a300bdc6f5c36 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -22,6 +22,7 @@ class PropertyAccessorBuilder { private $magicCall = false; private $throwExceptionOnInvalidIndex = false; + private $throwExceptionOnInvalidPropertyPath = true; /** * @var CacheItemPoolInterface|null @@ -97,6 +98,43 @@ public function isExceptionOnInvalidIndexEnabled() return $this->throwExceptionOnInvalidIndex; } + /** + * Enables exceptions when reading a non-existing property. + * + * This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue() + * which are always created on-the-fly. + * + * @return $this + */ + public function enableExceptionOnInvalidPropertyPath() + { + $this->throwExceptionOnInvalidPropertyPath = true; + + return $this; + } + + /** + * Disables exceptions when reading a non-existing index. + * + * Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index. + * + * @return $this + */ + public function disableExceptionOnInvalidPropertyPath() + { + $this->throwExceptionOnInvalidPropertyPath = false; + + return $this; + } + + /** + * @return bool whether an exception is thrown or null is returned when reading a non-existing property + */ + public function isExceptionOnInvalidPropertyPath() + { + return $this->throwExceptionOnInvalidPropertyPath; + } + /** * Sets a cache system. * @@ -128,6 +166,6 @@ public function getCacheItemPool() */ public function getPropertyAccessor() { - return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool); + return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool, $this->throwExceptionOnInvalidPropertyPath); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 2a7dd8a83192b..d0cbccf1ec63c 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; @@ -100,6 +101,16 @@ public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $p $this->propertyAccessor->getValue($objectOrArray, $path); } + /** + * @dataProvider getPathsWithMissingProperty + */ + public function testGetValueReturnsNullIfPropertyNotFoundAndExceptionIsDisabled($objectOrArray, $path) + { + $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->disableExceptionOnInvalidPropertyPath()->getPropertyAccessor(); + + $this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path), $path); + } + /** * @dataProvider getPathsWithMissingIndex */ @@ -618,6 +629,25 @@ public function testAnonymousClassRead() $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); } + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testAnonymousClassReadThrowExceptionOnInvalidPropertyPath() + { + $obj = $this->generateAnonymousClass('bar'); + + $this->propertyAccessor->getValue($obj, 'invalid_property'); + } + + public function testAnonymousClassReadReturnsNullOnInvalidPropertyWithDisabledException() + { + $obj = $this->generateAnonymousClass('bar'); + + $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->disableExceptionOnInvalidPropertyPath()->getPropertyAccessor(); + + $this->assertNull($this->propertyAccessor->getValue($obj, 'invalid_property')); + } + public function testAnonymousClassWrite() { $value = 'bar';