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 30a80a3

Browse filesBrowse files
committed
Feature 34483 - Having optional property accesses.
1 parent 1b4ab81 commit 30a80a3
Copy full SHA for 30a80a3

File tree

5 files changed

+85
-12
lines changed
Filter options

5 files changed

+85
-12
lines changed

‎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
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ public function isIndex(int $index)
193193
return $this->isIndex[$index];
194194
}
195195

196+
/**
197+
* {@inheritdoc}
198+
*/
199+
public function isOptional(int $index)
200+
{
201+
// Nothing is optional
202+
return false;
203+
}
204+
196205
/**
197206
* Returns whether an element maps directly to a form.
198207
*

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+18-7Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert
279279
for ($i = 0; $i < $lastIndex; ++$i) {
280280
$property = $propertyPath->getElement($i);
281281
$isIndex = $propertyPath->isIndex($i);
282+
$isOptional = $propertyPath->isOptional($i);
282283

283284
if ($isIndex) {
284285
// Create missing nested arrays on demand
@@ -309,11 +310,11 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert
309310

310311
$zval = $this->readIndex($zval, $property);
311312
} else {
312-
$zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty);
313+
$zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty, $isOptional);
313314
}
314315

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

@@ -367,15 +368,21 @@ private function readIndex(array $zval, $index): array
367368
*
368369
* @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public
369370
*/
370-
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false): array
371+
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false, $isOptional = false): array
371372
{
373+
$result = self::$resultProto;
374+
372375
if (!\is_object($zval[self::VALUE])) {
373-
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property));
376+
if (!$isOptional) {
377+
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property));
378+
} else {
379+
$result[self::VALUE] = null;
380+
return $result;
381+
}
374382
}
375383

376-
$result = self::$resultProto;
377384
$object = $zval[self::VALUE];
378-
$access = $this->getReadAccessInfo(\get_class($object), $property);
385+
$access = $this->getReadAccessInfo(\get_class($object), $property, $isOptional);
379386

380387
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
381388
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
@@ -399,6 +406,8 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
399406
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
400407
// we call the getter and hope the __call do the job
401408
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
409+
} elseif ($isOptional) {
410+
$result[self::VALUE] = null;
402411
} elseif (!$ignoreInvalidProperty) {
403412
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
404413
}
@@ -414,7 +423,7 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
414423
/**
415424
* Guesses how to read the property value.
416425
*/
417-
private function getReadAccessInfo(string $class, string $property): array
426+
private function getReadAccessInfo(string $class, string $property, $isOptional): array
418427
{
419428
$key = str_replace('\\', '.', $class).'..'.$property;
420429

@@ -467,6 +476,8 @@ private function getReadAccessInfo(string $class, string $property): array
467476
// we call the getter and hope the __call do the job
468477
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
469478
$access[self::ACCESS_NAME] = $getter;
479+
} elseif ($isOptional) {
480+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
470481
} else {
471482
$methods = [$getter, $getsetter, $isser, $hasser, '__get'];
472483
if ($this->magicCall) {

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyPath.php
+29Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
4949
*/
5050
private $isIndex = [];
5151

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

7786
return;
@@ -100,6 +109,13 @@ public function __construct($propertyPath)
100109
$this->isIndex[] = true;
101110
}
102111

112+
// Mark as optional when last character is "?".
113+
if ('?' === substr($element, -1, 1)) {
114+
$this->isOptional[] = true;
115+
$element = substr($element, 0, -1);
116+
} else {
117+
$this->isOptional[] = false;
118+
}
103119
$this->elements[] = $element;
104120

105121
$position += \strlen($matches[1]);
@@ -145,6 +161,7 @@ public function getParent()
145161
$parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '[')));
146162
array_pop($parent->elements);
147163
array_pop($parent->isIndex);
164+
array_pop($parent->isOptional);
148165

149166
return $parent;
150167
}
@@ -202,4 +219,16 @@ public function isIndex(int $index)
202219

203220
return $this->isIndex[$index];
204221
}
222+
223+
/**
224+
* {@inheritdoc}
225+
*/
226+
public function isOptional(int $index)
227+
{
228+
if (!isset($this->isOptional[$index])) {
229+
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
230+
}
231+
232+
return $this->isOptional[$index];
233+
}
205234
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyPathInterface.php
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,15 @@ public function isProperty(int $index);
8383
* @throws Exception\OutOfBoundsException If the offset is invalid
8484
*/
8585
public function isIndex(int $index);
86+
87+
/**
88+
* Returns whether the element at the given index is optional.
89+
*
90+
* @param int $index The index in the property path
91+
*
92+
* @return bool Whether the element at this index is an array index
93+
*
94+
* @throws Exception\OutOfBoundsException If the offset is invalid
95+
*/
96+
public function isOptional(int $index);
8697
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
+18-5Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public function getPathsWithMissingIndex()
8787
}
8888

8989
/**
90-
* @dataProvider getValidPropertyPaths
90+
* @dataProvider getValidReadPropertyPaths
9191
*/
9292
public function testGetValue($objectOrArray, $path, $value)
9393
{
@@ -212,7 +212,7 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p
212212
}
213213

214214
/**
215-
* @dataProvider getValidPropertyPaths
215+
* @dataProvider getValidWritePropertyPaths
216216
*/
217217
public function testSetValue($objectOrArray, $path)
218218
{
@@ -312,7 +312,7 @@ public function testGetValueWhenArrayValueIsNull()
312312
}
313313

314314
/**
315-
* @dataProvider getValidPropertyPaths
315+
* @dataProvider getValidReadPropertyPaths
316316
*/
317317
public function testIsReadable($objectOrArray, $path)
318318
{
@@ -373,7 +373,7 @@ public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $pa
373373
}
374374

375375
/**
376-
* @dataProvider getValidPropertyPaths
376+
* @dataProvider getValidWritePropertyPaths
377377
*/
378378
public function testIsWritable($objectOrArray, $path)
379379
{
@@ -433,7 +433,7 @@ public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $pa
433433
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path));
434434
}
435435

436-
public function getValidPropertyPaths()
436+
public function getValidWritePropertyPaths()
437437
{
438438
return [
439439
[['Bernhard', 'Schussek'], '[0]', 'Bernhard'],
@@ -479,6 +479,19 @@ public function getValidPropertyPaths()
479479
];
480480
}
481481

482+
public function getValidReadPropertyPaths()
483+
{
484+
$testCases = $this->getValidWritePropertyPaths();
485+
486+
// Optional paths can only be read and can't be written to.
487+
$testCases[] = [(object) ['foo' => (object) ['firstName' => 'Bernhard']], 'foo.bar?', null];
488+
$testCases[] = [(object) ['foo' => (object) ['firstName' => 'Bernhard']], 'foo.bar?.baz?', null];
489+
$testCases[] = [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?]', null];
490+
$testCases[] = [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?][baz?]', null];
491+
492+
return $testCases;
493+
}
494+
482495
public function testTicket5755()
483496
{
484497
$object = new Ticket5775Object();

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.