From 2b2fd12b0db33073b62a98e7dda4e7f341493895 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Tue, 17 Mar 2020 22:10:22 +0100 Subject: [PATCH] [PropertyAccess] Added an `UninitializedPropertyException` --- .../Component/PropertyAccess/CHANGELOG.md | 1 + .../UninitializedPropertyException.php | 21 ++++++++++++ .../PropertyAccess/PropertyAccessor.php | 32 +++++++++++++++---- .../Tests/PropertyAccessorTest.php | 22 +++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Exception/UninitializedPropertyException.php diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 7a545752b5e9..f6a167f85911 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 5.1.0 ----- +* Added an `UninitializedPropertyException` * Linking to PropertyInfo extractor to remove a lot of duplicate code 4.4.0 diff --git a/src/Symfony/Component/PropertyAccess/Exception/UninitializedPropertyException.php b/src/Symfony/Component/PropertyAccess/Exception/UninitializedPropertyException.php new file mode 100644 index 000000000000..c0d69735da0f --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/UninitializedPropertyException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property is not initialized. + * + * @author Jules Pietri + */ +class UninitializedPropertyException extends AccessException +{ +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index cbe81bdeb593..79eca586bddc 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -22,6 +22,7 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyReadInfo; use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; @@ -389,14 +390,33 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid $name = $access->getName(); $type = $access->getType(); - if (PropertyReadInfo::TYPE_METHOD === $type) { - $result[self::VALUE] = $object->$name(); - } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { - $result[self::VALUE] = $object->$name; + try { + if (PropertyReadInfo::TYPE_METHOD === $type) { + try { + $result[self::VALUE] = $object->$name(); + } catch (\TypeError $e) { + if (preg_match((sprintf('/^Return value of %s::%s\(\) must be of the type (\w+), null returned$/', preg_quote(\get_class($object)), $name)), $e->getMessage(), $matches)) { + throw new UninitializedPropertyException(sprintf('The method "%s::%s()" returned "null", but expected type "%3$s". Have you forgotten to initialize a property or to make the return type nullable using "?%3$s" instead?', \get_class($object), $name, $matches[1]), 0, $e); + } - if (isset($zval[self::REF]) && $access->canBeReference()) { - $result[self::REF] = &$object->$name; + throw $e; + } + } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { + $result[self::VALUE] = $object->$name; + + if (isset($zval[self::REF]) && $access->canBeReference()) { + $result[self::REF] = &$object->$name; + } } + } catch (\Error $e) { + // handle uninitialized properties in PHP >= 7.4 + if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ([\w\\\]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches)) { + $r = new \ReflectionProperty($matches[1], $matches[2]); + + throw new UninitializedPropertyException(sprintf('The property "%s::$%s" is not readable because it is typed "%3$s". You should either initialize it or make it nullable using "?%3$s" instead.', $r->getDeclaringClass()->getName(), $r->getName(), $r->getType()->getName()), 0, $e); + } + + throw $e; } } elseif ($object instanceof \stdClass && property_exists($object, $property)) { $result[self::VALUE] = $object->$property; diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 70c3b681b76a..2e2aebfe45dc 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\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped; @@ -28,6 +29,8 @@ use Symfony\Component\PropertyAccess\Tests\Fixtures\TestSingularAndPluralProps; use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object; use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted; +use Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedPrivateProperty; +use Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedProperty; class PropertyAccessorTest extends TestCase { @@ -131,6 +134,25 @@ public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnab $this->propertyAccessor->getValue($objectOrArray, $path); } + /** + * @requires PHP 7.4 + */ + public function testGetValueThrowsExceptionIfUninitializedProperty() + { + $this->expectException(UninitializedPropertyException::class); + $this->expectExceptionMessage('The property "Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedProperty::$uninitialized" is not readable because it is typed "string". You should either initialize it or make it nullable using "?string" instead.'); + + $this->propertyAccessor->getValue(new UninitializedProperty(), 'uninitialized'); + } + + public function testGetValueThrowsExceptionIfUninitializedPropertyWithGetter() + { + $this->expectException(UninitializedPropertyException::class); + $this->expectExceptionMessage('The method "Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedPrivateProperty::getUninitialized()" returned "null", but expected type "array". Have you forgotten to initialize a property or to make the return type nullable using "?array" instead?'); + + $this->propertyAccessor->getValue(new UninitializedPrivateProperty(), 'uninitialized'); + } + public function testGetValueThrowsExceptionIfNotArrayAccess() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException');