Skip to content

Commit a355742

Browse files
committed
Merge branch '3.4' into 4.4
* 3.4: Fix more quotes in exception messages [3.4] Minor fixes [PropertyAccess] Improved errors when reading uninitialized properties
2 parents 34cb00a + 494c9bb commit a355742

File tree

4 files changed

+103
-20
lines changed

4 files changed

+103
-20
lines changed

PropertyAccessor.php

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -378,30 +378,50 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
378378
$object = $zval[self::VALUE];
379379
$access = $this->getReadAccessInfo(\get_class($object), $property);
380380

381-
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
382-
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
383-
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
384-
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
381+
try {
382+
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
383+
try {
384+
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
385+
} catch (\TypeError $e) {
386+
// handle uninitialized properties in PHP >= 7
387+
if (preg_match((sprintf('/^Return value of %s::%s\(\) must be of the type (\w+), null returned$/', preg_quote(\get_class($object)), $access[self::ACCESS_NAME])), $e->getMessage(), $matches)) {
388+
throw new AccessException(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), $access[self::ACCESS_NAME], $matches[1]), 0, $e);
389+
}
390+
391+
throw $e;
392+
}
393+
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
394+
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
385395

386-
if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
387-
$result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
396+
if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
397+
$result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
398+
}
399+
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
400+
// Needed to support \stdClass instances. We need to explicitly
401+
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
402+
// a *protected* property was found on the class, property_exists()
403+
// returns true, consequently the following line will result in a
404+
// fatal error.
405+
406+
$result[self::VALUE] = $object->$property;
407+
if (isset($zval[self::REF])) {
408+
$result[self::REF] = &$object->$property;
409+
}
410+
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
411+
// we call the getter and hope the __call do the job
412+
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
413+
} elseif (!$ignoreInvalidProperty) {
414+
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
388415
}
389-
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
390-
// Needed to support \stdClass instances. We need to explicitly
391-
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
392-
// a *protected* property was found on the class, property_exists()
393-
// returns true, consequently the following line will result in a
394-
// fatal error.
416+
} catch (\Error $e) {
417+
// handle uninitialized properties in PHP >= 7.4
418+
if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ([\w\\\]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches)) {
419+
$r = new \ReflectionProperty($matches[1], $matches[2]);
395420

396-
$result[self::VALUE] = $object->$property;
397-
if (isset($zval[self::REF])) {
398-
$result[self::REF] = &$object->$property;
421+
throw new AccessException(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);
399422
}
400-
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
401-
// we call the getter and hope the __call do the job
402-
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
403-
} elseif (!$ignoreInvalidProperty) {
404-
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
423+
424+
throw $e;
405425
}
406426

407427
// Objects are always passed around by reference
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
13+
14+
class UninitializedPrivateProperty
15+
{
16+
private $uninitialized;
17+
18+
public function getUninitialized(): array
19+
{
20+
return $this->uninitialized;
21+
}
22+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
13+
14+
class UninitializedProperty
15+
{
16+
public string $uninitialized;
17+
}

Tests/PropertyAccessorTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestSingularAndPluralProps;
2929
use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object;
3030
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
31+
use Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedPrivateProperty;
32+
use Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedProperty;
3133

3234
class PropertyAccessorTest extends TestCase
3335
{
@@ -131,6 +133,28 @@ public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnab
131133
$this->propertyAccessor->getValue($objectOrArray, $path);
132134
}
133135

136+
/**
137+
* @requires PHP 7.4
138+
*/
139+
public function testGetValueThrowsExceptionIfUninitializedProperty()
140+
{
141+
$this->expectException('Symfony\Component\PropertyAccess\Exception\AccessException');
142+
$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.');
143+
144+
$this->propertyAccessor->getValue(new UninitializedProperty(), 'uninitialized');
145+
}
146+
147+
/**
148+
* @requires PHP 7
149+
*/
150+
public function testGetValueThrowsExceptionIfUninitializedPropertyWithGetter()
151+
{
152+
$this->expectException('Symfony\Component\PropertyAccess\Exception\AccessException');
153+
$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?');
154+
155+
$this->propertyAccessor->getValue(new UninitializedPrivateProperty(), 'uninitialized');
156+
}
157+
134158
public function testGetValueThrowsExceptionIfNotArrayAccess()
135159
{
136160
$this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException');

0 commit comments

Comments
 (0)