Skip to content

Instantly share code, notes, and snippets.

@bdelespierre
Created April 7, 2025 07:57
Show Gist options
  • Save bdelespierre/3b58a784019f85b97372b8ec4b985044 to your computer and use it in GitHub Desktop.
Save bdelespierre/3b58a784019f85b97372b8ec4b985044 to your computer and use it in GitHub Desktop.
TypeHelper.php
<?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