Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6455feb

Browse files
committed
feature #34483 - Allow optional property accesses
1 parent e6d6bed commit 6455feb

File tree

8 files changed

+73
-19
lines changed

8 files changed

+73
-19
lines changed

UPGRADE-6.2.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ PropertyAccess
6565
--------------
6666

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

6970
Security
7071
--------

src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ public function isIndex(int $index): bool
156156
return $this->isIndex[$index];
157157
}
158158

159+
public function isNullSafe(int $index): bool
160+
{
161+
return false;
162+
}
163+
159164
/**
160165
* Returns whether an element maps directly to a form.
161166
*

src/Symfony/Component/PropertyAccess/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

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

910
6.0
1011
---

src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert
288288
$property = $propertyPath->getElement($i);
289289
$isIndex = $propertyPath->isIndex($i);
290290

291+
$isNullSafe = false;
292+
if (method_exists($propertyPath, 'isNullSafe')) {
293+
// To be removed in symfony 7 once we are sure isNullSafe is always implemented.
294+
$isNullSafe = $propertyPath->isNullSafe($i);
295+
} else {
296+
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);
297+
}
298+
291299
if ($isIndex) {
292300
// Create missing nested arrays on demand
293301
if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
@@ -316,12 +324,14 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert
316324
}
317325

318326
$zval = $this->readIndex($zval, $property);
327+
} elseif ($isNullSafe && !\is_object($zval[self::VALUE])) {
328+
$zval[self::VALUE] = null;
319329
} else {
320-
$zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty);
330+
$zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty, $isNullSafe);
321331
}
322332

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

@@ -373,7 +383,7 @@ private function readIndex(array $zval, string|int $index): array
373383
*
374384
* @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public
375385
*/
376-
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false): array
386+
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false, bool $isNullSafe = false): array
377387
{
378388
if (!\is_object($zval[self::VALUE])) {
379389
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property));
@@ -433,6 +443,8 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
433443
if (isset($zval[self::REF])) {
434444
$result[self::REF] = &$object->$property;
435445
}
446+
} elseif ($isNullSafe) {
447+
$result[self::VALUE] = null;
436448
} elseif (!$ignoreInvalidProperty) {
437449
throw new NoSuchPropertyException(sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class));
438450
}

src/Symfony/Component/PropertyAccess/PropertyPath.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,18 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
4747
* Contains a Boolean for each property in $elements denoting whether this
4848
* element is an index. It is a property otherwise.
4949
*
50-
* @var array
50+
* @var array<bool>
5151
*/
5252
private $isIndex = [];
5353

54+
/**
55+
* Contains a Boolean for each property in $elements denoting whether this
56+
* element is optional or not.
57+
*
58+
* @var array<bool>
59+
*/
60+
private $isNullSafe = [];
61+
5462
/**
5563
* String representation of the path.
5664
*
@@ -72,6 +80,7 @@ public function __construct(self|string $propertyPath)
7280
$this->elements = $propertyPath->elements;
7381
$this->length = $propertyPath->length;
7482
$this->isIndex = $propertyPath->isIndex;
83+
$this->isNullSafe = $propertyPath->isNullSafe;
7584
$this->pathAsString = $propertyPath->pathAsString;
7685

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

109+
// Mark as optional when last character is "?".
110+
if (str_ends_with($element, '?')) {
111+
$this->isNullSafe[] = true;
112+
$element = substr($element, 0, -1);
113+
} else {
114+
$this->isNullSafe[] = false;
115+
}
116+
100117
$this->elements[] = $element;
101118

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

137155
return $parent;
138156
}
@@ -176,4 +194,13 @@ public function isIndex(int $index): bool
176194

177195
return $this->isIndex[$index];
178196
}
197+
198+
public function isNullSafe(int $index): bool
199+
{
200+
if (!isset($this->isNullSafe[$index])) {
201+
throw new OutOfBoundsException(sprintf('The index "%s" is not within the property path.', $index));
202+
}
203+
204+
return $this->isNullSafe[$index];
205+
}
179206
}

src/Symfony/Component/PropertyAccess/PropertyPathInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*
1717
* @author Bernhard Schussek <[email protected]>
1818
*
19+
* @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
20+
*
1921
* @extends \Traversable<int, string>
2022
*/
2123
interface PropertyPathInterface extends \Traversable

src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
2020
use Symfony\Component\PropertyAccess\PropertyAccess;
2121
use Symfony\Component\PropertyAccess\PropertyAccessor;
22+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2223
use Symfony\Component\PropertyAccess\Tests\Fixtures\ExtendedUninitializedProperty;
2324
use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped;
2425
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength;
@@ -41,10 +42,7 @@
4142

4243
class PropertyAccessorTest extends TestCase
4344
{
44-
/**
45-
* @var PropertyAccessor
46-
*/
47-
private $propertyAccessor;
45+
private PropertyAccessorInterface $propertyAccessor;
4846

4947
protected function setUp(): void
5048
{
@@ -83,7 +81,7 @@ public function getPathsWithMissingIndex()
8381
}
8482

8583
/**
86-
* @dataProvider getValidPropertyPaths
84+
* @dataProvider getValidReadPropertyPaths
8785
*/
8886
public function testGetValue($objectOrArray, $path, $value)
8987
{
@@ -312,7 +310,7 @@ public function testGetValueReadsMagicCallThatReturnsConstant()
312310
}
313311

314312
/**
315-
* @dataProvider getValidPropertyPaths
313+
* @dataProvider getValidWritePropertyPaths
316314
*/
317315
public function testSetValue($objectOrArray, $path)
318316
{
@@ -412,7 +410,7 @@ public function testGetValueWhenArrayValueIsNull()
412410
}
413411

414412
/**
415-
* @dataProvider getValidPropertyPaths
413+
* @dataProvider getValidReadPropertyPaths
416414
*/
417415
public function testIsReadable($objectOrArray, $path)
418416
{
@@ -465,7 +463,7 @@ public function testIsReadableRecognizesMagicCallIfEnabled()
465463
}
466464

467465
/**
468-
* @dataProvider getValidPropertyPaths
466+
* @dataProvider getValidWritePropertyPaths
469467
*/
470468
public function testIsWritable($objectOrArray, $path)
471469
{
@@ -517,7 +515,7 @@ public function testIsWritableRecognizesMagicCallIfEnabled()
517515
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
518516
}
519517

520-
public function getValidPropertyPaths()
518+
public function getValidWritePropertyPaths()
521519
{
522520
return [
523521
[['Bernhard', 'Schussek'], '[0]', 'Bernhard'],
@@ -563,6 +561,19 @@ public function getValidPropertyPaths()
563561
];
564562
}
565563

564+
public function getValidReadPropertyPaths()
565+
{
566+
$testCases = $this->getValidWritePropertyPaths();
567+
568+
// Optional paths can only be read and can't be written to.
569+
$testCases[] = [(object) ['foo' => (object) ['firstName' => 'Bernhard']], 'foo.bar?', null];
570+
$testCases[] = [(object) ['foo' => (object) ['firstName' => 'Bernhard']], 'foo.bar?.baz?', null];
571+
$testCases[] = [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?]', null];
572+
$testCases[] = [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?][baz?]', null];
573+
574+
return $testCases;
575+
}
576+
566577
public function testTicket5755()
567578
{
568579
$object = new Ticket5775Object();
@@ -738,17 +749,11 @@ public function __construct($foo)
738749
$this->foo = $foo;
739750
}
740751

741-
/**
742-
* @return mixed
743-
*/
744752
public function getFoo()
745753
{
746754
return $this->foo;
747755
}
748756

749-
/**
750-
* @param mixed $foo
751-
*/
752757
public function setFoo($foo)
753758
{
754759
$this->foo = $foo;

src/Symfony/Component/PropertyAccess/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
],
1818
"require": {
1919
"php": ">=8.1",
20+
"symfony/deprecation-contracts": "^2.1|^3",
2021
"symfony/property-info": "^5.4|^6.0"
2122
},
2223
"require-dev": {

0 commit comments

Comments
 (0)