Repository: JsonMapper/JsonMapper (php)

Issue:
Try and support PHP 7.1
**Is your feature request related to a problem? Please describe.**
In order to use JsonMapper in a version of Psalm, PHP7.1 support is needed.

**Describe the solution you'd like**
Ideally we should support older PHP versions in order to increase the number of potentials packages that can use this library.

**Describe alternatives you've considered**
None, lets just see if we can support PHP 7.1 without too many effort.

**Additional context**
None

Pull request description:
Allow JsonMapper work with PHP 7.1
This PR adds support for PHP 7.1 and would resolve #85.

Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index d7a74cf..79ee6f4 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -12,6 +12,13 @@ jobs:
     strategy:
       matrix:
         include:
+          - php: 7.1
+            allow_fail: false
+            name: 'PHP 7.1 with latest deps'
+          - php: 7.1
+            allow_fail: false
+            composer_update_flags: '--prefer-lowest --prefer-stable'
+            name: 'PHP 7.1 with lowest stable deps'
           - php: 7.2
             allow_fail: false
             name: 'PHP 7.2 with latest deps'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36c8f33..55154fa 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 
+- Support PHP 7.1 [PR#97](https://github.com/JsonMapper/JsonMapper/pull/97)
 
 ## [2.5.1] - 2021-07-06
 ### Fixed
diff --git a/composer.json b/composer.json
index 8ee24ba..b7b62fb 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,7 @@
     "license": "MIT",
     "minimum-stability": "stable",
     "require": {
-        "php": "^7.2 || ^8.0",
+        "php": "^7.1 || ^8.0",
         "ext-json": "*",
         "myclabs/php-enum": "^1.7",
         "nikic/php-parser": "^4.10",
@@ -24,7 +24,7 @@
         "squizlabs/php_codesniffer": "^3.5",
         "phpstan/phpstan": "^0.12.14",
         "php-coveralls/php-coveralls": "^2.4",
-        "phpunit/phpunit": "^8.5 || ^9.0",
+        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0",
         "guzzlehttp/guzzle": "^6.5 || ^7.0",
         "phpstan/phpstan-phpunit": "^0.12.17"
     },
diff --git a/src/Exception/TypeError.php b/src/Exception/TypeError.php
new file mode 100644
index 0000000..4588fec
--- /dev/null
+++ b/src/Exception/TypeError.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Exception;
+
+class TypeError extends \TypeError
+{
+    /** @param mixed $object */
+    public static function forObjectArgument(string $method, $object, int $argumentNumber): TypeError
+    {
+        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+        return new TypeError(sprintf(
+            '%s(): Argument #%d ($object) must be of type object, %s given, called in %s on line %d',
+            $method,
+            $argumentNumber,
+            gettype($object),
+            $trace[1]['file'],
+            $trace[1]['line']
+        ));
+    }
+}
diff --git a/src/Handler/PropertyMapper.php b/src/Handler/PropertyMapper.php
index 82f3b81..a7a33c7 100644
--- a/src/Handler/PropertyMapper.php
+++ b/src/Handler/PropertyMapper.php
@@ -17,7 +17,7 @@ class PropertyMapper
 {
     /** @var FactoryRegistry */
     private $classFactoryRegistry;
-    /**@var FactoryRegistry */
+    /** @var FactoryRegistry */
     private $nonInstantiableTypeResolver;
 
     public function __construct(
@@ -223,7 +223,7 @@ class PropertyMapper
     {
         if ($asArray) {
             return array_map(
-                function ($v) use ($type, $mapper): object {
+                function ($v) use ($type, $mapper) {
                     return $this->mapToObject($type, $v, false, $mapper);
                 },
                 (array) $value
@@ -242,8 +242,9 @@ class PropertyMapper
 
     /**
      * @param mixed $value
+     * @return object
      */
-    private function resolveUnInstantiableType(string $type, $value, JsonMapperInterface $mapper): object
+    private function resolveUnInstantiableType(string $type, $value, JsonMapperInterface $mapper)
     {
         try {
             $instance = $this->nonInstantiableTypeResolver->create($type, $value);
diff --git a/src/Helpers/UseStatementHelper.php b/src/Helpers/UseStatementHelper.php
index db9eee3..b334b62 100644
--- a/src/Helpers/UseStatementHelper.php
+++ b/src/Helpers/UseStatementHelper.php
@@ -10,6 +10,7 @@ use PhpParser\ParserFactory;
 
 class UseStatementHelper
 {
+    /** @var string */
     private static $evaldCodeFileNameEnding = "eval()'d code";
 
     public static function getImports(\ReflectionClass $class): array
diff --git a/src/JsonMapper.php b/src/JsonMapper.php
index 87a1fd5..7087c32 100644
--- a/src/JsonMapper.php
+++ b/src/JsonMapper.php
@@ -6,6 +6,7 @@ namespace JsonMapper;
 
 use JsonException;
 use JsonMapper\Dto\NamedMiddleware;
+use JsonMapper\Exception\TypeError;
 use JsonMapper\ValueObjects\PropertyMap;
 use JsonMapper\Wrapper\ObjectWrapper;
 
@@ -89,16 +90,26 @@ class JsonMapper implements JsonMapperInterface
         return $this;
     }
 
-    public function mapObject(\stdClass $json, object $object): void
+    /** @param object $object */
+    public function mapObject(\stdClass $json, $object): void
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $propertyMap = new PropertyMap();
 
         $handler = $this->resolve();
         $handler($json, new ObjectWrapper($object), $propertyMap, $this);
     }
 
-    public function mapArray(array $json, object $object): array
+    /** @param object $object */
+    public function mapArray(array $json, $object): array
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $results = [];
         foreach ($json as $key => $value) {
             $results[$key] = clone $object;
@@ -108,8 +119,13 @@ class JsonMapper implements JsonMapperInterface
         return $results;
     }
 
-    public function mapObjectFromString(string $json, object $object): void
+    /** @param object $object */
+    public function mapObjectFromString(string $json, $object): void
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $data = $this->decodeJsonString($json);
 
         if (! $data instanceof \stdClass) {
@@ -119,8 +135,13 @@ class JsonMapper implements JsonMapperInterface
         $this->mapObject($data, $object);
     }
 
-    public function mapArrayFromString(string $json, object $object): array
+    /** @param object $object */
+    public function mapArrayFromString(string $json, $object): array
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $data = $this->decodeJsonString($json);
 
         if (! is_array($data)) {
diff --git a/src/JsonMapperInterface.php b/src/JsonMapperInterface.php
index 39f33b9..802d89c 100644
--- a/src/JsonMapperInterface.php
+++ b/src/JsonMapperInterface.php
@@ -20,11 +20,15 @@ interface JsonMapperInterface
 
     public function removeByName(string $remove): self;
 
-    public function mapObject(\stdClass $json, object $object): void;
+    /** @param object $object */
+    public function mapObject(\stdClass $json, $object): void;
 
-    public function mapArray(array $json, object $object): array;
+    /** @param object $object */
+    public function mapArray(array $json, $object): array;
 
-    public function mapObjectFromString(string $jsonString, object $object): void;
+    /** @param object $object */
+    public function mapObjectFromString(string $jsonString, $object): void;
 
-    public function mapArrayFromString(string $jsonStrings, object $object): array;
+    /** @param object $object */
+    public function mapArrayFromString(string $jsonStrings, $object): array;
 }
diff --git a/src/Wrapper/ObjectWrapper.php b/src/Wrapper/ObjectWrapper.php
index 07e3041..dcdaf7a 100644
--- a/src/Wrapper/ObjectWrapper.php
+++ b/src/Wrapper/ObjectWrapper.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace JsonMapper\Wrapper;
 
+use JsonMapper\Exception\TypeError;
+
 class ObjectWrapper
 {
     /** @var object */
@@ -11,12 +13,18 @@ class ObjectWrapper
     /** @var \ReflectionClass */
     private $reflectedObject;
 
-    public function __construct(object $object)
+    /** @param object $object */
+    public function __construct($object)
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 1);
+        }
+
         $this->object = $object;
     }
 
-    public function getObject(): object
+    /** @return object */
+    public function getObject()
     {
         return $this->object;
     }


Test patch:
diff --git a/tests/Implementation/JsonMapper.php b/tests/Implementation/JsonMapper.php
index 04f3ac8..cee8e8f 100644
--- a/tests/Implementation/JsonMapper.php
+++ b/tests/Implementation/JsonMapper.php
@@ -4,7 +4,9 @@ declare(strict_types=1);
 
 namespace JsonMapper\Tests\Implementation;
 
+use JsonException;
 use JsonMapper\Dto\NamedMiddleware;
+use JsonMapper\Exception\TypeError;
 use JsonMapper\JsonMapperInterface;
 use JsonMapper\ValueObjects\PropertyMap;
 use JsonMapper\Wrapper\ObjectWrapper;
@@ -83,16 +85,26 @@ class JsonMapper implements JsonMapperInterface
         return $this;
     }
 
-    public function mapObject(\stdClass $json, object $object): void
+    /** @param object $object */
+    public function mapObject(\stdClass $json, $object): void
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $propertyMap = new PropertyMap();
 
         $handler = $this->resolve();
         $handler($json, new ObjectWrapper($object), $propertyMap, $this);
     }
 
-    public function mapArray(array $json, object $object): array
+    /** @param object $object */
+    public function mapArray(array $json, $object): array
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $results = [];
         foreach ($json as $key => $value) {
             $results[$key] = clone $object;
@@ -102,8 +114,13 @@ class JsonMapper implements JsonMapperInterface
         return $results;
     }
 
-    public function mapObjectFromString(string $json, object $object): void
+    /** @param object $object */
+    public function mapObjectFromString(string $json, $object): void
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $data = $this->decodeJsonString($json);
 
         if (! $data instanceof \stdClass) {
@@ -113,8 +130,13 @@ class JsonMapper implements JsonMapperInterface
         $this->mapObject($data, $object);
     }
 
-    public function mapArrayFromString(string $json, object $object): array
+    /** @param object $object */
+    public function mapArrayFromString(string $json, $object): array
     {
+        if (!is_object($object)) {
+            throw TypeError::forObjectArgument(__METHOD__, $object, 2);
+        }
+
         $data = $this->decodeJsonString($json);
 
         if (! is_array($data)) {
diff --git a/tests/Unit/Exception/TypeErrorTest.php b/tests/Unit/Exception/TypeErrorTest.php
new file mode 100644
index 0000000..613d267
--- /dev/null
+++ b/tests/Unit/Exception/TypeErrorTest.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Unit\Exception;
+
+use JsonMapper\Exception\TypeError;
+use PHPUnit\Framework\TestCase;
+
+class TypeErrorTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Exception\TypeError
+     */
+    public function testForObjectArgumentReturnsExpectedException(): void
+    {
+        $e = $this->createTypeError(__METHOD__, '', 1);
+
+        self::assertEquals(
+            sprintf(
+                '%s(): Argument #1 ($object) must be of type object, string given, called in %s on line %d',
+                __METHOD__,
+                __FILE__,
+                __LINE__ - 7
+            ),
+            $e->getMessage()
+        );
+    }
+
+    private function createTypeError(string $method, $object, int $argumentNumber): TypeError
+    {
+        return TypeError::forObjectArgument($method, $object, $argumentNumber);
+    }
+}
diff --git a/tests/Unit/Handler/PropertyMapperTest.php b/tests/Unit/Handler/PropertyMapperTest.php
index f57a05e..29365d5 100644
--- a/tests/Unit/Handler/PropertyMapperTest.php
+++ b/tests/Unit/Handler/PropertyMapperTest.php
@@ -175,7 +175,7 @@ class PropertyMapperTest extends TestCase
 
         $propertyMapper->__invoke($json, $wrapped, $propertyMap, $jsonMapper);
 
-        self::assertEquals(2, count($object->getChildren()));
+        self::assertCount(2, $object->getChildren());
     }
 
     /**
diff --git a/tests/Unit/JsonMapperTest.php b/tests/Unit/JsonMapperTest.php
index 49ce91d..4e685d2 100644
--- a/tests/Unit/JsonMapperTest.php
+++ b/tests/Unit/JsonMapperTest.php
@@ -228,4 +228,72 @@ class JsonMapperTest extends TestCase
 
         self::assertTrue($this->handler->isCalled());
     }
+
+    /**
+     * @covers \JsonMapper\JsonMapper
+     */
+    public function testMapObjectWithInvalidObjectThrowsTypeException(): void
+    {
+        $jsonMapper = new JsonMapper($this->handler);
+
+        $this->expectException(\TypeError::class);
+        $this->expectExceptionMessage(sprintf(
+            '%s::mapObject(): Argument #2 ($object) must be of type object, string given, called in %s on line %d',
+            get_class($jsonMapper),
+            __FILE__,
+            __LINE__ + 2
+        ));
+        $jsonMapper->mapObject(new \stdClass(), '');
+    }
+
+    /**
+     * @covers \JsonMapper\JsonMapper
+     */
+    public function testMapObjectFromStringWithInvalidObjectThrowsTypeException(): void
+    {
+        $jsonMapper = new JsonMapper($this->handler);
+
+        $this->expectException(\TypeError::class);
+        $this->expectExceptionMessage(sprintf(
+            '%s::mapObjectFromString(): Argument #2 ($object) must be of type object, string given, called in %s on line %d',
+            get_class($jsonMapper),
+            __FILE__,
+            __LINE__ + 2
+        ));
+        $jsonMapper->mapObjectFromString('', '');
+    }
+
+    /**
+     * @covers \JsonMapper\JsonMapper
+     */
+    public function testMapArrayWithInvalidObjectThrowsTypeException(): void
+    {
+        $jsonMapper = new JsonMapper($this->handler);
+
+        $this->expectException(\TypeError::class);
+        $this->expectExceptionMessage(sprintf(
+            '%s::mapArray(): Argument #2 ($object) must be of type object, string given, called in %s on line %d',
+            get_class($jsonMapper),
+            __FILE__,
+            __LINE__ + 2
+        ));
+        $jsonMapper->mapArray([], '');
+    }
+
+    /**
+     * @covers \JsonMapper\JsonMapper
+     */
+    public function testMapArrayFromStringWithInvalidObjectThrowsTypeException(): void
+    {
+        $jsonMapper = new JsonMapper($this->handler);
+
+        $this->expectException(\TypeError::class);
+        $this->expectExceptionMessage(sprintf(
+            '%s::mapArrayFromString(): Argument #2 ($object) must be of type object, string given, called in %s on line %d',
+            get_class($jsonMapper),
+            __FILE__,
+            __LINE__ + 2
+        ));
+        $jsonMapper->mapArrayFromString('', '');
+    }
 }
diff --git a/tests/Unit/Wrapper/ObjectWrapperTest.php b/tests/Unit/Wrapper/ObjectWrapperTest.php
index 648de97..3b1a19f 100644
--- a/tests/Unit/Wrapper/ObjectWrapperTest.php
+++ b/tests/Unit/Wrapper/ObjectWrapperTest.php
@@ -4,11 +4,27 @@ declare(strict_types=1);
 
 namespace JsonMapper\Tests\Unit\Wrapper;
 
+use JsonMapper\JsonMapper;
 use JsonMapper\Wrapper\ObjectWrapper;
 use PHPUnit\Framework\TestCase;
 
 class ObjectWrapperTest extends TestCase
 {
+    /**
+     * @covers \JsonMapper\Wrapper\ObjectWrapper
+     */
+    public function testConstructorInvalidObjectThrowsTypeException(): void
+    {
+        $this->expectException(\TypeError::class);
+        $this->expectExceptionMessage(sprintf(
+            '%s::__construct(): Argument #1 ($object) must be of type object, string given, called in %s on line %d',
+            ObjectWrapper::class,
+            __FILE__,
+            __LINE__ + 2
+        ));
+        new ObjectWrapper('');
+    }
+
     /**
      * @covers \JsonMapper\Wrapper\ObjectWrapper
      */