Repository: janephp/janephp (php)

Issue:
Add a `skip-null-values` in context when using DataMapper
## Description

I currently have 2 objects. A POPO container public properties values called `Input` and a entity called `MyEntity` containing the same properties with getters / setters.
I want to map an `Input` instance to a `MyEntity` one **but** when a property value is `null` in the `Input` instance, I want to ignore it in order not to overwrite the existing value in `MyEntity`.

## Example

My classes:

```php
class Input
{
  public ?string $name = null;
}
```

```php
class MyEntity
{
  private string $name;
  public function setName(string $name) {
    $this->name = $name;
  }
  public function getName() {
    return $this->name;
  }
}
```

Given an existing `MyEntity` instance and an `Input` one like this:

```php
$myEntity = new MyEntity();
$myEntity->setName('foobar');

$input = new Input();
```

Here is the current behavior when mapping the `Input` to the `MyEntity`:

```php
$autoMapper->map($input, $myEntity);

echo $myEntity->getName(); // null
```

Here is what I would like to do:

```php
$autoMapper->map($input, $myEntity, ['skip-null-values' => true]);

echo $myEntity->getName(); // "foobar"
```

## Proposed solution

Using context seems to be the only possible solution.
The name `skip-null-values` is directly inspired by the option name available when generating a client from JSON Schema / Open API ([see here](https://jane.readthedocs.io/en/latest/components/JsonSchema.html?highlight=skip#options)).

Pull request:
[AutoMapper] Add `skip_null_values` feature
Fixes #434

Will add a new context option: `skip_null_values`, that will skip automapping when the value is `null`.
With this feature all already generated should be removed since `\Jane\Component\AutoMapper\MapperContext::isAllowedAttribute` method has changed.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50c26a91e..522383d8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [AutoMapper] [GH#433](https://github.com/janephp/janephp/pull/433) Handle dictionaries with ArrayTransformer
 - [AutoMapper] [GH#432](https://github.com/janephp/janephp/pull/432) Ignore API Platform resources when using AutoMapper normalizer
 - [AutoMapper] [GH#495](https://github.com/janephp/janephp/pull/495) Add Symfony Uid transformers #495
+- [AutoMapper] [GH#507](https://github.com/janephp/janephp/pull/507) Add `skip_null_values` feature
 
 ### Changed
 - [Jane] [GH#464](https://github.com/janephp/janephp/pull/464) Update version deps for 7-dev
diff --git a/src/Component/AutoMapper/Generator/Generator.php b/src/Component/AutoMapper/Generator/Generator.php
index b9ec69827..4637b5992 100644
--- a/src/Component/AutoMapper/Generator/Generator.php
+++ b/src/Component/AutoMapper/Generator/Generator.php
@@ -142,7 +142,8 @@ final class Generator
                 continue;
             }
 
-            [$output, $propStatements] = $transformer->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $result, $propertyMapping, $uniqueVariableScope);
+            $sourcePropertyAccessor = $propertyMapping->getReadAccessor()->getExpression($sourceInput);
+            [$output, $propStatements] = $transformer->transform($sourcePropertyAccessor, $result, $propertyMapping, $uniqueVariableScope);
             $writeExpression = $propertyMapping->getWriteMutator()->getExpression($result, $output, $transformer instanceof AssignedByReferenceTransformerInterface ? $transformer->assignByRef() : false);
 
             if (null === $writeExpression) {
@@ -189,6 +190,7 @@ final class Generator
                 $conditions[] = new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'isAllowedAttribute', [
                     new Arg($contextVariable),
                     new Arg(new Scalar\String_($propertyMapping->getProperty())),
+                    new Arg($sourcePropertyAccessor),
                 ]);
             }
 
diff --git a/src/Component/AutoMapper/MapperContext.php b/src/Component/AutoMapper/MapperContext.php
index b6c1c7370..f2a74343b 100644
--- a/src/Component/AutoMapper/MapperContext.php
+++ b/src/Component/AutoMapper/MapperContext.php
@@ -23,6 +23,7 @@ class MapperContext
     public const DEPTH = 'depth';
     public const TARGET_TO_POPULATE = 'target_to_populate';
     public const CONSTRUCTOR_ARGUMENTS = 'constructor_arguments';
+    public const SKIP_NULL_VALUES = 'skip_null_values';
 
     private $context = [
         self::DEPTH => 0,
@@ -156,8 +157,12 @@ class MapperContext
     /**
      * Check whether an attribute is allowed to be mapped.
      */
-    public static function isAllowedAttribute(array $context, string $attribute): bool
+    public static function isAllowedAttribute(array $context, string $attribute, $value): bool
     {
+        if (($context[self::SKIP_NULL_VALUES] ?? false) && null === $value) {
+            return false;
+        }
+
         if (($context[self::IGNORED_ATTRIBUTES] ?? false) && \in_array($attribute, $context[self::IGNORED_ATTRIBUTES], true)) {
             return false;
         }


Test patch:
diff --git a/src/Component/AutoMapper/Tests/AutoMapperTest.php b/src/Component/AutoMapper/Tests/AutoMapperTest.php
index 453e0e44c..9808d7325 100644
--- a/src/Component/AutoMapper/Tests/AutoMapperTest.php
+++ b/src/Component/AutoMapper/Tests/AutoMapperTest.php
@@ -850,4 +850,15 @@ class AutoMapperTest extends AutoMapperBaseTest
         self::assertEquals($uuidV4->toRfc4122(), $newUser->getUuid()->toRfc4122());
         self::assertEquals('Grégoire Pineau', $newUser->name);
     }
+
+    public function testSkipNullValues(): void
+    {
+        $entity = new Fixtures\SkipNullValues\Entity();
+        $entity->setName('foobar');
+        $input = new Fixtures\SkipNullValues\Input();
+
+        /** @var Fixtures\SkipNullValues\Entity $entity */
+        $entity = $this->autoMapper->map($input, $entity, ['skip_null_values' => true]);
+        self::assertEquals('foobar', $entity->getName());
+    }
 }
diff --git a/src/Component/AutoMapper/Tests/Fixtures/SkipNullValues/Entity.php b/src/Component/AutoMapper/Tests/Fixtures/SkipNullValues/Entity.php
new file mode 100644
index 000000000..fe0561ea0
--- /dev/null
+++ b/src/Component/AutoMapper/Tests/Fixtures/SkipNullValues/Entity.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Tests\Fixtures\SkipNullValues;
+
+class Entity
+{
+    /**
+     * @var string
+     */
+    private $name;
+
+    public function setName(string $name): void
+    {
+        $this->name = $name;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+}
diff --git a/src/Component/AutoMapper/Tests/Fixtures/SkipNullValues/Input.php b/src/Component/AutoMapper/Tests/Fixtures/SkipNullValues/Input.php
new file mode 100644
index 000000000..8631b3917
--- /dev/null
+++ b/src/Component/AutoMapper/Tests/Fixtures/SkipNullValues/Input.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Tests\Fixtures\SkipNullValues;
+
+class Input
+{
+    /**
+     * @var string|null
+     */
+    public $name = null;
+}
diff --git a/src/Component/AutoMapper/Tests/MapperContextTest.php b/src/Component/AutoMapper/Tests/MapperContextTest.php
index 8682b3094..6e037b665 100644
--- a/src/Component/AutoMapper/Tests/MapperContextTest.php
+++ b/src/Component/AutoMapper/Tests/MapperContextTest.php
@@ -17,9 +17,9 @@ class MapperContextTest extends TestCase
         $context->setAllowedAttributes(['id', 'age']);
         $context->setIgnoredAttributes(['age']);
 
-        self::assertTrue(MapperContext::isAllowedAttribute($context->toArray(), 'id'));
-        self::assertFalse(MapperContext::isAllowedAttribute($context->toArray(), 'age'));
-        self::assertFalse(MapperContext::isAllowedAttribute($context->toArray(), 'name'));
+        self::assertTrue(MapperContext::isAllowedAttribute($context->toArray(), 'id', 1));
+        self::assertFalse(MapperContext::isAllowedAttribute($context->toArray(), 'age', 29));
+        self::assertFalse(MapperContext::isAllowedAttribute($context->toArray(), 'name', 'Baptiste'));
     }
 
     public function testCircularReferenceLimit(): void
@@ -129,4 +129,10 @@ class MapperContextTest extends TestCase
 
         self::assertEquals(['bar'], $newContext[MapperContext::ALLOWED_ATTRIBUTES]);
     }
+
+    public function testSkipNullValues(): void
+    {
+        $context = [MapperContext::SKIP_NULL_VALUES => true];
+        self::assertFalse(MapperContext::isAllowedAttribute($context, 'id', null));
+    }
 }