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 b22389d

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

File tree

Expand file treeCollapse file tree

4 files changed

+82
-14
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+82
-14
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+24-9Lines changed: 24 additions & 9 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,12 +310,16 @@ 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
316317
if ($i + 1 < $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
317-
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
318+
if ($isOptional) {
319+
$propertyValues[] = null;
320+
} else {
321+
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
322+
}
318323
}
319324

320325
if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) {
@@ -367,15 +372,21 @@ private function readIndex(array $zval, $index): array
367372
*
368373
* @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public
369374
*/
370-
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false): array
375+
private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false, $isOptional = false): array
371376
{
372-
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));
377+
$result = self::$resultProto;
378+
379+
if (!\is_object($zval[self::VALUE]) && !$isOptional) {
380+
if ($isOptional) {
381+
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property));
382+
} else {
383+
$result[self::VALUE] = null;
384+
return $result;
385+
}
374386
}
375387

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

380391
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
381392
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
@@ -399,7 +410,9 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
399410
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
400411
// we call the getter and hope the __call do the job
401412
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
402-
} elseif (!$ignoreInvalidProperty) {
413+
} elseif ($isOptional) {
414+
$result[self::VALUE] = null;
415+
}elseif (!$ignoreInvalidProperty) {
403416
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
404417
}
405418

@@ -414,7 +427,7 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
414427
/**
415428
* Guesses how to read the property value.
416429
*/
417-
private function getReadAccessInfo(string $class, string $property): array
430+
private function getReadAccessInfo(string $class, string $property, $isOptional): array
418431
{
419432
$key = str_replace('\\', '.', $class).'..'.$property;
420433

@@ -467,6 +480,8 @@ private function getReadAccessInfo(string $class, string $property): array
467480
// we call the getter and hope the __call do the job
468481
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
469482
$access[self::ACCESS_NAME] = $getter;
483+
} elseif ($isOptional) {
484+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
470485
} else {
471486
$methods = [$getter, $getsetter, $isser, $hasser, '__get'];
472487
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?]', 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.