Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 1a58136

Browse filesBrowse files
committed
feature #34483 - Allow optional property accesses
1 parent ef92fb5 commit 1a58136
Copy full SHA for 1a58136

File tree

8 files changed

+80
-12
lines changed
Filter options

8 files changed

+80
-12
lines changed

‎UPGRADE-6.2.md

Copy file name to clipboardExpand all lines: UPGRADE-6.2.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ Mailer
2929

3030
* Deprecate the `OhMySMTP` transport, use `MailPace` instead
3131

32+
PropertyAccess
33+
--------------
34+
35+
* Implementing the `PropertyPathInterface` without implementing the `__isNullSafe()` method is deprecated
36+
3237
Security
3338
--------
3439

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.2
5+
---
6+
7+
* Added method `isNullSafe()` to `PropertyPathInterface`
8+
49
6.0
510
---
611

‎src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+15-3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyPath.php
+27Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
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
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyPathInterface.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*
1717
* @author Bernhard Schussek <bschussek@gmail.com>
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
+20-9Lines changed: 20 additions & 9 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();

‎src/Symfony/Component/PropertyAccess/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/composer.json
+1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.