IGNITE-10022: JS, PHP thin clients: a more meaningful exception when
ENUM type is not registered

This closes #5187
diff --git a/src/Apache/Ignite/Internal/Binary/BinaryCommunicator.php b/src/Apache/Ignite/Internal/Binary/BinaryCommunicator.php
index 520063c..781a730 100644
--- a/src/Apache/Ignite/Internal/Binary/BinaryCommunicator.php
+++ b/src/Apache/Ignite/Internal/Binary/BinaryCommunicator.php
@@ -265,8 +265,10 @@
         $ordinal = $buffer->readInteger();
         $enumItem->setOrdinal($ordinal);
         $type = $this->typeStorage->getType($enumItem->getTypeId());
-        if (!$type->isEnum() || !$type->getEnumValues() || count($type->getEnumValues()) <= $ordinal) {
-            BinaryUtils::serializationError(false, 'EnumItem can not be deserialized: type mismatch');
+        if (!$type || !$type->isEnum()) {
+            BinaryUtils::enumSerializationError(false, sprintf('enum type id "%d" is not registered', $enumItem->getTypeId()));
+        } elseif (!$type->getEnumValues() || count($type->getEnumValues()) <= $ordinal) {
+            BinaryUtils::enumSerializationError(false, 'type mismatch');
         }
         $enumValues = $type->getEnumValues();
         $enumItem->setName($enumValues[$ordinal][0]);
@@ -396,21 +398,22 @@
 
     private function writeEnum(MessageBuffer $buffer, EnumItem $enumValue): void
     {
+        $type = $this->typeStorage->getType($enumValue->getTypeId());
+        if (!$type || !$type->isEnum()) {
+            BinaryUtils::enumSerializationError(true, sprintf('enum type id "%d" is not registered', $enumValue->getTypeId()));
+        }
         $buffer->writeInteger($enumValue->getTypeId());
         if ($enumValue->getOrdinal() !== null) {
             $buffer->writeInteger($enumValue->getOrdinal());
             return;
         } elseif ($enumValue->getName() !== null || $enumValue->getValue() !== null) {
-            $type = $this->typeStorage->getType($enumValue->getTypeId());
-            if ($type && $type->isEnum()) {
-                $enumValues = $type->getEnumValues();
-                if ($enumValues) {
-                    for ($i = 0; $i < count($enumValues); $i++) {
-                        if ($enumValue->getName() === $enumValues[$i][0] ||
-                            $enumValue->getValue() === $enumValues[$i][1]) {
-                            $buffer->writeInteger($i);
-                            return;
-                        }
+            $enumValues = $type->getEnumValues();
+            if ($enumValues) {
+                for ($i = 0; $i < count($enumValues); $i++) {
+                    if ($enumValue->getName() === $enumValues[$i][0] ||
+                        $enumValue->getValue() === $enumValues[$i][1]) {
+                        $buffer->writeInteger($i);
+                        return;
                     }
                 }
             }
diff --git a/src/Apache/Ignite/Internal/Binary/BinaryUtils.php b/src/Apache/Ignite/Internal/Binary/BinaryUtils.php
index ad0bf56..e9ff2f1 100644
--- a/src/Apache/Ignite/Internal/Binary/BinaryUtils.php
+++ b/src/Apache/Ignite/Internal/Binary/BinaryUtils.php
@@ -203,6 +203,9 @@
             $actualTypeCode === ObjectType::BINARY_OBJECT &&
             $expectedTypeCode === ObjectType::COMPLEX_OBJECT) {
             return;
+        } elseif ($expectedTypeCode === ObjectType::ENUM &&
+            $actualTypeCode === ObjectType::BINARY_ENUM) {
+            return;
         } elseif ($actualTypeCode !== $expectedTypeCode) {
             BinaryUtils::typeCastError($actualTypeCode, $expectedTypeCode);
         }
@@ -419,6 +422,15 @@
         throw new ClientException($msg);
     }
 
+    public static function enumSerializationError(bool $serialize, string $message = null): void
+    {
+        $msg = $serialize ? 'Enum item can not be serialized' : 'Enum item can not be deserialized';
+        if ($message) {
+            $msg = $msg . ': ' . $message;
+        }
+        throw new ClientException($msg);
+    }
+
     public static function typeCastError($fromType, $toType): void
     {
         throw new ClientException(sprintf('Type "%s" can not be cast to %s',
diff --git a/tests/CachePutGetTest.php b/tests/CachePutGetTest.php
index 9d15ab2..c3ff1e9 100644
--- a/tests/CachePutGetTest.php
+++ b/tests/CachePutGetTest.php
@@ -18,14 +18,20 @@
 
 namespace Apache\Ignite\Tests;
 
+use \DateTime;
 use Ds\Map;
 use Ds\Set;
 use PHPUnit\Framework\TestCase;
+use Apache\Ignite\Type\ObjectType;
 use Apache\Ignite\Type\MapObjectType;
 use Apache\Ignite\Type\CollectionObjectType;
 use Apache\Ignite\Type\ObjectArrayType;
 use Apache\Ignite\Type\ComplexObjectType;
 use Apache\Ignite\Data\BinaryObject;
+use Apache\Ignite\Data\Date;
+use Apache\Ignite\Data\Timestamp;
+use Apache\Ignite\Data\EnumItem;
+use Apache\Ignite\Exception\ClientException;
 
 class TstComplObjectWithPrimitiveFields
 {
@@ -490,6 +496,119 @@
         $this->putGetObjectArrays(new ObjectArrayType(new ObjectArrayType(new ComplexObjectType())), $array);
     }
 
+    public function testPutGetDateTime(): void
+    {
+        $this->putGetDate("Y-m-d H:i:s", "2018-10-19 18:31:13", 0);
+        $this->putGetDate("Y-m-d H:i:s", "2018-10-19 18:31:13", 29726);
+        $this->putGetDate("Y-m-d H:i:s", "2018-10-19 18:31:13", 999999);
+
+        $this->putGetTimestamp("Y-m-d H:i:s", "2018-10-19 18:31:13", 0);
+        $this->putGetTimestamp("Y-m-d H:i:s", "2018-10-19 18:31:13", 29726000);
+        $this->putGetTimestamp("Y-m-d H:i:s", "2018-10-19 18:31:13", 999999999);
+
+        $this->putGetTimestampFromDateTime("Y-m-d H:i:s", "2018-10-19 18:31:13", 0);
+        $this->putGetTimestampFromDateTime("Y-m-d H:i:s", "2018-10-19 18:31:13", 29726);
+        $this->putGetTimestampFromDateTime("Y-m-d H:i:s", "2018-10-19 18:31:13", 999999);
+    }
+
+    public function testPutEnumItems(): void
+    {
+        $fakeTypeId = 12345;
+        $enumItem1 = new EnumItem($fakeTypeId);
+        $enumItem1->setOrdinal(1);
+        $this->putEnumItem($enumItem1, null);
+        $this->putEnumItem($enumItem1, ObjectType::ENUM);
+        $enumItem2 = new EnumItem($fakeTypeId);
+        $enumItem2->setName('name');
+        $this->putEnumItem($enumItem2, null);
+        $this->putEnumItem($enumItem2, ObjectType::ENUM);
+        $enumItem3 = new EnumItem($fakeTypeId);
+        $enumItem3->setOrdinal(2);
+        $this->putEnumItem($enumItem3, null);
+        $this->putEnumItem($enumItem3, ObjectType::ENUM);
+    }
+
+    private function putEnumItem($value, $valueType): void
+    {
+        $key = microtime();
+        self::$cache->
+            setKeyType(null)->
+            setValueType($valueType);
+        // Enums registration is not supported by the client, therefore put EnumItem must throw ClientException
+        try {
+            self::$cache->put($key, $value);
+            $this->fail('put EnumItem must throw ClientException');
+        } catch (ClientException $e) {
+            $this->assertContains('Enum item can not be serialized', $e->getMessage());
+        } finally {
+            self::$cache->removeAll();
+        }
+    }
+
+    private function putGetDate(string $format, string $dateString, int $micros): void
+    {
+        $key = microtime();
+        self::$cache->
+            setKeyType(null)->
+            setValueType(ObjectType::DATE);
+        try {
+            $dt = DateTime::createFromFormat("$format.u", sprintf("%s.%06d", $dateString, $micros));
+            $iDate = Date::fromDateTime($dt);
+            self::$cache->put($key, $iDate);
+            $result = self::$cache->get($key);
+
+            $this->assertEquals(sprintf("%06d", intval($micros / 1000) * 1000), $result->toDateTime()->format('u'));
+            $this->assertEquals($dateString, $result->toDateTime()->format($format));
+        } finally {
+            self::$cache->removeAll();
+        }
+    }
+
+    private function putGetTimestamp(string $format, string $dateString, int $nanos): void
+    {
+        $key = microtime();
+        self::$cache->
+            setKeyType(null)->
+            setValueType(ObjectType::TIMESTAMP);
+
+        try {
+            $millis = intval($nanos / 1000000);
+            $nanosInMillis = $nanos % 1000000;
+            self::$cache->put($key,
+                new Timestamp(
+                    DateTime::createFromFormat($format, $dateString)->getTimestamp() * 1000 + $millis,
+                    $nanosInMillis
+                )
+            );
+            $result = self::$cache->get($key);
+
+            $this->assertEquals($nanos % 1000000, $result->getNanos());
+            $this->assertEquals($dateString, $result->toDateTime()->format($format));
+        } finally {
+            self::$cache->removeAll();
+        }
+    }
+
+    private function putGetTimestampFromDateTime(string $format, string $dateString, $micros): void
+    {
+        $key = microtime();
+        self::$cache->
+            setKeyType(null)->
+            setValueType(ObjectType::TIMESTAMP);
+
+        try {
+            self::$cache->put($key, Timestamp::fromDateTime(
+                DateTime::createFromFormat("$format.u", sprintf("%s.%06d", $dateString, $micros))
+            ));
+            $result = self::$cache->get($key);
+
+            $this->assertEquals(intval($micros / 1000) * 1000, $result->toDateTime()->format('u'));
+            $this->assertEquals($dateString, $result->toDateTime()->format($format));
+        } finally {
+            self::$cache->removeAll();
+        }
+    }
+
     private function putGetObjectArrays(?ObjectArrayType $arrayType, array $value): void
     {
         $key = microtime();