From 1a2e966247a25405a8e9032ee2523fd428e92f45 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Feb 2025 11:17:58 +0000 Subject: [PATCH 1/5] wip: update checkArrayType with castable values for #49 --- src/DataObject.php | 26 ++++++++++++++++++-------- test/phpunit/DataObjectTest.php | 30 ++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/DataObject.php b/src/DataObject.php index 88d3a86..75b7912 100644 --- a/src/DataObject.php +++ b/src/DataObject.php @@ -79,7 +79,7 @@ public function getArray(string $name, string $type = null):?array { $array = $this->get($name); if($array && $type) { - $this->checkArrayType($array, $type); + $array = $this->checkArrayType($array, $type); } return $array; @@ -110,8 +110,11 @@ public function asObject():object { return (object)$array; } - /** @param mixed[] $array */ - private function checkArrayType(array $array, string $type):void { + /** + * @param array $array + * @return array + */ + private function checkArrayType(array $array, string $type):array { $errorMessage = ""; foreach($array as $i => $value) { @@ -122,16 +125,23 @@ private function checkArrayType(array $array, string $type):void { $errorMessage = "Array index $i must be of type $type, $actualType given"; } } - else { - $checkFunction = "is_$type"; - if(!call_user_func($checkFunction, $value)) { - $errorMessage = "Array index $i must be of type $type, $actualType given"; - } + elseif(function_exists("is_$type")) { + $castedValue = match($type) { + "int" => (int)$value, + "bool" => (bool)$value, + "string" => (string)$value, + "float", "double" => (float)$value, + "array" => (array)$value, + default => null, + }; + $array[$i] = $castedValue; } } if($errorMessage) { throw new TypeError($errorMessage); } + + return $array; } } diff --git a/test/phpunit/DataObjectTest.php b/test/phpunit/DataObjectTest.php index dba3498..61e114f 100644 --- a/test/phpunit/DataObjectTest.php +++ b/test/phpunit/DataObjectTest.php @@ -3,9 +3,11 @@ use DateTime; use DateTimeInterface; +use Error; use Gt\DataObject\DataObject; use PHPUnit\Framework\TestCase; use stdClass; +use Throwable; use TypeError; class DataObjectTest extends TestCase { @@ -105,6 +107,21 @@ public function testGetStringNull() { self::assertNull($sut->getString("nothing")); } + public function testGetStringFromDateTime() { + $sut = (new DataObject()) + ->with("dt", new DateTime()); + + $exception = null; + + try { + $data = $sut->getString("dt"); + } + catch(Throwable $exception) {} + + self::assertInstanceOf(Error::class, $exception); + self::assertSame("Object of class DateTime could not be converted to string", $exception->getMessage()); + } + public function testGetIntFromString() { $sut = (new DataObject()) ->with("one", "1") @@ -374,15 +391,20 @@ public function testGetArrayFixedTypeInt_typeError() { 49997, 50000, 49999, - "PLOP!", + "50003", 50001, ]; $sut = (new DataObject()) ->with("timestamps", $timestampArray); - self::expectException(TypeError::class); - self::expectExceptionMessage("Array index 3 must be of type int, string given"); - $sut->getArray("timestamps", "int"); + $array = $sut->getArray("timestamps", "int"); + foreach($array as $i => $value) { + if($i === 3) { + self::assertSame(50003, $value); + } + + self::assertIsInt($value); + } } public function testGetArrayFixedTypeFloat() { From 7bfc7a729d88a1dde14c230c4a274c192c3642f7 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Feb 2025 11:18:17 +0000 Subject: [PATCH 2/5] wip: update examples --- example/01-json-encode.php | 15 +++++++++++++++ example/02-json-decode.php | 15 +++++++++++++++ example/03-type-casting.php | 22 ++++++++++++++++++++++ example/04-type-safety-arrays.php | 20 ++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 example/01-json-encode.php create mode 100644 example/02-json-decode.php create mode 100644 example/03-type-casting.php create mode 100644 example/04-type-safety-arrays.php diff --git a/example/01-json-encode.php b/example/01-json-encode.php new file mode 100644 index 0000000..bc1077c --- /dev/null +++ b/example/01-json-encode.php @@ -0,0 +1,15 @@ +with("name", "Cody") + ->with("colour", "orange") + ->with("food", [ + "biscuits", + "mushrooms", + "corn on the cob", + ]); + +echo json_encode($obj), PHP_EOL; diff --git a/example/02-json-decode.php b/example/02-json-decode.php new file mode 100644 index 0000000..bdb616e --- /dev/null +++ b/example/02-json-decode.php @@ -0,0 +1,15 @@ +fromObject(json_decode($jsonString)); + +echo "Hello, ", + $obj->getString("name"), + "! Your favourite food is ", + $obj->getArray("food")[0], + PHP_EOL; diff --git a/example/03-type-casting.php b/example/03-type-casting.php new file mode 100644 index 0000000..a529eff --- /dev/null +++ b/example/03-type-casting.php @@ -0,0 +1,22 @@ +with("one", "1"); +$obj = $obj->with("two", "two"); +$obj = $obj->with("pi", "3.14159"); + +// Automatically cast to int: + +$int1 = $obj->getInt("one"); +$int2 = $obj->getInt("two"); +$int3 = $obj->getInt("pi"); + +echo "One: $int1, two: $int2, three: $int3", PHP_EOL; +// Outputs: One: 1, two: 0, three: 3 +// Note how the decimal data is lost in the cast to int, +// but how the original data is not lost. diff --git a/example/04-type-safety-arrays.php b/example/04-type-safety-arrays.php new file mode 100644 index 0000000..57cc074 --- /dev/null +++ b/example/04-type-safety-arrays.php @@ -0,0 +1,20 @@ +with("arrayOfData", [ + 1, + 2, + 3.14159, +]); + +echo "The third element in the array is: ", + $obj->getArray("arrayOfData", "int")[2], // note the type check of "int" + PHP_EOL; + +/* Output: +The third element in the array is: 3 +*/ From e85a6961d5aa32a84544361458d50a4f4bae3cdd Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Feb 2025 12:37:29 +0000 Subject: [PATCH 3/5] feature: consistent type safety within arrays closes #49 --- src/DataObject.php | 4 ++-- test/phpunit/DataObjectTest.php | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/DataObject.php b/src/DataObject.php index d25e9f4..54e064c 100644 --- a/src/DataObject.php +++ b/src/DataObject.php @@ -114,8 +114,8 @@ public function asObject():object { } /** - * @param array $array - * @return array + * @param array $array + * @return array */ private function checkArrayType(array $array, string $type):array { $errorMessage = ""; diff --git a/test/phpunit/DataObjectTest.php b/test/phpunit/DataObjectTest.php index 61e114f..c2612e1 100644 --- a/test/phpunit/DataObjectTest.php +++ b/test/phpunit/DataObjectTest.php @@ -455,7 +455,7 @@ public function testGetArrayFixedTypeString() { } } - public function testGetArrayFixedTypeString_typeErrorWithObject() { + public function testGetArrayFixedTypeString_typeErrorWithDateTime() { $wordsArray = [ "one", "two", @@ -465,7 +465,7 @@ public function testGetArrayFixedTypeString_typeErrorWithObject() { ]; $sut = (new DataObject()) ->with("words", $wordsArray); - self::expectExceptionMessage("Array index 3 must be of type string, DateTime given"); + self::expectExceptionMessage("Object of class DateTime could not be converted to string"); $sut->getArray("words", "string"); } @@ -478,7 +478,7 @@ public function testGetArrayFixedTypeDateTime() { ->with("dates", $dateArray); $array = $sut->getArray("dates", DateTimeInterface::class); foreach($array as $i => $value) { - self::assertInstanceOf(DateTimeInterface::class, $value); + self::assertInstanceOf(DateTime::class, $value); } } @@ -492,9 +492,8 @@ public function testGetArrayFixedTypesMismatch() { ]; $sut = (new DataObject()) ->with("timestamps", $timestampArray); - self::expectException(TypeError::class); - self::expectExceptionMessage("Array index 2 must be of type int, double given"); - $sut->getArray("timestamps", "int"); + $array = $sut->getArray("timestamps", "int"); + self::assertSame(49999, $array[2]); } public function testGetArray_nullableType():void { From 3dbe3d39515aa87c7718eb2c23ba12fec55b43db Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Feb 2025 12:56:17 +0000 Subject: [PATCH 4/5] tidy: reduce cyclomatic complexity --- src/DataObject.php | 72 +++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/src/DataObject.php b/src/DataObject.php index 54e064c..44666f4 100644 --- a/src/DataObject.php +++ b/src/DataObject.php @@ -117,34 +117,60 @@ public function asObject():object { * @param array $array * @return array */ - private function checkArrayType(array $array, string $type):array { - $errorMessage = ""; + private function checkArrayType(array $array, string $type): array { + $this->validateTypeExists($type); - foreach($array as $i => $value) { - $actualType = is_scalar($value) ? gettype($value) : get_class($value); + foreach ($array as $i => $value) { + $array[$i] = $this->processValue($value, $type, $i); + } - if(class_exists($type) || interface_exists($type)) { - if(!is_a($value, $type)) { - $errorMessage = "Array index $i must be of type $type, $actualType given"; - } - } - elseif(function_exists("is_$type")) { - $castedValue = match($type) { - "int" => (int)$value, - "bool" => (bool)$value, - "string" => (string)$value, - "float", "double" => (float)$value, - "array" => (array)$value, - default => null, - }; - $array[$i] = $castedValue; - } + return $array; + } + + private function validateTypeExists(string $type): void { + if(!class_exists($type) + && !interface_exists($type) + && !function_exists("is_$type")) { + throw new TypeError("Invalid type: $type does not exist."); } + } - if($errorMessage) { - throw new TypeError($errorMessage); + private function processValue( + mixed $value, + string $type, + int $index, + ): mixed { + if (class_exists($type) || interface_exists($type)) { + $this->assertInstanceOfType($value, $type, $index); + } elseif (function_exists("is_$type")) { + return $this->castValue($value, $type); } - return $array; + return $value; + } + + private function assertInstanceOfType( + mixed $value, + string $type, + int $index, + ): void { + if (!is_a($value, $type)) { + $actualType = is_scalar($value) + ? gettype($value) + : get_class($value); + throw new TypeError("Array index $index" + . " must be of type $type, $actualType given"); + } + } + + private function castValue(mixed $value, string $type): mixed { + return match ($type) { + "int" => (int)$value, + "bool" => (bool)$value, + "string" => (string)$value, + "float", "double" => (float)$value, + "array" => (array)$value, + default => null, + }; } } From 41ad40c0c26453d09089e287c0558d961fe3dce8 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Feb 2025 16:22:55 +0000 Subject: [PATCH 5/5] tidy: split example JSON --- example/02-json-decode.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/example/02-json-decode.php b/example/02-json-decode.php index bdb616e..b7fa3fa 100644 --- a/example/02-json-decode.php +++ b/example/02-json-decode.php @@ -3,7 +3,17 @@ require __DIR__ . "/../vendor/autoload.php"; -$jsonString = '{"name":"Cody","colour":"orange","food":["biscuits","mushrooms","corn on the cob"]}'; +$jsonString = <<fromObject(json_decode($jsonString));