Created
April 7, 2025 07:57
-
-
Save bdelespierre/3b58a784019f85b97372b8ec4b985044 to your computer and use it in GitHub Desktop.
TypeHelper.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Support; | |
use App\Exceptions\UnexpectedArgumentTypeException; | |
use Stringable; | |
use UnexpectedValueException; | |
class TypeHelper | |
{ | |
/** | |
* @var array{string: callable(mixed $value, bool $strict): bool} | |
*/ | |
public static array $validators = []; | |
/** | |
* @example <code> | |
* UnexpectedArgumentTypeException::expectType('string|string[]', ['foo', 'bar']); | |
* </code> | |
* @example <code> | |
* UnexpectedArgumentTypeException::expectType('?int', null); | |
* </code> | |
* @example <code> | |
* UnexpectedArgumentTypeException::expectType([A::class, B::class], $object); | |
* </code> | |
* @param string|string[] $expected | |
*/ | |
public static function expectType($expected, $value, ?string $message = null): void | |
{ | |
if (!is_string($expected) && !is_array($expected)) { | |
throw new UnexpectedArgumentTypeException("Invalid type expectation", "string|string[]", $expected); | |
} | |
if (is_string($expected) && strpos($expected, '|') !== false) { | |
$expected = array_filter(explode('|', $expected)); | |
} | |
if (is_array($expected)) { | |
self::expectAnyType($expected, $value, $message); | |
return; | |
} | |
$expected = trim($expected); | |
if (strlen($expected) === 0) { | |
throw new UnexpectedValueException("Invalid type expectation: empty-string"); | |
} | |
if (str_starts_with($expected, '?')) { | |
if (self::isType('null', $value)) { | |
return; | |
} | |
$expected = substr($expected, 1); | |
} | |
if (str_ends_with($expected, '[]')) { | |
self::expectArrayOf(substr($expected, 0, -2), $value, $message); | |
return; | |
} | |
if (!self::isType($expected, $value)) { | |
throw new UnexpectedArgumentTypeException($message ?? "Unexpected argument type", $expected, $value); | |
} | |
} | |
public static function expectAnyType(array $expected, $value, ?string $message = null): void | |
{ | |
$fails = 0; | |
foreach ($expected as $type) { | |
try { | |
self::expectType($type, $value); | |
} catch (UnexpectedArgumentTypeException $e) { | |
$fails++; | |
continue; | |
} | |
break; | |
} | |
if ($fails == count($expected)) { | |
throw new UnexpectedArgumentTypeException($message ?? "Unexpected argument type", $expected, $value); | |
} | |
} | |
public static function expectArrayOf(string $expected, $value, ?string $message = null): void | |
{ | |
if (!is_array($value)) { | |
throw new UnexpectedArgumentTypeException($message ?? "Unexpected argument type", $expected, $value); | |
} | |
foreach ($value as $v) { | |
if (!self::isType($expected, $v)) { | |
throw new UnexpectedArgumentTypeException($message ?? "Unexpected argument type", $expected, $value); | |
} | |
} | |
} | |
public static function isType(string $type, $value): bool | |
{ | |
$strict = str_ends_with($type, '!'); | |
if ($strict) { | |
$type = substr($type, 0, -1); | |
} | |
switch (strtolower($type)) { | |
case "any": | |
case "mixed": | |
return true; | |
case "null": | |
return is_null($value); | |
case "not-null": | |
case "non-null": | |
return !is_null($value); | |
case "non-empty": | |
return !empty($value); | |
case "scalar": | |
return is_scalar($value); | |
case "array": | |
return is_array($value); | |
case "object": | |
return is_object($value); | |
case "resource": | |
return is_resource($value); | |
case "callable": | |
return is_callable($value); | |
case "iterable": | |
return is_iterable($value); | |
case "countable": | |
return is_countable($value); | |
case "bool": | |
case "boolean": | |
return $strict ? is_bool($value) : is_bool($value) || (self::isType('numeric', $value) && ($value == 1 || $value == 0)); | |
case "true": | |
return $strict ? $value === true : $value == true; | |
case "false": | |
return $strict ? $value === false : $value == false; | |
case "number": | |
case "numeric": | |
return is_numeric($value); | |
case "int": | |
case "integer": | |
return $strict ? is_int($value) : self::isType('numeric', $value) && (int) $value == $value; | |
case "positive-int": | |
case "positive-integer": | |
return self::isType("int", $value, $strict) && intval($value) > 0; | |
case "negative-int": | |
case "negative-integer": | |
return self::isType("int", $value, $strict) && intval($value) < 0; | |
case "non-positive-int": | |
case "non-positive-integer": | |
return self::isType("int", $value, $strict) && intval($value) <= 0; | |
case "non-negative-int": | |
case "non-negative-integer": | |
return self::isType("int", $value, $strict) && intval($value) >= 0; | |
case "non-zero-int": | |
case "non-zero-integer": | |
return self::isType("int", $value, $strict) && intval($value) != 0; | |
case "float": | |
case "long": | |
case "double": | |
return $strict ? is_float($value) : is_numeric($value); | |
case "positive-float": | |
case "positive-long": | |
case "positive-double": | |
return self::isType("float", $value, $strict) && floatval($value) > 0; | |
case "negative-float": | |
case "negative-long": | |
case "negative-double": | |
return self::isType("float", $value, $strict) && floatval($value) < 0; | |
case "non-positive-float": | |
case "non-positive-long": | |
case "non-positive-double": | |
return self::isType("float", $value, $strict) && floatval($value) <= 0; | |
case "non-negative-float": | |
case "non-negative-long": | |
case "non-negative-double": | |
return self::isType("float", $value, $strict) && floatval($value) >= 0; | |
case "non-zero-float": | |
case "non-zero-long": | |
case "non-zero-double": | |
return self::isType("float", $value, $strict) && floatval($value) != 0; | |
case "string": | |
return $strict ? is_string($value) : is_scalar($value) || $value instanceof Stringable || (is_object($value) && method_exists($value, '__toString')); | |
case "class-string": | |
return self::isType('string', $value, true) && class_exists($value); | |
case "callable-string": | |
return self::isType('string', $value, true) && self::isType('callable', $value); | |
case "numeric-string": | |
return self::isType('string', $value, true) && self::isType('number', $value); | |
case "non-empty-string": | |
return self::isType('string', $value, true) && strlen($value) > 0; | |
case "truthy-string": | |
case "non-falsy-string": | |
return self::isType('string', $value, true) && (bool) $value == true; | |
case "lowercase-string": | |
return self::isType('string', $value, true) && strtolower($value) == $value; | |
default: | |
if (class_exists($type)) { | |
return $value instanceof $type; | |
} | |
if (isset(self::$validators[$type])) { | |
return call_user_func(self::$validators[$type], $value, $strict); | |
} | |
throw new UnexpectedValueException("Invalid type expectation: {$type}"); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment