Skip to content

Commit 954f1d7

Browse files
committed
Added skip unitialized values context param
1 parent fc6005c commit 954f1d7

15 files changed

+235
-21
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Added
10+
- [GH#200](https://github.com/jolicode/automapper/pull/200) Added skip_uninitialized_values context to skip non initialized properties
11+
- [GH#200](https://github.com/jolicode/automapper/pull/200) Changed skip_null_values behavior to not handle initialized properties anymore
12+
913
### Fixed
1014
- [GH#207](https://github.com/jolicode/automapper/pull/207) [GH#208](https://github.com/jolicode/automapper/pull/208) Fix implicity nullable parameter deprecations
1115

16+
### Removed
17+
- [GH#200](https://github.com/jolicode/automapper/pull/200) Drop nikic/php-parser < 5.0 compatibility
18+
1219
## [9.2.0] - 2024-11-19
1320
### Added
1421
- [GH#180](https://github.com/jolicode/automapper/pull/180) Add configuration to generate code with strict types

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
],
1818
"require": {
1919
"php": "^8.2",
20-
"nikic/php-parser": "^4.18 || ^5.0",
20+
"nikic/php-parser": "^5.0",
2121
"symfony/deprecation-contracts": "^3.0",
2222
"symfony/event-dispatcher": "^6.4 || ^7.0",
2323
"symfony/expression-language": "^6.4 || ^7.0",

src/Extractor/ReadAccessor.php

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,18 @@ public function getIsNullExpression(Expr\Variable $input): Expr
188188
/*
189189
* Use the property fetch to read the value
190190
*
191-
* isset($input->property_name)
191+
* isset($input->property_name) && null === $input->property_name
192192
*/
193-
return new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)]));
193+
return new Expr\BinaryOp\LogicalAnd(new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)])), new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), new Expr\PropertyFetch($input, $this->accessor)));
194194
}
195195

196196
if (self::TYPE_ARRAY_DIMENSION === $this->type) {
197197
/*
198198
* Use the array dim fetch to read the value
199199
*
200-
* isset($input['property_name'])
200+
* isset($input['property_name']) && null === $input->property_name
201201
*/
202-
return new Expr\BooleanNot(new Expr\Isset_([new Expr\ArrayDimFetch($input, new Scalar\String_($this->accessor))]));
202+
return new Expr\BinaryOp\LogicalAnd(new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)])), new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), new Expr\PropertyFetch($input, $this->accessor)));
203203
}
204204

205205
if (self::TYPE_SOURCE === $this->type) {
@@ -212,6 +212,52 @@ public function getIsNullExpression(Expr\Variable $input): Expr
212212
throw new CompileException('Invalid accessor for read expression');
213213
}
214214

215+
public function getIsUndefinedExpression(Expr\Variable $input): Expr
216+
{
217+
if (\in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
218+
/*
219+
* false
220+
*/
221+
return new Expr\ConstFetch(new Name('false'));
222+
}
223+
224+
if (self::TYPE_PROPERTY === $this->type) {
225+
if ($this->private) {
226+
/*
227+
* When the property is private we use the extract callback that can read this value
228+
*
229+
* @see \AutoMapper\Extractor\ReadAccessor::getExtractIsUndefinedCallback()
230+
*
231+
* $this->extractIsUndefinedCallbacks['property_name']($input)
232+
*/
233+
return new Expr\FuncCall(
234+
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
235+
[
236+
new Arg($input),
237+
]
238+
);
239+
}
240+
241+
/*
242+
* Use the property fetch to read the value
243+
*
244+
* !isset($input->property_name)
245+
*/
246+
return new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)]));
247+
}
248+
249+
if (self::TYPE_ARRAY_DIMENSION === $this->type) {
250+
/*
251+
* Use the array dim fetch to read the value
252+
*
253+
* !array_key_exists('property_name', $input)
254+
*/
255+
return new Expr\BooleanNot(new Expr\FuncCall(new Name('array_key_exists'), [new Arg(new Scalar\String_($this->accessor)), new Arg($input)]));
256+
}
257+
258+
throw new CompileException('Invalid accessor for read expression');
259+
}
260+
215261
/**
216262
* Get AST expression for binding closure when dealing with a private property.
217263
*/
@@ -261,6 +307,38 @@ public function getExtractIsNullCallback(string $className): ?Expr
261307
return null;
262308
}
263309

310+
/*
311+
* Create extract is null callback for this accessor
312+
*
313+
* \Closure::bind(function ($object) {
314+
* return !isset($object->property_name) && null === $object->property_name;
315+
* }, null, $className)
316+
*/
317+
return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [
318+
new Arg(
319+
new Expr\Closure([
320+
'params' => [
321+
new Param(new Expr\Variable('object')),
322+
],
323+
'stmts' => [
324+
new Stmt\Return_(new Expr\BinaryOp\LogicalAnd(new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch(new Expr\Variable('object'), $this->accessor)])), new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), new Expr\PropertyFetch(new Expr\Variable('object'), $this->accessor)))),
325+
],
326+
])
327+
),
328+
new Arg(new Expr\ConstFetch(new Name('null'))),
329+
new Arg(new Scalar\String_($className)),
330+
]);
331+
}
332+
333+
/**
334+
* Get AST expression for binding closure when dealing with a private property.
335+
*/
336+
public function getExtractIsUndefinedCallback(string $className): ?Expr
337+
{
338+
if ($this->type !== self::TYPE_PROPERTY || !$this->private) {
339+
return null;
340+
}
341+
264342
/*
265343
* Create extract is null callback for this accessor
266344
*

src/GeneratedMapper.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
4848
/** @var array<string, callable(): bool>) */
4949
protected array $extractIsNullCallbacks = [];
5050

51+
/** @var array<string, callable(): bool>) */
52+
protected array $extractIsUndefinedCallbacks = [];
53+
5154
/** @var Target|\ReflectionClass<object> */
5255
protected mixed $cachedTarget;
5356
}

src/Generator/CreateTargetStatementsGenerator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ private function constructorArgument(GeneratorMetadata $metadata, PropertyMetada
193193
new Arg(new Scalar\String_(sprintf('Cannot create an instance of "%s" from mapping data because its constructor requires the following parameters to be present : "$%s".', $metadata->mapperMetadata->target, $propertyMetadata->target->property))),
194194
new Arg(create_scalar_int(0)),
195195
new Arg(new Expr\ConstFetch(new Name('null'))),
196-
new Arg(new Expr\Array_([ // @phpstan-ignore argument.type
196+
new Arg(new Expr\Array_([
197197
create_expr_array_item(new Scalar\String_($propertyMetadata->target->property)),
198198
])),
199199
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
@@ -262,7 +262,7 @@ private function constructorArgumentWithoutSource(GeneratorMetadata $metadata, \
262262
new Arg(new Scalar\String_(sprintf('Cannot create an instance of "%s" from mapping data because its constructor requires the following parameters to be present : "$%s".', $metadata->mapperMetadata->target, $constructorParameter->getName()))),
263263
new Arg(create_scalar_int(0)),
264264
new Arg(new Expr\ConstFetch(new Name('null'))),
265-
new Arg(new Expr\Array_([ // @phpstan-ignore argument.type
265+
new Arg(new Expr\Array_([
266266
create_expr_array_item(new Scalar\String_($constructorParameter->getName())),
267267
])),
268268
new Arg(new Scalar\String_($constructorParameter->getName())),

src/Generator/MapperConstructorGenerator.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public function getStatements(GeneratorMetadata $metadata): array
3131
foreach ($metadata->propertiesMetadata as $propertyMetadata) {
3232
$constructStatements[] = $this->extractCallbackForProperty($metadata, $propertyMetadata);
3333
$constructStatements[] = $this->extractIsNullCallbackForProperty($metadata, $propertyMetadata);
34+
$constructStatements[] = $this->extractIsUndefinedCallbackForProperty($metadata, $propertyMetadata);
3435
$constructStatements[] = $this->hydrateCallbackForProperty($metadata, $propertyMetadata);
3536
}
3637

@@ -83,6 +84,28 @@ private function extractIsNullCallbackForProperty(GeneratorMetadata $metadata, P
8384
));
8485
}
8586

87+
/**
88+
* Add read callback to the constructor of the generated mapper.
89+
*
90+
* ```php
91+
* $this->extractIsUndefinedCallbacks['propertyName'] = $extractIsNullCallback;
92+
* ```
93+
*/
94+
private function extractIsUndefinedCallbackForProperty(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Stmt\Expression
95+
{
96+
$extractUndefinedCallback = $propertyMetadata->source->accessor?->getExtractIsUndefinedCallback($metadata->mapperMetadata->source);
97+
98+
if (!$extractUndefinedCallback) {
99+
return null;
100+
}
101+
102+
return new Stmt\Expression(
103+
new Expr\Assign(
104+
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($propertyMetadata->source->property)),
105+
$extractUndefinedCallback
106+
));
107+
}
108+
86109
/**
87110
* Add hydrate callback to the constructor of the generated mapper.
88111
*

src/Generator/MapperGenerator.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ public function generate(GeneratorMetadata $metadata): array
7474

7575
$statements = [];
7676
if ($metadata->strictTypes) {
77-
// @phpstan-ignore argument.type
7877
$statements[] = new Stmt\Declare_([create_declare_item('strict_types', create_scalar_int(1))]);
7978
}
8079
$statements[] = (new Builder\Class_($metadata->mapperMetadata->className))

src/Generator/PropertyConditionsGenerator.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ private function isAllowedAttribute(GeneratorMetadata $metadata, PropertyMetadat
138138
return new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'isAllowedAttribute', [
139139
new Arg($variableRegistry->getContext()),
140140
new Arg(new Scalar\String_($propertyMetadata->source->property)),
141-
new Arg($propertyMetadata->source->accessor->getIsNullExpression($variableRegistry->getSourceInput())),
141+
new Arg(new Expr\Closure([
142+
'uses' => [new Expr\ClosureUse($variableRegistry->getSourceInput())],
143+
'stmts' => [new Stmt\Return_($propertyMetadata->source->accessor->getIsNullExpression($variableRegistry->getSourceInput()))],
144+
])),
145+
new Arg($propertyMetadata->source->accessor->getIsUndefinedExpression($variableRegistry->getSourceInput())),
142146
]);
143147
}
144148

@@ -172,7 +176,7 @@ private function groupsCheck(VariableRegistry $variableRegistry, ?array $groups
172176
new Expr\Array_()
173177
)
174178
),
175-
new Arg(new Expr\Array_(array_map(function (string $group) { // @phpstan-ignore argument.type
179+
new Arg(new Expr\Array_(array_map(function (string $group) {
176180
return create_expr_array_item(new Scalar\String_($group));
177181
}, $groups))),
178182
])

src/MapperContext.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* "deep_target_to_populate"?: bool,
2929
* "constructor_arguments"?: array<string, array<string, mixed>>,
3030
* "skip_null_values"?: bool,
31+
* "skip_uninitialized_values"?: bool,
3132
* "allow_readonly_target_to_populate"?: bool,
3233
* "datetime_format"?: string,
3334
* "datetime_force_timezone"?: string,
@@ -49,6 +50,7 @@ class MapperContext
4950
public const DEEP_TARGET_TO_POPULATE = 'deep_target_to_populate';
5051
public const CONSTRUCTOR_ARGUMENTS = 'constructor_arguments';
5152
public const SKIP_NULL_VALUES = 'skip_null_values';
53+
public const SKIP_UNINITIALIZED_VALUES = 'skip_uninitialized_values';
5254
public const ALLOW_READONLY_TARGET_TO_POPULATE = 'allow_readonly_target_to_populate';
5355
public const DATETIME_FORMAT = 'datetime_format';
5456
public const DATETIME_FORCE_TIMEZONE = 'datetime_force_timezone';
@@ -135,6 +137,13 @@ public function setSkipNullValues(bool $skipNullValues): self
135137
return $this;
136138
}
137139

140+
public function setSkipUnitializedValues(bool $skipUnitializedValues): self
141+
{
142+
$this->context[self::SKIP_UNINITIALIZED_VALUES] = $skipUnitializedValues;
143+
144+
return $this;
145+
}
146+
138147
public function setAllowReadOnlyTargetToPopulate(bool $allowReadOnlyTargetToPopulate): self
139148
{
140149
$this->context[self::ALLOW_READONLY_TARGET_TO_POPULATE] = $allowReadOnlyTargetToPopulate;
@@ -231,9 +240,13 @@ public static function withReference(array $context, string $reference, mixed &$
231240
*
232241
* @internal
233242
*/
234-
public static function isAllowedAttribute(array $context, string $attribute, bool $valueIsNullOrUndefined): bool
243+
public static function isAllowedAttribute(array $context, string $attribute, callable $valueIsNull, bool $valueIsUndefined): bool
235244
{
236-
if (($context[self::SKIP_NULL_VALUES] ?? false) && $valueIsNullOrUndefined) {
245+
if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? false) && $valueIsUndefined) {
246+
return false;
247+
}
248+
249+
if (($context[self::SKIP_NULL_VALUES] ?? false) && !$valueIsUndefined && $valueIsNull()) {
237250
return false;
238251
}
239252

src/Transformer/BuiltinTransformer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public function getCheckExpression(Expr $input, Expr $target, PropertyMetadata $
136136

137137
private function toArray(Expr $input): Expr
138138
{
139-
return new Expr\Array_([create_expr_array_item($input)]); // @phpstan-ignore argument.type
139+
return new Expr\Array_([create_expr_array_item($input)]);
140140
}
141141

142142
private function fromIteratorToArray(Expr $input): Expr

0 commit comments

Comments
 (0)