Skip to content

Commit 42171a1

Browse files
bug #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 #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 #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 <fabien@symfony.com>
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 <soyuka@gmail.com>
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy