Skip to content

Commit 42171a1

Browse files
bug symfony#60856 [ObjectMapper] handle non existing property errors (soyuka)
This PR was squashed before being merged into the 7.3 branch. Discussion ---------- [ObjectMapper] handle non existing property errors | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | no <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Issues | Fix symfony#60848 | License | MIT The property accessor allows to ignore exceptions when a property does not exist. Without the property accessor we now throw a proper exception when the property does not exist (as opposed to the PHP Warning it'd trigger otherwise). I think its better to thrown then to hide the error in that particular case. This fixes symfony#60848 providing a working solution and a better DX, especially that in PHP the behavior of dynamic properties is to avoid them as much as possible. Commits ------- 818e7e8 [ObjectMapper] handle non existing property errors
2 parents ed27476 + 818e7e8 commit 42171a1

File tree

5 files changed

+89
-1
lines changed

5 files changed

+89
-1
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ObjectMapper\Exception;
13+
14+
/**
15+
* Thrown when a property cannot be found.
16+
*
17+
* @author Antoine Bluchet <[email protected]>
18+
*/
19+
class NoSuchPropertyException extends MappingException
20+
{
21+
}

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\ObjectMapper\Exception\MappingException;
1616
use Symfony\Component\ObjectMapper\Exception\MappingTransformException;
17+
use Symfony\Component\ObjectMapper\Exception\NoSuchPropertyException;
1718
use Symfony\Component\ObjectMapper\Metadata\Mapping;
1819
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
1920
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;
21+
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException as PropertyAccessorNoSuchPropertyException;
2022
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2123

2224
/**
@@ -167,7 +169,19 @@ public function map(object $source, object|string|null $target = null): object
167169

168170
private function getRawValue(object $source, string $propertyName): mixed
169171
{
170-
return $this->propertyAccessor ? $this->propertyAccessor->getValue($source, $propertyName) : $source->{$propertyName};
172+
if ($this->propertyAccessor) {
173+
try {
174+
return $this->propertyAccessor->getValue($source, $propertyName);
175+
} catch (PropertyAccessorNoSuchPropertyException $e) {
176+
throw new NoSuchPropertyException($e->getMessage(), $e->getCode(), $e);
177+
}
178+
}
179+
180+
if (!property_exists($source, $propertyName) && !isset($source->{$propertyName})) {
181+
throw new NoSuchPropertyException(sprintf('The property "%s" does not exist on "%s".', $propertyName, get_debug_type($source)));
182+
}
183+
184+
return $source->{$propertyName};
171185
}
172186

173187
private function getSourceValue(object $source, object $target, mixed $value, \SplObjectStorage $objectMap, ?Mapping $mapping = null): mixed

src/Symfony/Component/ObjectMapper/ObjectMapperInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\ObjectMapper\Exception\MappingException;
1515
use Symfony\Component\ObjectMapper\Exception\MappingTransformException;
16+
use Symfony\Component\ObjectMapper\Exception\NoSuchPropertyException;
1617

1718
/**
1819
* Object to object mapper.
@@ -33,6 +34,7 @@ interface ObjectMapperInterface
3334
*
3435
* @throws MappingException When the mapping configuration is wrong
3536
* @throws MappingTransformException When a transformation on an object does not return an object
37+
* @throws NoSuchPropertyException When a property does not exist
3638
*/
3739
public function map(object $source, object|string|null $target = null): object;
3840
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultValueStdClass;
4+
5+
use Symfony\Component\ObjectMapper\Attribute\Map;
6+
7+
class TargetDto
8+
{
9+
public function __construct(
10+
public string $id,
11+
#[Map(source: 'optional', if: [self::class, 'isDefined'])]
12+
public ?string $optional = null,
13+
) {
14+
}
15+
16+
public static function isDefined($source): bool
17+
{
18+
return isset($source);
19+
}
20+
}

src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Psr\Container\ContainerInterface;
1616
use Symfony\Component\ObjectMapper\Exception\MappingException;
1717
use Symfony\Component\ObjectMapper\Exception\MappingTransformException;
18+
use Symfony\Component\ObjectMapper\Exception\NoSuchPropertyException;
1819
use Symfony\Component\ObjectMapper\Metadata\Mapping;
1920
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
2021
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;
@@ -28,6 +29,7 @@
2829
use Symfony\Component\ObjectMapper\Tests\Fixtures\DeeperRecursion\RecursiveDto;
2930
use Symfony\Component\ObjectMapper\Tests\Fixtures\DeeperRecursion\Relation;
3031
use Symfony\Component\ObjectMapper\Tests\Fixtures\DeeperRecursion\RelationDto;
32+
use Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultValueStdClass\TargetDto;
3133
use Symfony\Component\ObjectMapper\Tests\Fixtures\Flatten\TargetUser;
3234
use Symfony\Component\ObjectMapper\Tests\Fixtures\Flatten\User;
3335
use Symfony\Component\ObjectMapper\Tests\Fixtures\Flatten\UserProfile;
@@ -247,8 +249,17 @@ public function testSourceOnly()
247249
$mapped = $mapper->map($a, SourceOnly::class);
248250
$this->assertInstanceOf(SourceOnly::class, $mapped);
249251
$this->assertSame('test', $mapped->mappedName);
252+
}
250253

254+
public function testSourceOnlyWithMagicMethods()
255+
{
256+
$mapper = new ObjectMapper();
251257
$a = new class {
258+
public function __isset($key): bool
259+
{
260+
return 'name' === $key;
261+
}
262+
252263
public function __get(string $key): string
253264
{
254265
return match ($key) {
@@ -314,4 +325,24 @@ public function testMultipleTargetMapProperty()
314325
$this->assertEquals('donotmap', $c->foo);
315326
$this->assertEquals('foo', $c->doesNotExistInTargetB);
316327
}
328+
329+
public function testDefaultValueStdClass()
330+
{
331+
$this->expectException(NoSuchPropertyException::class);
332+
$u = new \stdClass();
333+
$u->id = 'abc';
334+
$mapper = new ObjectMapper();
335+
$b = $mapper->map($u, TargetDto::class);
336+
}
337+
338+
public function testDefaultValueStdClassWithPropertyInfo()
339+
{
340+
$u = new \stdClass();
341+
$u->id = 'abc';
342+
$mapper = new ObjectMapper(propertyAccessor: PropertyAccess::createPropertyAccessorBuilder()->disableExceptionOnInvalidPropertyPath()->getPropertyAccessor());
343+
$b = $mapper->map($u, TargetDto::class);
344+
$this->assertInstanceOf(TargetDto::class, $b);
345+
$this->assertSame('abc', $b->id);
346+
$this->assertNull($b->optional);
347+
}
317348
}

0 commit comments

Comments
 (0)