Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-6.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ PropertyAccess
--------------

* Deprecate calling `PropertyAccessorBuilder::setCacheItemPool()` without arguments
* Implementing the `PropertyPathInterface` without implementing the `isNullSafe()` method is deprecated

Security
--------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ public function isIndex(int $index): bool
return $this->isIndex[$index];
}

public function isNullSafe(int $index): bool
{
return false;
}

/**
* Returns whether an element maps directly to a form.
*
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/PropertyAccess/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Deprecate calling `PropertyAccessorBuilder::setCacheItemPool()` without arguments
* Added method `isNullSafe()` to `PropertyPathInterface`

6.0
---
Expand Down
18 changes: 15 additions & 3 deletions src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert
$property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i);

$isNullSafe = false;
if (method_exists($propertyPath, 'isNullSafe')) {
// To be removed in symfony 7 once we are sure isNullSafe is always implemented.
$isNullSafe = $propertyPath->isNullSafe($i);
} else {
trigger_deprecation('symfony/property-access', '6.2', 'The "%s()" method in class "%s" needs to be implemented in version 7.0, not defining it is deprecated.', 'isNullSafe', PropertyPathInterface::class);
}

if ($isIndex) {
// Create missing nested arrays on demand
if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
Expand Down Expand Up @@ -316,12 +324,14 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert
}

$zval = $this->readIndex($zval, $property);
} elseif ($isNullSafe && !\is_object($zval[self::VALUE])) {
$zval[self::VALUE] = null;
} else {
$zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty);
$zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty, $isNullSafe);
}

// the final value of the path must not be validated
if ($i + 1 < $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
if ($i + 1 < $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE]) && !$isNullSafe) {
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
}

Expand Down Expand Up @@ -373,7 +383,7 @@ private function readIndex(array $zval, string|int $index): array
*
* @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public
*/
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false): array
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false, bool $isNullSafe = false): array
{
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));
Expand Down Expand Up @@ -433,6 +443,8 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
if (isset($zval[self::REF])) {
$result[self::REF] = &$object->$property;
}
} elseif ($isNullSafe) {
$result[self::VALUE] = null;
} elseif (!$ignoreInvalidProperty) {
throw new NoSuchPropertyException(sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class));
}
Expand Down
29 changes: 28 additions & 1 deletion src/Symfony/Component/PropertyAccess/PropertyPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,18 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
* Contains a Boolean for each property in $elements denoting whether this
* element is an index. It is a property otherwise.
*
* @var array
* @var array<bool>
*/
private $isIndex = [];

/**
* Contains a Boolean for each property in $elements denoting whether this
* element is optional or not.
*
* @var array<bool>
*/
private $isNullSafe = [];

/**
* String representation of the path.
*
Expand All @@ -72,6 +80,7 @@ public function __construct(self|string $propertyPath)
$this->elements = $propertyPath->elements;
$this->length = $propertyPath->length;
$this->isIndex = $propertyPath->isIndex;
$this->isNullSafe = $propertyPath->isNullSafe;
$this->pathAsString = $propertyPath->pathAsString;

return;
Expand All @@ -97,6 +106,14 @@ public function __construct(self|string $propertyPath)
$this->isIndex[] = true;
}

// Mark as optional when last character is "?".
if (str_ends_with($element, '?')) {
$this->isNullSafe[] = true;
$element = substr($element, 0, -1);
} else {
$this->isNullSafe[] = false;
}

$this->elements[] = $element;

$position += \strlen($matches[1]);
Expand Down Expand Up @@ -133,6 +150,7 @@ public function getParent(): ?PropertyPathInterface
$parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '[')));
array_pop($parent->elements);
array_pop($parent->isIndex);
array_pop($parent->isNullSafe);

return $parent;
}
Expand Down Expand Up @@ -176,4 +194,13 @@ public function isIndex(int $index): bool

return $this->isIndex[$index];
}

public function isNullSafe(int $index): bool
{
if (!isset($this->isNullSafe[$index])) {
throw new OutOfBoundsException(sprintf('The index "%s" is not within the property path.', $index));
}

return $this->isNullSafe[$index];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @method bool isNullSafe(int $index) Returns whether the element at the given index is null safe. Not implementing it is deprecated since Symfony 6.2
*
* @extends \Traversable<int, string>
*/
interface PropertyPathInterface extends \Traversable
Expand Down
35 changes: 20 additions & 15 deletions src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\Tests\Fixtures\ExtendedUninitializedProperty;
use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength;
Expand All @@ -41,10 +42,7 @@

class PropertyAccessorTest extends TestCase
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;
private PropertyAccessorInterface $propertyAccessor;

protected function setUp(): void
{
Expand Down Expand Up @@ -83,7 +81,7 @@ public function getPathsWithMissingIndex()
}

/**
* @dataProvider getValidPropertyPaths
* @dataProvider getValidReadPropertyPaths
*/
public function testGetValue($objectOrArray, $path, $value)
{
Expand Down Expand Up @@ -312,7 +310,7 @@ public function testGetValueReadsMagicCallThatReturnsConstant()
}

/**
* @dataProvider getValidPropertyPaths
* @dataProvider getValidWritePropertyPaths
*/
public function testSetValue($objectOrArray, $path)
{
Expand Down Expand Up @@ -412,7 +410,7 @@ public function testGetValueWhenArrayValueIsNull()
}

/**
* @dataProvider getValidPropertyPaths
* @dataProvider getValidReadPropertyPaths
*/
public function testIsReadable($objectOrArray, $path)
{
Expand Down Expand Up @@ -465,7 +463,7 @@ public function testIsReadableRecognizesMagicCallIfEnabled()
}

/**
* @dataProvider getValidPropertyPaths
* @dataProvider getValidWritePropertyPaths
*/
public function testIsWritable($objectOrArray, $path)
{
Expand Down Expand Up @@ -517,7 +515,7 @@ public function testIsWritableRecognizesMagicCallIfEnabled()
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}

public function getValidPropertyPaths()
public function getValidWritePropertyPaths()
{
return [
[['Bernhard', 'Schussek'], '[0]', 'Bernhard'],
Expand Down Expand Up @@ -563,6 +561,19 @@ public function getValidPropertyPaths()
];
}

public function getValidReadPropertyPaths()
{
$testCases = $this->getValidWritePropertyPaths();

// Optional paths can only be read and can't be written to.
$testCases[] = [(object) ['foo' => (object) ['firstName' => 'Bernhard']], 'foo.bar?', null];
$testCases[] = [(object) ['foo' => (object) ['firstName' => 'Bernhard']], 'foo.bar?.baz?', null];
$testCases[] = [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?]', null];
$testCases[] = [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?][baz?]', null];

return $testCases;
}

public function testTicket5755()
{
$object = new Ticket5775Object();
Expand Down Expand Up @@ -738,17 +749,11 @@ public function __construct($foo)
$this->foo = $foo;
}

/**
* @return mixed
*/
public function getFoo()
{
return $this->foo;
}

/**
* @param mixed $foo
*/
public function setFoo($foo)
{
$this->foo = $foo;
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/PropertyAccess/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
],
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/property-info": "^5.4|^6.0"
},
"require-dev": {
Expand Down