Repository: JsonMapper/JsonMapper (php)

Issue:
NamespaceResolver doesn't cache anything resulting in imports being fetched on every run
**Describe the bug**
The first loc that gets executed on the NamespaceResolver is:
```php
$imports = UseStatementHelper::getImports($object->getReflectedObject());
```
without caching or any other optimisation. This means the umber of times an object is mapped is equal the the number
of times the imports are loaded. Even when mapping the same object. The `UseStatementHelper::getImports` is an expensive call as it loads the object class source code into an AST to fetch the used imports.

**To Reproduce**
I've created a vanilla script 
```php
<?php
# ./tests/Benchmark/vanilla.php

declare(strict_types=1);

use JsonMapper\JsonMapperFactory;
use JsonMapper\Tests\Benchmark\Joke;

chdir(__DIR__ . '/../../');
require_once 'vendor/autoload.php';

$mapper = (new JsonMapperFactory())->bestFit();

for ($x = 1; $x < 1000; $x++) {
    $joke = new Joke();
    $json = '{"id":131,"type":"general","setup":"How do you organize a space party?","punchline":"You planet."}';
    $mapper->mapObjectFromString($json, $joke);
}
```
which I've executed with the Xdebug profile enabled. See the screenshot attached. It shows that getting the imports took 80%time.
<img width="1570" alt="Screenshot 2021-04-15 at 21 01 20" src="https://user-images.githubusercontent.com/618940/114924040-d0e02700-9e2d-11eb-929e-5d339eb57416.png">

**Expected behavior**
Since the source code is not expected to change at runtime we should cache the results similar how the other middleware does this.

**Stacktrace**

**Environment (please complete the following information):**
 - PHP: 8.0
 - XDebug: 3.0
 
**Additional context**
This research was triggered by the question from @TeroBlaZe in #80 


Pull request description:
Resolve lack of cache in namespace resolver middleware
The number are of the charts with 5000 iterations the time required is reduced to 20% of the original and memory usage is only 0.2% of the original. All because for the same object the `UseStatementHelper::getImports($object->getReflectedObject());` is only being called once instead of N times (where N equals the amount of `mapObject` or `mapArray` is being called.

Would resolve #81 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a11ca7..279aa06 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
+- Caching to the namespace resolved was added to improve on time and memory [PR#82](https://github.com/JsonMapper/JsonMapper/pull/82)
 
 ## [2.3.1] - 2021-03-30
 ### Fixed
diff --git a/src/JsonMapperBuilder.php b/src/JsonMapperBuilder.php
index 3dee4c6..0798c87 100644
--- a/src/JsonMapperBuilder.php
+++ b/src/JsonMapperBuilder.php
@@ -87,9 +87,9 @@ class JsonMapperBuilder
         return $this->withMiddleware(new DocBlockAnnotations($cache ?: clone $this->defaultCache), DocBlockAnnotations::class);
     }
 
-    public function withNamespaceResolverMiddleware(): JsonMapperBuilder
+    public function withNamespaceResolverMiddleware(?CacheInterface $cache = null): JsonMapperBuilder
     {
-        return $this->withMiddleware(new NamespaceResolver(), NamespaceResolver::class);
+        return $this->withMiddleware(new NamespaceResolver($cache ?: clone $this->defaultCache), NamespaceResolver::class);
     }
 
     public function withTypedPropertiesMiddleware(?CacheInterface $cache = null): JsonMapperBuilder
diff --git a/src/Middleware/NamespaceResolver.php b/src/Middleware/NamespaceResolver.php
index c0c999b..0bc89e5 100644
--- a/src/Middleware/NamespaceResolver.php
+++ b/src/Middleware/NamespaceResolver.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace JsonMapper\Middleware;
 
+use JsonMapper\Cache\NullCache;
 use JsonMapper\Enums\ScalarType;
 use JsonMapper\Helpers\ClassHelper;
 use JsonMapper\Helpers\UseStatementHelper;
@@ -12,25 +13,49 @@ use JsonMapper\ValueObjects\Property;
 use JsonMapper\ValueObjects\PropertyMap;
 use JsonMapper\ValueObjects\PropertyType;
 use JsonMapper\Wrapper\ObjectWrapper;
+use Psr\SimpleCache\CacheInterface;
 
 class NamespaceResolver extends AbstractMiddleware
 {
+    /** @var CacheInterface|null */
+    private $cache;
+
+    public function __construct(CacheInterface $cache = null)
+    {
+        $this->cache = $cache ?? new NullCache();
+    }
+
     public function handle(
         \stdClass $json,
         ObjectWrapper $object,
         PropertyMap $propertyMap,
         JsonMapperInterface $mapper
     ): void {
+        $propertyMap->merge($this->fetchPropertyMapForObject($object, $propertyMap));
+    }
+
+    private function fetchPropertyMapForObject(ObjectWrapper $object, PropertyMap $originalPropertyMap): PropertyMap
+    {
+        $cacheKey = sprintf('%s::Cache::%s', __CLASS__, $object->getName());
+        if ($this->cache->has($cacheKey)) {
+            return $this->cache->get($cacheKey);
+        }
+
+        $intermediatePropertyMap = new PropertyMap();
         $imports = UseStatementHelper::getImports($object->getReflectedObject());
 
         /** @var Property $property */
-        foreach ($propertyMap as $property) {
+        foreach ($originalPropertyMap as $property) {
             $types = $property->getPropertyTypes();
             foreach ($types as $index => $type) {
                 $types[$index] = $this->resolveSingleType($type, $object, $imports);
             }
-            $propertyMap->addProperty($property->asBuilder()->setTypes(...$types)->build());
+            $intermediatePropertyMap->addProperty($property->asBuilder()->setTypes(...$types)->build());
         }
+
+        $this->cache->set($cacheKey, $intermediatePropertyMap);
+
+        return $intermediatePropertyMap;
     }
 
     private function resolveSingleType(PropertyType $type, ObjectWrapper $object, array $imports): PropertyType


Test patch:
diff --git a/tests/Unit/Middleware/NamespaceResolverTest.php b/tests/Unit/Middleware/NamespaceResolverTest.php
index b75fb0f..620d027 100644
--- a/tests/Unit/Middleware/NamespaceResolverTest.php
+++ b/tests/Unit/Middleware/NamespaceResolverTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace JsonMapper\Tests\Unit\Middleware;
 
 use JsonMapper\Builders\PropertyBuilder;
+use JsonMapper\Cache\NullCache;
 use JsonMapper\Enums\Visibility;
 use JsonMapper\JsonMapperInterface;
 use JsonMapper\Middleware\NamespaceResolver;
@@ -14,7 +15,9 @@ use JsonMapper\Tests\Implementation\Models\User;
 use JsonMapper\Tests\Implementation\SimpleObject;
 use JsonMapper\ValueObjects\PropertyMap;
 use JsonMapper\Wrapper\ObjectWrapper;
+use PHPUnit\Framework\Assert;
 use PHPUnit\Framework\TestCase;
+use Psr\SimpleCache\CacheInterface;
 
 class NamespaceResolverTest extends TestCase
 {
@@ -25,7 +28,7 @@ class NamespaceResolverTest extends TestCase
      */
     public function testItResolvesNamespacesForImportedNamespace(): void
     {
-        $middleware = new NamespaceResolver();
+        $middleware = new NamespaceResolver(new NullCache());
         $object = new ComplexObject();
         $property = PropertyBuilder::new()
             ->setName('user')
@@ -49,7 +52,7 @@ class NamespaceResolverTest extends TestCase
      */
     public function testItResolvesNamespacesWithinSameNamespace(): void
     {
-        $middleware = new NamespaceResolver();
+        $middleware = new NamespaceResolver(new NullCache());
         $object = new ComplexObject();
         $property = PropertyBuilder::new()
             ->setName('child')
@@ -73,7 +76,7 @@ class NamespaceResolverTest extends TestCase
      */
     public function testItDoesntApplyResolvingToScalarTypes(): void
     {
-        $middleware = new NamespaceResolver();
+        $middleware = new NamespaceResolver(new NullCache());
         $object = new SimpleObject();
         $property = PropertyBuilder::new()
             ->setName('name')
@@ -97,7 +100,7 @@ class NamespaceResolverTest extends TestCase
      */
     public function testItDoesntApplyResolvingToFullyQualifiedClassName(): void
     {
-        $middleware = new NamespaceResolver();
+        $middleware = new NamespaceResolver(new NullCache());
         $object = new SimpleObject();
         $property = PropertyBuilder::new()
             ->setName('name')
@@ -121,7 +124,7 @@ class NamespaceResolverTest extends TestCase
      */
     public function testItResolvesNamespacesForImportedNamespaceWithArray(): void
     {
-        $middleware = new NamespaceResolver();
+        $middleware = new NamespaceResolver(new NullCache());
         $object = new ComplexObject();
         $property = PropertyBuilder::new()
             ->setName('user')
@@ -145,7 +148,7 @@ class NamespaceResolverTest extends TestCase
      */
     public function testItResolvesNamespacesWithinSameNamespaceWithArray(): void
     {
-        $middleware = new NamespaceResolver();
+        $middleware = new NamespaceResolver(new NullCache());
         $object = new ComplexObject();
         $property = PropertyBuilder::new()
             ->setName('child')
@@ -163,4 +166,22 @@ class NamespaceResolverTest extends TestCase
         $this->assertThatProperty($propertyMap->getProperty('child'))
             ->hasType(SimpleObject::class . '[]', false);
     }
+
+    /**
+     * @covers \JsonMapper\Middleware\NamespaceResolver
+     */
+    public function testReturnsFromCacheWhenAvailable(): void
+    {
+        $propertyMap = new PropertyMap();
+        $objectWrapper = $this->createMock(ObjectWrapper::class);
+        $objectWrapper->method('getName')->willReturn(__METHOD__);
+        $objectWrapper->expects(self::never())->method('getReflectedObject');
+        $cache = $this->createMock(CacheInterface::class);
+        $cache->method('has')->with(Assert::stringContains(__METHOD__))->willReturn(true);
+        $cache->method('get')->with(Assert::stringContains(__METHOD__))->willReturn($propertyMap);
+        $middleware = new NamespaceResolver($cache);
+        $jsonMapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle(new \stdClass(), $objectWrapper, $propertyMap, $jsonMapper);
+    }
 }
diff --git a/tests/benchmark/Joke.php b/tests/benchmark/Joke.php
new file mode 100644
index 0000000..f4631ea
--- /dev/null
+++ b/tests/benchmark/Joke.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Benchmark;
+
+class Joke
+{
+    public int $id;
+    public string $type;
+    public string $setup;
+    public string $punchline;
+}
diff --git a/tests/benchmark/vanilla.php b/tests/benchmark/vanilla.php
new file mode 100644
index 0000000..31bb292
--- /dev/null
+++ b/tests/benchmark/vanilla.php
@@ -0,0 +1,23 @@
+<?php
+
+# In order to get a cache grind output file (assuming your on PHP8 with Xdebug 3.x)
+# add the following lines to your php.ini:
+
+# xdebug.mode = profile;
+# xdebug.output_dir = "/Users/dannyvandersluijs/Projects/JsonMapper/build/profiles"
+
+declare(strict_types=1);
+
+use JsonMapper\JsonMapperFactory;
+use JsonMapper\Tests\Benchmark\Joke;
+
+chdir(__DIR__ . '/../../');
+require_once 'vendor/autoload.php';
+
+$mapper = (new JsonMapperFactory())->bestFit();
+
+for ($x = 1; $x < 5000; $x++) {
+    $joke = new Joke();
+    $json = '{"id":131,"type":"general","setup":"How do you organize a space party?","punchline":"You planet."}';
+    $mapper->mapObjectFromString($json, $joke);
+}