Repository: JsonMapper/JsonMapper (php)

Issue:
Add a middleware to utilise the PHP 8 Attribute
In order to help easy the mapping JsonMapper should utilise the PHP 8 Attribute options (See https://wiki.php.net/rfc/attributes_v2#alternative_syntaxwhy_not_use_or_like_other_languages)
It should introduce a middleware that processes the Attribute feature go php

The original RFC is tracked [here](https://wiki.php.net/rfc/attributes_v2)

Pull request description:
Add Attributes middleware; Small tweaks for PHP8.0 builds
Would resolve #33 

Patch:
diff --git a/.travis.yml b/.travis.yml
index 7f474f0..79caf71 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,11 +19,12 @@ matrix:
 
       # Ignore the platform requirements for the upcoming PHP version
     - php: nightly
-      env: COMPOSER_FLAGS='--ignore-platform-reqs'
+      env:
+          PHP_INI='xdebug.mode=coverage'
     - php: nightly
       env:
-        - COMPOSER_FLAGS='--ignore-platform-reqs'
-          COMPOSER_UPDATE_FLAGS='--prefer-lowest --prefer-stable --ignore-platform-reqs'
+          COMPOSER_UPDATE_FLAGS='--prefer-lowest --prefer-stable'
+          PHP_INI='xdebug.mode=coverage'
   allow_failures:
     - php: nightly
   fast_finish: true
@@ -35,8 +36,9 @@ branches:
 
 install:
   - curl -s http://getcomposer.org/installer | php
-  - php composer.phar install --no-interaction $COMPOSER_FLAGS
+  - php composer.phar install --no-interaction
   - if [[ -n $COMPOSER_UPDATE_FLAGS ]]; then php composer.phar update $COMPOSER_UPDATE_FLAGS; fi
+  - if [[ -n $PHP_INI ]]; then echo $PHP_INI >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi
 
 script:
   - mkdir -p build/logs;
diff --git a/composer.json b/composer.json
index 9578146..c220094 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,7 @@
         "php": "^7.2 || ^8.0",
         "ext-json": "*",
         "myclabs/php-enum": "^1.7",
-        "nikic/php-parser": "^4.3",
+        "nikic/php-parser": "^4.10",
         "psr/log": "^1.1",
         "psr/simple-cache": "^1.0",
         "symfony/polyfill-php73": "^1.18"
diff --git a/src/Middleware/Attributes/Attributes.php b/src/Middleware/Attributes/Attributes.php
new file mode 100644
index 0000000..8f3d359
--- /dev/null
+++ b/src/Middleware/Attributes/Attributes.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Middleware\Attributes;
+
+use JsonMapper\JsonMapperInterface;
+use JsonMapper\Middleware\AbstractMiddleware;
+use JsonMapper\ValueObjects\PropertyMap;
+use JsonMapper\Wrapper\ObjectWrapper;
+use ReflectionClass;
+
+class Attributes extends AbstractMiddleware
+{
+    public function handle(\stdClass $json, ObjectWrapper $object, PropertyMap $propertyMap, JsonMapperInterface $mapper): void
+    {
+        $reflectionClass = new ReflectionClass($object->getObject());
+
+        foreach ($reflectionClass->getProperties() as $property) {
+            $attributes = $property->getAttributes(MapFrom::class);
+
+            foreach ($attributes as $attribute) {
+                /** @var MapFrom $mapFrom */
+                $mapFrom = $attribute->newInstance();
+                $source = $mapFrom->source;
+                $target = $property->name;
+
+                if ($source === $target) {
+                    continue;
+                }
+
+                if (isset($json->$source)) {
+                    $json->$target = $json->$source;
+                    unset($json->$source);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Middleware/Attributes/MapFrom.php b/src/Middleware/Attributes/MapFrom.php
new file mode 100644
index 0000000..e5bca1e
--- /dev/null
+++ b/src/Middleware/Attributes/MapFrom.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Middleware\Attributes;
+
+use Attribute;
+
+#[Attribute]
+class MapFrom
+{
+    /** @var string */
+    public $source;
+
+    public function __construct(string $source)
+    {
+        $this->source = $source;
+    }
+}
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
index 78a6f53..6696f41 100644
--- a/.github/workflows/run-tests.yaml
+++ b/.github/workflows/run-tests.yaml
@@ -35,10 +35,12 @@ jobs:
             name: 'PHP 7.4 with lowest stable deps'
           - php: 8.0
             allow_fail: true
+            php_ini: 'xdebug.coverage_enable=On'
             name: 'PHP 8.0 with latest deps'
           - php: 8.0
             allow_fail: true
             composer_update_flags: '--prefer-lowest --prefer-stable'
+            php_ini: 'xdebug.coverage_enable=On'
             name: 'PHP 8.0 with lowest stable deps'
 
     runs-on: ubuntu-latest
@@ -49,23 +51,14 @@ jobs:
     - name: Validate composer.json and composer.lock
       run: composer validate
 
-    - name: Cache Composer packages
-      id: composer-cache
-      uses: actions/cache@v2
-      with:
-        path: vendor
-        key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-php-
-
     - name: Setup PHP ${{ matrix.php }}
       uses: shivammathur/setup-php@v2
       with:
         php-version: ${{ matrix.php }}
         coverage: xdebug
+        ini-values: ${{ matrix.php_ini }}
 
     - name: Install dependencies
-      if: steps.composer-cache.outputs.cache-hit != 'true'
       run: composer install ${{ matrix.composer_flags }}
       continue-on-error: ${{ matrix.allow_fail }}
 
diff --git a/tests/Implementation/Php80/AttributePopo.php b/tests/Implementation/Php80/AttributePopo.php
new file mode 100644
index 0000000..87eab0f
--- /dev/null
+++ b/tests/Implementation/Php80/AttributePopo.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Php80;
+
+use JsonMapper\Middleware\Attributes\MapFrom;
+
+class AttributePopo
+{
+    #[MapFrom("Identifier")]
+    public int $id;
+    #[MapFrom("UserName")]
+    public string $name;
+    #[MapFrom("email")]
+    public string $email;
+}
\ No newline at end of file
diff --git a/tests/Integration/Middleware/Attributes/AttributeTest.php b/tests/Integration/Middleware/Attributes/AttributeTest.php
new file mode 100644
index 0000000..48a341b
--- /dev/null
+++ b/tests/Integration/Middleware/Attributes/AttributeTest.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Integration\Middleware\Attributes;
+
+use JsonMapper\Cache\NullCache;
+use JsonMapper\Handler\PropertyMapper;
+use JsonMapper\JsonMapperFactory;
+use JsonMapper\Middleware\Attributes\Attributes;
+use JsonMapper\Middleware\TypedProperties;
+use JsonMapper\Tests\Implementation\Php80\AttributePopo;
+use PHPUnit\Framework\TestCase;
+
+class AttributeTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Middleware\Attributes\Attributes
+     * @requires PHP >= 8.0
+     */
+    public function testAttributesMiddlewareDoesMapFrom(): void
+    {
+        $cache = new NullCache();
+        $mapper = (new JsonMapperFactory())->create(new PropertyMapper(), new Attributes(), new TypedProperties($cache));
+        $object = new AttributePopo();
+        $json = json_encode(['Identifier' => 42, 'UserName' => 'John Doe']);
+
+        $mapper->mapObjectFromString($json, $object);
+
+        self::assertSame(42, $object->id);
+        self::assertSame('John Doe', $object->name);
+    }
+}
\ No newline at end of file
diff --git a/tests/Unit/Middleware/Attributes/AttributesTest.php b/tests/Unit/Middleware/Attributes/AttributesTest.php
new file mode 100644
index 0000000..3bec09d
--- /dev/null
+++ b/tests/Unit/Middleware/Attributes/AttributesTest.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Unit\Middleware\Attributes;
+
+use JsonMapper\JsonMapperInterface;
+use JsonMapper\Middleware\Attributes\Attributes;
+use JsonMapper\Tests\Implementation\Php80\AttributePopo;
+use JsonMapper\ValueObjects\PropertyMap;
+use JsonMapper\Wrapper\ObjectWrapper;
+use PHPUnit\Framework\TestCase;
+
+class AttributesTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Middleware\Attributes\Attributes
+     * @requires PHP >= 8.0
+     */
+    public function testAttributesMiddlewareDoesMapFrom(): void
+    {
+        $json = (object) ['Identifier' => 42, 'UserName' => 'John Doe'];
+        $object = new AttributePopo();
+        $propertyMap = new PropertyMap();
+        $middleware = new Attributes();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($json, new ObjectWrapper($object), $propertyMap, $mapper);
+
+        self::assertEquals((object) ['id' => 42, 'name' => 'John Doe'], $json);
+    }
+
+    /**
+     * @covers \JsonMapper\Middleware\Attributes\Attributes
+     * @requires PHP >= 8.0
+     */
+    public function testAttributesMiddlewareWithPartialDataDoesMapFrom(): void
+    {
+        $json = (object) ['Identifier' => 42];
+        $object = new AttributePopo();
+        $propertyMap = new PropertyMap();
+        $middleware = new Attributes();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($json, new ObjectWrapper($object), $propertyMap, $mapper);
+
+        self::assertEquals((object) ['id' => 42], $json);
+    }
+
+    /**
+     * @covers \JsonMapper\Middleware\Attributes\Attributes
+     * @requires PHP >= 8.0
+     */
+    public function testAttributesMiddlewareWhenSourceAndTargetAreEqualDoesntRemoveSource(): void
+    {
+        $json = (object) ['email' => 'JohnDoe@example.org'];
+        $object = new AttributePopo();
+        $propertyMap = new PropertyMap();
+        $middleware = new Attributes();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($json, new ObjectWrapper($object), $propertyMap, $mapper);
+
+        self::assertEquals((object) ['email' => 'JohnDoe@example.org'], $json);
+    }
+}
diff --git a/tests/Unit/Middleware/Attributes/MapFromTest.php b/tests/Unit/Middleware/Attributes/MapFromTest.php
new file mode 100644
index 0000000..e756c21
--- /dev/null
+++ b/tests/Unit/Middleware/Attributes/MapFromTest.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Unit\Middleware\Attributes;
+
+use JsonMapper\Middleware\Attributes\MapFrom;
+use PHPUnit\Framework\TestCase;
+
+class MapFromTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Middleware\Attributes\MapFrom
+     */
+    public function testConstructorSetsProperties(): void
+    {
+        $source = __CLASS__;
+        $mapFrom = new MapFrom($source);
+
+        self::assertEquals(__CLASS__, $mapFrom->source);
+    }
+}