Repository: JsonMapper/JsonMapper (php)

Issue:
Add support to call a method after the mapping is completed
In order to call a method at the end of mapping a simple middleware could be added that can be constructed with a callback. This will enable users to easily decorate their existing software as middleware for JsonMapper.

Consider the call back being invoked at each mapping (recursive) or only when leaving the top-level mapping method.

Reference: https://github.com/cweiske/jsonmapper/issues/142

Pull request description:
Add FinalCallback middleware; Resolves #5

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61fe2bc..3652424 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ 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 
+- New FinalCallback middleware to invoke a final callback when mapping is completed.
 
 ## [0.2.1] - 2020-03-25
 ### Fixed
diff --git a/src/JsonMapper.php b/src/JsonMapper.php
index 1131b8d..042564f 100644
--- a/src/JsonMapper.php
+++ b/src/JsonMapper.php
@@ -21,7 +21,7 @@ class JsonMapper implements JsonMapperInterface
         $this->handler = $handler;
     }
 
-    public function push(callable $middleware, string $name = ''): self
+    public function push(callable $middleware, string $name = ''): JsonMapperInterface
     {
         $this->stack[] = [$middleware, $name];
         $this->cached = null;
@@ -29,20 +29,6 @@ class JsonMapper implements JsonMapperInterface
         return $this;
     }
 
-    public function resolve(): callable
-    {
-        if (!$this->cached) {
-            $prev = $this->handler;
-            foreach (array_reverse($this->stack) as $fn) {
-                $prev = $fn[0]($prev);
-            }
-
-            $this->cached = $prev;
-        }
-
-        return $this->cached;
-    }
-
     public function mapObject(\stdClass $json, object $object): void
     {
         $propertyMap = new PropertyMap();
@@ -61,4 +47,18 @@ class JsonMapper implements JsonMapperInterface
 
         return $results;
     }
+
+    private function resolve(): callable
+    {
+        if (!$this->cached) {
+            $prev = $this->handler;
+            foreach (array_reverse($this->stack) as $fn) {
+                $prev = $fn[0]($prev);
+            }
+
+            $this->cached = $prev;
+        }
+
+        return $this->cached;
+    }
 }
diff --git a/src/JsonMapperInterface.php b/src/JsonMapperInterface.php
index 39c2989..2c38519 100644
--- a/src/JsonMapperInterface.php
+++ b/src/JsonMapperInterface.php
@@ -6,6 +6,8 @@ namespace JsonMapper;
 
 interface JsonMapperInterface
 {
+    public function push(callable $middleware, string $name = ''): self;
+
     public function mapObject(\stdClass $json, object $object): void;
 
     public function mapArray(array $json, object $object): array;
diff --git a/src/Middleware/FinalCallback.php b/src/Middleware/FinalCallback.php
new file mode 100644
index 0000000..fbfcc28
--- /dev/null
+++ b/src/Middleware/FinalCallback.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Middleware;
+
+use JsonMapper\JsonMapperInterface;
+use JsonMapper\ValueObjects\PropertyMap;
+use JsonMapper\Wrapper\ObjectWrapper;
+
+class FinalCallback implements MiddlewareInterface
+{
+    /** @var int */
+    private static $nestingLevel = 0;
+
+    /** @var callable */
+    private $callback;
+    /** @var bool */
+    private $onlyApplyCallBackOnTopLevel;
+
+    public function __construct(callable $callback, bool $onlyApplyCallBackOnTopLevel = true)
+    {
+        $this->callback = $callback;
+        $this->onlyApplyCallBackOnTopLevel = $onlyApplyCallBackOnTopLevel;
+    }
+
+    public function __invoke(callable $handler): callable
+    {
+        return function (
+            \stdClass $json,
+            ObjectWrapper $object,
+            PropertyMap $map,
+            JsonMapperInterface $mapper
+        ) use (
+            $handler
+        ) {
+            self::$nestingLevel++;
+            $handler($json, $object, $map, $mapper);
+            self::$nestingLevel--;
+
+            if (! $this->onlyApplyCallBackOnTopLevel || self::$nestingLevel === 0) {
+                call_user_func($this->callback, $json, $object, $map, $mapper);
+            }
+        };
+    }
+}


Test patch:
diff --git a/tests/Integration/Middleware/FinalCallbackTest.php b/tests/Integration/Middleware/FinalCallbackTest.php
new file mode 100644
index 0000000..549f2b9
--- /dev/null
+++ b/tests/Integration/Middleware/FinalCallbackTest.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Integration\Middleware;
+
+use JsonMapper\JsonMapperFactory;
+use JsonMapper\Middleware\FinalCallback;
+use JsonMapper\Tests\Implementation\ComplexObject;
+use PHPUnit\Framework\TestCase;
+
+class FinalCallbackTest extends TestCase
+{
+    public function testCallbackIsOnlyInvokedOnceOnNestedStructure(): void
+    {
+        $invocationCount = 0;
+        $callback = static function () use (&$invocationCount) {
+            $invocationCount++;
+        };
+        $mapper = (new JsonMapperFactory())->default();
+        $mapper->push(new FinalCallback($callback));
+        $object = new ComplexObject();
+        $json = (object) ['user' => (object) ['name' => __METHOD__]];
+
+        $mapper->mapObject($json, $object);
+
+        self::assertEquals(1, $invocationCount);
+    }
+
+    public function testCallbackIsInvokedForEveryPass(): void
+    {
+        $invocationCount = 0;
+        $callback = static function () use (&$invocationCount) {
+            $invocationCount++;
+        };
+        $mapper = (new JsonMapperFactory())->default();
+        $mapper->push(new FinalCallback($callback, false));
+        $object = new ComplexObject();
+        $json = (object) ['user' => (object) ['name' => __METHOD__]];
+
+        $mapper->mapObject($json, $object);
+
+        self::assertEquals(2, $invocationCount);
+    }
+}
diff --git a/tests/Unit/Middleware/FinalCallbackTest.php b/tests/Unit/Middleware/FinalCallbackTest.php
new file mode 100644
index 0000000..fecf3cb
--- /dev/null
+++ b/tests/Unit/Middleware/FinalCallbackTest.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Unit\Middleware;
+
+use JsonMapper\Handler\PropertyMapper;
+use JsonMapper\JsonMapperInterface;
+use JsonMapper\Middleware\FinalCallback;
+use JsonMapper\Tests\Implementation\SimpleObject;
+use JsonMapper\ValueObjects\PropertyMap;
+use JsonMapper\Wrapper\ObjectWrapper;
+use PHPUnit\Framework\TestCase;
+
+class FinalCallbackTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Middleware\FinalCallback
+     */
+    public function testCallbackIsInvoked(): void
+    {
+        $isCalled = false;
+        $middleware = new FinalCallback(static function () use (&$isCalled) {
+            $isCalled = true;
+        });
+        $object = new ObjectWrapper(new SimpleObject());
+        $function = $middleware->__invoke(new PropertyMapper());
+
+        $function(new \stdClass(), $object, new PropertyMap(), $this->createMock(JsonMapperInterface::class));
+
+        self::assertTrue($isCalled);
+    }
+}