Skip to content

Commit

Permalink
feat: ignoreUninitialized argument in AsPropertyMapper to ignore …
Browse files Browse the repository at this point in the history
…uninitialized properties (#260)
  • Loading branch information
priyadi authored Jan 27, 2025
1 parent 3e132cb commit decad8e
Show file tree
Hide file tree
Showing 15 changed files with 69 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* feat: `RefuseToMapException`, thrown by property mapper if it needs to leave
the target property unmapped
* build: update phpstan, psalm, rector
* feat: `ignoreUninitialized` argument in `AsPropertyMapper` to ignore
uninitialized properties

## 1.13.5

Expand Down
1 change: 1 addition & 0 deletions src/Attribute/AsPropertyMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
public function __construct(
public ?string $property = null,
public ?string $targetClass = null,
public bool $ignoreUninitialized = false,
) {}
}
2 changes: 2 additions & 0 deletions src/CustomMapper/Implementation/PropertyMapperResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ public function addPropertyMapper(
string $serviceId,
string $method,
bool $hasExistingTarget,
bool $ignoreUninitialized,
array $extraArguments = [],
): void {
$this->propertyMappers[$targetClass][$property][$sourceClass]
= new ServiceMethodSpecification(
serviceId: $serviceId,
method: $method,
hasExistingTarget: $hasExistingTarget,
ignoreUninitialized: $ignoreUninitialized,
extraArguments: $extraArguments,
);
}
Expand Down
1 change: 1 addition & 0 deletions src/CustomMapper/ObjectMapperTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function addObjectMapper(
serviceId: $serviceId,
method: $method,
hasExistingTarget: $hasExistingTarget,
ignoreUninitialized: false,
extraArguments: $extraArguments,
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/CompilerPass/PropertyMapperPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function process(ContainerBuilder $container): void
throw new InvalidArgumentException(\sprintf('Class "%s" does not exist', $serviceClass));
}

/** @var array{sourceClass:class-string,targetClass:class-string,property:string,serviceId:string,method:string,hasExistingTarget:bool,extraArguments:array<string,int>} $tag */
/** @var array{sourceClass:class-string,targetClass:class-string,property:string,serviceId:string,method:string,hasExistingTarget:bool,ignoreUninitialized:bool,extraArguments:array<string,int>} $tag */
foreach ($tags as $tag) {
$method = $tag['method'] ?? throw new InvalidArgumentException('Method is required');

Expand All @@ -55,6 +55,7 @@ public function process(ContainerBuilder $container): void
'$serviceId' => $serviceId,
'$method' => $method,
'$hasExistingTarget' => $hasExistingTargetParameter,
'$ignoreUninitialized' => $tag['ignoreUninitialized'],
'$extraArguments' => ServiceMethodExtraArgumentUtil::getExtraArguments($serviceClass, $method, $hasExistingTargetParameter),
],
);
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/RekalogikaMapperExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ private function propertyMapperConfigurator(
$tagAttributes['targetClass'] = $attribute->targetClass;
}

$tagAttributes['ignoreUninitialized'] = $attribute->ignoreUninitialized;

// Use the class of the first argument of the method as the source class

$parameters = $reflection->getParameters();
Expand Down
19 changes: 11 additions & 8 deletions src/MapperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
class MapperFactory
{
/**
* @var array<int,array{sourceClass:class-string,targetClass:class-string,property:string,service:object,method:string,hasExistingTarget:bool,extraArguments:array<int,ServiceMethodSpecification::ARGUMENT_*>}>
* @var array<int,array{sourceClass:class-string,targetClass:class-string,property:string,service:object,method:string,hasExistingTarget:bool,ignoreUninitialized:bool,extraArguments:array<int,ServiceMethodSpecification::ARGUMENT_*>}>
*/
private array $propertyMappers = [];

Expand Down Expand Up @@ -240,6 +240,7 @@ public function addPropertyMapper(
object $service,
string $method,
bool $hasExistingTarget,
bool $ignoreUninitialized,
array $extraArguments = [],
): void {
$this->propertyMappers[] = [
Expand All @@ -249,6 +250,7 @@ public function addPropertyMapper(
'service' => $service,
'method' => $method,
'hasExistingTarget' => $hasExistingTarget,
'ignoreUninitialized' => $ignoreUninitialized,
'extraArguments' => $extraArguments,
];
}
Expand Down Expand Up @@ -766,13 +768,14 @@ protected function getPropertyMapperResolver(): PropertyMapperResolverInterface
$this->propertyMapperResolver = new PropertyMapperResolver();
foreach ($this->propertyMappers as $propertyMapper) {
$this->propertyMapperResolver->addPropertyMapper(
$propertyMapper['sourceClass'],
$propertyMapper['targetClass'],
$propertyMapper['property'],
$propertyMapper['service']::class,
$propertyMapper['method'],
$propertyMapper['hasExistingTarget'],
$propertyMapper['extraArguments'],
sourceClass: $propertyMapper['sourceClass'],
targetClass: $propertyMapper['targetClass'],
property: $propertyMapper['property'],
serviceId: $propertyMapper['service']::class,
method: $propertyMapper['method'],
hasExistingTarget: $propertyMapper['hasExistingTarget'],
ignoreUninitialized: $propertyMapper['ignoreUninitialized'],
extraArguments: $propertyMapper['extraArguments'],
);
}
}
Expand Down
16 changes: 14 additions & 2 deletions src/ServiceMethod/ServiceMethodRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Psr\Container\ContainerInterface;
use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Exception\RefuseToMapException;
use Rekalogika\Mapper\MainTransformer\MainTransformerInterface;
use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface;
use Symfony\Component\PropertyInfo\Type;
Expand Down Expand Up @@ -99,8 +100,19 @@ public function runPropertyMapper(
),
];

/** @psalm-suppress MixedMethodCall */
return $service->{$method}(...$arguments);
try {
/** @psalm-suppress MixedMethodCall */
return $service->{$method}(...$arguments);
} catch (\Error $e) {
if (
$serviceMethodSpecification->ignoreUninitialized()
&& str_contains($e->getMessage(), 'must not be accessed before initialization')
) {
throw new RefuseToMapException();
}

throw $e;
}
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/ServiceMethod/ServiceMethodSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function __construct(
private string $serviceId,
private string $method,
private bool $hasExistingTarget,
private bool $ignoreUninitialized,
private array $extraArguments,
) {}

Expand All @@ -56,4 +57,9 @@ public function hasExistingTarget(): bool
{
return $this->hasExistingTarget;
}

public function ignoreUninitialized(): bool
{
return $this->ignoreUninitialized;
}
}
22 changes: 11 additions & 11 deletions tests/config/rekalogika-mapper/generated-mappings.php
Original file line number Diff line number Diff line change
Expand Up @@ -785,67 +785,67 @@
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 246
// tests/src/IntegrationTest/PropertyMappingTest.php on line 252
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\Bar::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\Baz::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 201
// tests/src/IntegrationTest/PropertyMappingTest.php on line 207
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\ChildOfSomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\ChildOfSomeObjectDto::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 184
// tests/src/IntegrationTest/PropertyMappingTest.php on line 190
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\ChildOfSomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObjectDto::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 243
// tests/src/IntegrationTest/PropertyMappingTest.php on line 249
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\Foo::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\Baz::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 218
// tests/src/IntegrationTest/PropertyMappingTest.php on line 224
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\ChildOfSomeObjectDto::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 273
// tests/src/IntegrationTest/PropertyMappingTest.php on line 279
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\ObjectWithChild1::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 286
// tests/src/IntegrationTest/PropertyMappingTest.php on line 292
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\ObjectWithChild2::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 167
// tests/src/IntegrationTest/PropertyMappingTest.php on line 173
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObjectDto::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 235
// tests/src/IntegrationTest/PropertyMappingTest.php on line 241
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObjectWithConstructorDto::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 299
// tests/src/IntegrationTest/PropertyMappingTest.php on line 305
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObject::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObjectWithDateTimeImmutableDto::class
);

$mappingCollection->addObjectMapping(
// tests/src/IntegrationTest/PropertyMappingTest.php on line 309
// tests/src/IntegrationTest/PropertyMappingTest.php on line 315
source: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObjectWithUninitializedVariable::class,
target: \Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\SomeObjectWithUninitializedVariableDto::class
);
Expand Down
3 changes: 2 additions & 1 deletion tests/src/Common/IntegrationTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ protected function setUp(): void
service: $propertyMapper['service'],
method: $propertyMapper['method'],
hasExistingTarget: $propertyMapper['hasExistingTarget'],
ignoreUninitialized: $propertyMapper['ignoreUninitialized'],
);
}

Expand All @@ -65,7 +66,7 @@ protected function getAdditionalTransformers(): array
}

/**
* @return iterable<array{sourceClass:class-string,targetClass:class-string,property:string,service:object,method:string,hasExistingTarget:bool}>
* @return iterable<array{sourceClass:class-string,targetClass:class-string,property:string,service:object,method:string,hasExistingTarget:bool,ignoreUninitialized:bool}>
*/
protected function getPropertyMappers(): iterable
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
class SomeObjectWithUninitializedVariable
{
public string $propertyA;
public string $propertyB;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
class SomeObjectWithUninitializedVariableDto
{
public string $propertyA;
public string $propertyB;
}
7 changes: 7 additions & 0 deletions tests/src/IntegrationTest/PropertyMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public static function propertyMapperResolverDataProvider(): iterable
serviceId: PropertyMapperWithoutClassAttribute::class,
method: 'mapPropertyA',
hasExistingTarget: false,
ignoreUninitialized: false,
extraArguments: [],
),
];
Expand All @@ -98,6 +99,7 @@ public static function propertyMapperResolverDataProvider(): iterable
serviceId: PropertyMapperWithClassAttribute::class,
method: 'mapPropertyB',
hasExistingTarget: false,
ignoreUninitialized: false,
extraArguments: [],
),
];
Expand All @@ -117,6 +119,7 @@ public static function propertyMapperResolverDataProvider(): iterable
serviceId: PropertyMapperWithConstructorWithoutClassAttribute::class,
method: 'mapPropertyA',
hasExistingTarget: false,
ignoreUninitialized: false,
extraArguments: [],
),
];
Expand All @@ -129,6 +132,7 @@ public static function propertyMapperResolverDataProvider(): iterable
serviceId: PropertyMapperWithConstructorWithClassAttribute::class,
method: 'mapPropertyB',
hasExistingTarget: false,
ignoreUninitialized: false,
extraArguments: [],
),
];
Expand All @@ -141,6 +145,7 @@ public static function propertyMapperResolverDataProvider(): iterable
serviceId: PropertyMapperWithClassAttributeWithoutExplicitProperty::class,
method: 'mapPropertyD',
hasExistingTarget: false,
ignoreUninitialized: false,
extraArguments: [],
),
];
Expand All @@ -153,6 +158,7 @@ public static function propertyMapperResolverDataProvider(): iterable
serviceId: PropertyMapperWithExtraArguments::class,
method: 'mapPropertyE',
hasExistingTarget: false,
ignoreUninitialized: false,
extraArguments: [
ServiceMethodSpecification::ARGUMENT_CONTEXT,
ServiceMethodSpecification::ARGUMENT_MAIN_TRANSFORMER,
Expand Down Expand Up @@ -309,5 +315,6 @@ public function testFromUninitializedVariable(): void
$result = $this->mapper->map($source, $target);

$this->assertFalse(isset($result->propertyA));
$this->assertFalse(isset($result->propertyB));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ public function mapPropertyA(SomeObjectWithUninitializedVariable $object): strin
throw $e;
}
}

#[AsPropertyMapper('propertyB', ignoreUninitialized: true)]
public function mapPropertyB(SomeObjectWithUninitializedVariable $object): string
{
return $object->propertyA;
}
}

0 comments on commit decad8e

Please sign in to comment.