Skip to content

Commit

Permalink
Array type safety (#51)
Browse files Browse the repository at this point in the history
* wip: update checkArrayType with castable values
for #49

* wip: update examples

* feature: consistent type safety within arrays
closes #49

* tidy: reduce cyclomatic complexity

* tidy: split example JSON
  • Loading branch information
g105b authored Feb 14, 2025
1 parent de5fb8e commit 534b593
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 29 deletions.
15 changes: 15 additions & 0 deletions example/01-json-encode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
use Gt\DataObject\DataObject;

require __DIR__ . "/../vendor/autoload.php";

$obj = (new DataObject())
->with("name", "Cody")
->with("colour", "orange")
->with("food", [
"biscuits",
"mushrooms",
"corn on the cob",
]);

echo json_encode($obj), PHP_EOL;
25 changes: 25 additions & 0 deletions example/02-json-decode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
use Gt\DataObject\DataObjectBuilder;

require __DIR__ . "/../vendor/autoload.php";

$jsonString = <<<JSON
{
"name": "Cody",
"colour": "orange",
"food": [
"biscuits",
"mushrooms",
"corn on the cob"
]
}
JSON;

$builder = new DataObjectBuilder();
$obj = $builder->fromObject(json_decode($jsonString));

echo "Hello, ",
$obj->getString("name"),
"! Your favourite food is ",
$obj->getArray("food")[0],
PHP_EOL;
22 changes: 22 additions & 0 deletions example/03-type-casting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Gt\DataObject\DataObject;

require __DIR__ . "/../vendor/autoload.php";

// Set an object with all string values, similar to a web requests:
$obj = new DataObject();
$obj = $obj->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.
20 changes: 20 additions & 0 deletions example/04-type-safety-arrays.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use Gt\DataObject\DataObject;

require __DIR__ . "/../vendor/autoload.php";

$obj = new DataObject();
$obj = $obj->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
*/
74 changes: 55 additions & 19 deletions src/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function getArray(string $name, ?string $type = null):?array {
}

if($array && $type) {
$this->checkArrayType($array, $type);
$array = $this->checkArrayType($array, $type);
}

return $array;
Expand Down Expand Up @@ -113,28 +113,64 @@ public function asObject():object {
return (object)$array;
}

/** @param mixed[] $array */
private function checkArrayType(array $array, string $type):void {
$errorMessage = "";
/**
* @param array<mixed> $array
* @return array<mixed>
*/
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";
}
}
else {
$checkFunction = "is_$type";
if(!call_user_func($checkFunction, $value)) {
$errorMessage = "Array index $i must be of type $type, $actualType given";
}
}
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 $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,
};
}
}
41 changes: 31 additions & 10 deletions test/phpunit/DataObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -433,7 +455,7 @@ public function testGetArrayFixedTypeString() {
}
}

public function testGetArrayFixedTypeString_typeErrorWithObject() {
public function testGetArrayFixedTypeString_typeErrorWithDateTime() {
$wordsArray = [
"one",
"two",
Expand All @@ -443,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");
}

Expand All @@ -456,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);
}
}

Expand All @@ -470,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 {
Expand Down

0 comments on commit 534b593

Please sign in to comment.