Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added skip unitialized values context param #200

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- [GH#200](https://github.com/jolicode/automapper/pull/200) Added skip_uninitialized_values context to skip non initialized properties
- [GH#200](https://github.com/jolicode/automapper/pull/200) Changed skip_null_values behavior to not handle initialized properties anymore

### Fixed
- [GH#207](https://github.com/jolicode/automapper/pull/207) [GH#208](https://github.com/jolicode/automapper/pull/208) Fix implicity nullable parameter deprecations

### Removed
- [GH#200](https://github.com/jolicode/automapper/pull/200) Drop nikic/php-parser < 5.0 compatibility

## [9.2.0] - 2024-11-19
### Added
- [GH#180](https://github.com/jolicode/automapper/pull/180) Add configuration to generate code with strict types
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"require": {
"php": "^8.2",
"nikic/php-parser": "^4.18 || ^5.0",
"nikic/php-parser": "^5.0",
"symfony/deprecation-contracts": "^3.0",
"symfony/event-dispatcher": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
Expand Down
86 changes: 82 additions & 4 deletions src/Extractor/ReadAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,18 @@ public function getIsNullExpression(Expr\Variable $input): Expr
/*
* Use the property fetch to read the value
*
* isset($input->property_name)
* isset($input->property_name) && null === $input->property_name
*/
return new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)]));
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)));
}

if (self::TYPE_ARRAY_DIMENSION === $this->type) {
/*
* Use the array dim fetch to read the value
*
* isset($input['property_name'])
* isset($input['property_name']) && null === $input->property_name
*/
return new Expr\BooleanNot(new Expr\Isset_([new Expr\ArrayDimFetch($input, new Scalar\String_($this->accessor))]));
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)));
}

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

public function getIsUndefinedExpression(Expr\Variable $input): Expr
{
if (\in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
/*
* false
*/
return new Expr\ConstFetch(new Name('false'));
}

if (self::TYPE_PROPERTY === $this->type) {
if ($this->private) {
/*
* When the property is private we use the extract callback that can read this value
*
* @see \AutoMapper\Extractor\ReadAccessor::getExtractIsUndefinedCallback()
*
* $this->extractIsUndefinedCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
);
}

/*
* Use the property fetch to read the value
*
* !isset($input->property_name)
*/
return new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)]));
}

if (self::TYPE_ARRAY_DIMENSION === $this->type) {
/*
* Use the array dim fetch to read the value
*
* !array_key_exists('property_name', $input)
*/
return new Expr\BooleanNot(new Expr\FuncCall(new Name('array_key_exists'), [new Arg(new Scalar\String_($this->accessor)), new Arg($input)]));
}

throw new CompileException('Invalid accessor for read expression');
}

/**
* Get AST expression for binding closure when dealing with a private property.
*/
Expand Down Expand Up @@ -261,6 +307,38 @@ public function getExtractIsNullCallback(string $className): ?Expr
return null;
}

/*
* Create extract is null callback for this accessor
*
* \Closure::bind(function ($object) {
* return !isset($object->property_name) && null === $object->property_name;
* }, null, $className)
*/
return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [
new Arg(
new Expr\Closure([
'params' => [
new Param(new Expr\Variable('object')),
],
'stmts' => [
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)))),
],
])
),
new Arg(new Expr\ConstFetch(new Name('null'))),
new Arg(new Scalar\String_($className)),
]);
}

/**
* Get AST expression for binding closure when dealing with a private property.
*/
public function getExtractIsUndefinedCallback(string $className): ?Expr
{
if ($this->type !== self::TYPE_PROPERTY || !$this->private) {
return null;
}

/*
* Create extract is null callback for this accessor
*
Expand Down
3 changes: 3 additions & 0 deletions src/GeneratedMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
/** @var array<string, callable(): bool>) */
protected array $extractIsNullCallbacks = [];

/** @var array<string, callable(): bool>) */
protected array $extractIsUndefinedCallbacks = [];

/** @var Target|\ReflectionClass<object> */
protected mixed $cachedTarget;
}
4 changes: 2 additions & 2 deletions src/Generator/CreateTargetStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private function constructorArgument(GeneratorMetadata $metadata, PropertyMetada
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))),
new Arg(create_scalar_int(0)),
new Arg(new Expr\ConstFetch(new Name('null'))),
new Arg(new Expr\Array_([ // @phpstan-ignore argument.type
new Arg(new Expr\Array_([
create_expr_array_item(new Scalar\String_($propertyMetadata->target->property)),
])),
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
Expand Down Expand Up @@ -262,7 +262,7 @@ private function constructorArgumentWithoutSource(GeneratorMetadata $metadata, \
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()))),
new Arg(create_scalar_int(0)),
new Arg(new Expr\ConstFetch(new Name('null'))),
new Arg(new Expr\Array_([ // @phpstan-ignore argument.type
new Arg(new Expr\Array_([
create_expr_array_item(new Scalar\String_($constructorParameter->getName())),
])),
new Arg(new Scalar\String_($constructorParameter->getName())),
Expand Down
23 changes: 23 additions & 0 deletions src/Generator/MapperConstructorGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function getStatements(GeneratorMetadata $metadata): array
foreach ($metadata->propertiesMetadata as $propertyMetadata) {
$constructStatements[] = $this->extractCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractIsNullCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractIsUndefinedCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->hydrateCallbackForProperty($metadata, $propertyMetadata);
}

Expand Down Expand Up @@ -83,6 +84,28 @@ private function extractIsNullCallbackForProperty(GeneratorMetadata $metadata, P
));
}

/**
* Add read callback to the constructor of the generated mapper.
*
* ```php
* $this->extractIsUndefinedCallbacks['propertyName'] = $extractIsNullCallback;
* ```
*/
private function extractIsUndefinedCallbackForProperty(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Stmt\Expression
{
$extractUndefinedCallback = $propertyMetadata->source->accessor?->getExtractIsUndefinedCallback($metadata->mapperMetadata->source);

if (!$extractUndefinedCallback) {
return null;
}

return new Stmt\Expression(
new Expr\Assign(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($propertyMetadata->source->property)),
$extractUndefinedCallback
));
}

/**
* Add hydrate callback to the constructor of the generated mapper.
*
Expand Down
1 change: 0 additions & 1 deletion src/Generator/MapperGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ public function generate(GeneratorMetadata $metadata): array

$statements = [];
if ($metadata->strictTypes) {
// @phpstan-ignore argument.type
$statements[] = new Stmt\Declare_([create_declare_item('strict_types', create_scalar_int(1))]);
}
$statements[] = (new Builder\Class_($metadata->mapperMetadata->className))
Expand Down
8 changes: 6 additions & 2 deletions src/Generator/PropertyConditionsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ private function isAllowedAttribute(GeneratorMetadata $metadata, PropertyMetadat
return new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'isAllowedAttribute', [
new Arg($variableRegistry->getContext()),
new Arg(new Scalar\String_($propertyMetadata->source->property)),
new Arg($propertyMetadata->source->accessor->getIsNullExpression($variableRegistry->getSourceInput())),
new Arg(new Expr\Closure([
'uses' => [new Expr\ClosureUse($variableRegistry->getSourceInput())],
'stmts' => [new Stmt\Return_($propertyMetadata->source->accessor->getIsNullExpression($variableRegistry->getSourceInput()))],
])),
new Arg($propertyMetadata->source->accessor->getIsUndefinedExpression($variableRegistry->getSourceInput())),
]);
}

Expand Down Expand Up @@ -172,7 +176,7 @@ private function groupsCheck(VariableRegistry $variableRegistry, ?array $groups
new Expr\Array_()
)
),
new Arg(new Expr\Array_(array_map(function (string $group) { // @phpstan-ignore argument.type
new Arg(new Expr\Array_(array_map(function (string $group) {
return create_expr_array_item(new Scalar\String_($group));
}, $groups))),
])
Expand Down
17 changes: 15 additions & 2 deletions src/MapperContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* "deep_target_to_populate"?: bool,
* "constructor_arguments"?: array<string, array<string, mixed>>,
* "skip_null_values"?: bool,
* "skip_uninitialized_values"?: bool,
* "allow_readonly_target_to_populate"?: bool,
* "datetime_format"?: string,
* "datetime_force_timezone"?: string,
Expand All @@ -49,6 +50,7 @@ class MapperContext
public const DEEP_TARGET_TO_POPULATE = 'deep_target_to_populate';
public const CONSTRUCTOR_ARGUMENTS = 'constructor_arguments';
public const SKIP_NULL_VALUES = 'skip_null_values';
public const SKIP_UNINITIALIZED_VALUES = 'skip_uninitialized_values';
public const ALLOW_READONLY_TARGET_TO_POPULATE = 'allow_readonly_target_to_populate';
public const DATETIME_FORMAT = 'datetime_format';
public const DATETIME_FORCE_TIMEZONE = 'datetime_force_timezone';
Expand Down Expand Up @@ -135,6 +137,13 @@ public function setSkipNullValues(bool $skipNullValues): self
return $this;
}

public function setSkipUnitializedValues(bool $skipUnitializedValues): self
{
$this->context[self::SKIP_UNINITIALIZED_VALUES] = $skipUnitializedValues;

return $this;
}

public function setAllowReadOnlyTargetToPopulate(bool $allowReadOnlyTargetToPopulate): self
{
$this->context[self::ALLOW_READONLY_TARGET_TO_POPULATE] = $allowReadOnlyTargetToPopulate;
Expand Down Expand Up @@ -231,9 +240,13 @@ public static function withReference(array $context, string $reference, mixed &$
*
* @internal
*/
public static function isAllowedAttribute(array $context, string $attribute, bool $valueIsNullOrUndefined): bool
public static function isAllowedAttribute(array $context, string $attribute, callable $valueIsNull, bool $valueIsUndefined): bool
{
if (($context[self::SKIP_NULL_VALUES] ?? false) && $valueIsNullOrUndefined) {
if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? false) && $valueIsUndefined) {
return false;
}

if (($context[self::SKIP_NULL_VALUES] ?? false) && !$valueIsUndefined && $valueIsNull()) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Transformer/BuiltinTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public function getCheckExpression(Expr $input, Expr $target, PropertyMetadata $

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

private function fromIteratorToArray(Expr $input): Expr
Expand Down
20 changes: 18 additions & 2 deletions tests/AutoMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
use AutoMapper\Tests\Fixtures\Issue111\Colour;
use AutoMapper\Tests\Fixtures\Issue111\ColourTransformer;
use AutoMapper\Tests\Fixtures\Issue111\FooDto;
use AutoMapper\Tests\Fixtures\Issue189\User as Issue189User;
use AutoMapper\Tests\Fixtures\Issue189\UserPatchInput as Issue189UserPatchInput;
use AutoMapper\Tests\Fixtures\ObjectsUnion\Bar;
use AutoMapper\Tests\Fixtures\ObjectsUnion\Foo;
use AutoMapper\Tests\Fixtures\ObjectsUnion\ObjectsUnionProperty;
Expand Down Expand Up @@ -1009,7 +1011,7 @@ public function testSkipNullValues(): void
$input = new Fixtures\SkipNullValues\Input();

/** @var Fixtures\SkipNullValues\Entity $entity */
$entity = $this->autoMapper->map($input, $entity, ['skip_null_values' => true]);
$entity = $this->autoMapper->map($input, $entity, [MapperContext::SKIP_NULL_VALUES => true]);
self::assertEquals('foobar', $entity->getName());
}

Expand Down Expand Up @@ -1353,7 +1355,7 @@ public function testNoErrorWithUninitializedProperty(): void

self::assertSame(
['bar' => 'bar'],
$this->autoMapper->map(new Uninitialized(), 'array', ['skip_null_values' => true])
$this->autoMapper->map(new Uninitialized(), 'array', [MapperContext::SKIP_UNINITIALIZED_VALUES => true])
);
}

Expand Down Expand Up @@ -1603,4 +1605,18 @@ public function testParamDocBlock(): void
'foo' => ['foo1', 'foo2'],
], $array);
}

public function testUninitializedProperties(): void
{
$payload = new Issue189UserPatchInput();
$payload->firstName = 'John';
$payload->lastName = 'Doe';

/** @var Issue189User $data */
$data = $this->autoMapper->map($payload, Issue189User::class, [MapperContext::SKIP_UNINITIALIZED_VALUES => true]);

$this->assertEquals('John', $data->getFirstName());
$this->assertEquals('Doe', $data->getLastName());
$this->assertTrue(!isset($data->birthDate));
}
}
48 changes: 48 additions & 0 deletions tests/Fixtures/Issue189/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures\Issue189;

class User
{
public string $lastName;
public string $firstName;
public ?\DateTimeImmutable $birthDate;

public function getLastName(): string
{
return $this->lastName;
}

public function setLastName(string $lastName): self
{
$this->lastName = $lastName;

return $this;
}

public function getFirstName(): string
{
return $this->firstName;
}

public function setFirstName(string $firstName): self
{
$this->firstName = $firstName;

return $this;
}

public function getBirthDate(): ?\DateTimeImmutable
{
return $this->birthDate;
}

public function setBirthDate(?\DateTimeImmutable $birthDate): self
{
$this->birthDate = $birthDate;

return $this;
}
}
12 changes: 12 additions & 0 deletions tests/Fixtures/Issue189/UserPatchInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures\Issue189;

class UserPatchInput
{
public string $lastName;
public string $firstName;
public ?\DateTimeImmutable $birthDate;
}
Loading
Loading