Skip to content

Commit 534b593

Browse files
author
Greg Bowler
authored
Array type safety (#51)
* 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
1 parent de5fb8e commit 534b593

6 files changed

Lines changed: 168 additions & 29 deletions

File tree

‎example/01-json-encode.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
use Gt\DataObject\DataObject;
3+
4+
require __DIR__ . "/../vendor/autoload.php";
5+
6+
$obj = (new DataObject())
7+
->with("name", "Cody")
8+
->with("colour", "orange")
9+
->with("food", [
10+
"biscuits",
11+
"mushrooms",
12+
"corn on the cob",
13+
]);
14+
15+
echo json_encode($obj), PHP_EOL;

‎example/02-json-decode.php‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
use Gt\DataObject\DataObjectBuilder;
3+
4+
require __DIR__ . "/../vendor/autoload.php";
5+
6+
$jsonString = <<<JSON
7+
{
8+
"name": "Cody",
9+
"colour": "orange",
10+
"food": [
11+
"biscuits",
12+
"mushrooms",
13+
"corn on the cob"
14+
]
15+
}
16+
JSON;
17+
18+
$builder = new DataObjectBuilder();
19+
$obj = $builder->fromObject(json_decode($jsonString));
20+
21+
echo "Hello, ",
22+
$obj->getString("name"),
23+
"! Your favourite food is ",
24+
$obj->getArray("food")[0],
25+
PHP_EOL;

‎example/03-type-casting.php‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use Gt\DataObject\DataObject;
4+
5+
require __DIR__ . "/../vendor/autoload.php";
6+
7+
// Set an object with all string values, similar to a web requests:
8+
$obj = new DataObject();
9+
$obj = $obj->with("one", "1");
10+
$obj = $obj->with("two", "two");
11+
$obj = $obj->with("pi", "3.14159");
12+
13+
// Automatically cast to int:
14+
15+
$int1 = $obj->getInt("one");
16+
$int2 = $obj->getInt("two");
17+
$int3 = $obj->getInt("pi");
18+
19+
echo "One: $int1, two: $int2, three: $int3", PHP_EOL;
20+
// Outputs: One: 1, two: 0, three: 3
21+
// Note how the decimal data is lost in the cast to int,
22+
// but how the original data is not lost.

‎example/04-type-safety-arrays.php‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
use Gt\DataObject\DataObject;
4+
5+
require __DIR__ . "/../vendor/autoload.php";
6+
7+
$obj = new DataObject();
8+
$obj = $obj->with("arrayOfData", [
9+
1,
10+
2,
11+
3.14159,
12+
]);
13+
14+
echo "The third element in the array is: ",
15+
$obj->getArray("arrayOfData", "int")[2], // note the type check of "int"
16+
PHP_EOL;
17+
18+
/* Output:
19+
The third element in the array is: 3
20+
*/

‎src/DataObject.php‎

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function getArray(string $name, ?string $type = null):?array {
8282
}
8383

8484
if($array && $type) {
85-
$this->checkArrayType($array, $type);
85+
$array = $this->checkArrayType($array, $type);
8686
}
8787

8888
return $array;
@@ -113,28 +113,64 @@ public function asObject():object {
113113
return (object)$array;
114114
}
115115

116-
/** @param mixed[] $array */
117-
private function checkArrayType(array $array, string $type):void {
118-
$errorMessage = "";
116+
/**
117+
* @param array<mixed> $array
118+
* @return array<mixed>
119+
*/
120+
private function checkArrayType(array $array, string $type): array {
121+
$this->validateTypeExists($type);
119122

120-
foreach($array as $i => $value) {
121-
$actualType = is_scalar($value) ? gettype($value) : get_class($value);
123+
foreach ($array as $i => $value) {
124+
$array[$i] = $this->processValue($value, $type, $i);
125+
}
122126

123-
if(class_exists($type) || interface_exists($type)) {
124-
if(!is_a($value, $type)) {
125-
$errorMessage = "Array index $i must be of type $type, $actualType given";
126-
}
127-
}
128-
else {
129-
$checkFunction = "is_$type";
130-
if(!call_user_func($checkFunction, $value)) {
131-
$errorMessage = "Array index $i must be of type $type, $actualType given";
132-
}
133-
}
127+
return $array;
128+
}
129+
130+
private function validateTypeExists(string $type): void {
131+
if(!class_exists($type)
132+
&& !interface_exists($type)
133+
&& !function_exists("is_$type")) {
134+
throw new TypeError("Invalid type: $type does not exist.");
134135
}
136+
}
135137

136-
if($errorMessage) {
137-
throw new TypeError($errorMessage);
138+
private function processValue(
139+
mixed $value,
140+
string $type,
141+
int $index,
142+
): mixed {
143+
if (class_exists($type) || interface_exists($type)) {
144+
$this->assertInstanceOfType($value, $type, $index);
145+
} elseif (function_exists("is_$type")) {
146+
return $this->castValue($value, $type);
138147
}
148+
149+
return $value;
150+
}
151+
152+
private function assertInstanceOfType(
153+
mixed $value,
154+
string $type,
155+
int $index,
156+
): void {
157+
if (!is_a($value, $type)) {
158+
$actualType = is_scalar($value)
159+
? gettype($value)
160+
: get_class($value);
161+
throw new TypeError("Array index $index"
162+
. " must be of type $type, $actualType given");
163+
}
164+
}
165+
166+
private function castValue(mixed $value, string $type): mixed {
167+
return match ($type) {
168+
"int" => (int)$value,
169+
"bool" => (bool)$value,
170+
"string" => (string)$value,
171+
"float", "double" => (float)$value,
172+
"array" => (array)$value,
173+
default => null,
174+
};
139175
}
140176
}

‎test/phpunit/DataObjectTest.php‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
use DateTime;
55
use DateTimeInterface;
6+
use Error;
67
use Gt\DataObject\DataObject;
78
use PHPUnit\Framework\TestCase;
89
use stdClass;
10+
use Throwable;
911
use TypeError;
1012

1113
class DataObjectTest extends TestCase {
@@ -105,6 +107,21 @@ public function testGetStringNull() {
105107
self::assertNull($sut->getString("nothing"));
106108
}
107109

110+
public function testGetStringFromDateTime() {
111+
$sut = (new DataObject())
112+
->with("dt", new DateTime());
113+
114+
$exception = null;
115+
116+
try {
117+
$data = $sut->getString("dt");
118+
}
119+
catch(Throwable $exception) {}
120+
121+
self::assertInstanceOf(Error::class, $exception);
122+
self::assertSame("Object of class DateTime could not be converted to string", $exception->getMessage());
123+
}
124+
108125
public function testGetIntFromString() {
109126
$sut = (new DataObject())
110127
->with("one", "1")
@@ -374,15 +391,20 @@ public function testGetArrayFixedTypeInt_typeError() {
374391
49997,
375392
50000,
376393
49999,
377-
"PLOP!",
394+
"50003",
378395
50001,
379396
];
380397
$sut = (new DataObject())
381398
->with("timestamps", $timestampArray);
382399

383-
self::expectException(TypeError::class);
384-
self::expectExceptionMessage("Array index 3 must be of type int, string given");
385-
$sut->getArray("timestamps", "int");
400+
$array = $sut->getArray("timestamps", "int");
401+
foreach($array as $i => $value) {
402+
if($i === 3) {
403+
self::assertSame(50003, $value);
404+
}
405+
406+
self::assertIsInt($value);
407+
}
386408
}
387409

388410
public function testGetArrayFixedTypeFloat() {
@@ -433,7 +455,7 @@ public function testGetArrayFixedTypeString() {
433455
}
434456
}
435457

436-
public function testGetArrayFixedTypeString_typeErrorWithObject() {
458+
public function testGetArrayFixedTypeString_typeErrorWithDateTime() {
437459
$wordsArray = [
438460
"one",
439461
"two",
@@ -443,7 +465,7 @@ public function testGetArrayFixedTypeString_typeErrorWithObject() {
443465
];
444466
$sut = (new DataObject())
445467
->with("words", $wordsArray);
446-
self::expectExceptionMessage("Array index 3 must be of type string, DateTime given");
468+
self::expectExceptionMessage("Object of class DateTime could not be converted to string");
447469
$sut->getArray("words", "string");
448470
}
449471

@@ -456,7 +478,7 @@ public function testGetArrayFixedTypeDateTime() {
456478
->with("dates", $dateArray);
457479
$array = $sut->getArray("dates", DateTimeInterface::class);
458480
foreach($array as $i => $value) {
459-
self::assertInstanceOf(DateTimeInterface::class, $value);
481+
self::assertInstanceOf(DateTime::class, $value);
460482
}
461483
}
462484

@@ -470,9 +492,8 @@ public function testGetArrayFixedTypesMismatch() {
470492
];
471493
$sut = (new DataObject())
472494
->with("timestamps", $timestampArray);
473-
self::expectException(TypeError::class);
474-
self::expectExceptionMessage("Array index 2 must be of type int, double given");
475-
$sut->getArray("timestamps", "int");
495+
$array = $sut->getArray("timestamps", "int");
496+
self::assertSame(49999, $array[2]);
476497
}
477498

478499
public function testGetArray_nullableType():void {

0 commit comments

Comments
 (0)