Created
May 9, 2016 05:59
-
-
Save ezzatron/ef3007e7e99c1f635e0f67d14a15ccc6 to your computer and use it in GitHub Desktop.
PHP type-checking proxy
This file contains 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 | |
class TypeCheckingProxy | |
{ | |
public function __construct($object) | |
{ | |
$this->object = $object; | |
$this->class = get_class($object); | |
$reflector = new ReflectionObject($object); | |
$this->methods = []; | |
foreach ($reflector->getMethods() as $method) { | |
if ($method->isStatic() || !$method->isPublic()) { | |
continue; | |
} | |
$this->methods[$method->getName()] = $method->getParameters(); | |
} | |
} | |
public function __call($name, array $arguments) | |
{ | |
if (isset($this->methods[$name])) { | |
try { | |
return $this->object->$name(...$arguments); | |
} catch (TypeError $e) { | |
foreach ($this->methods[$name] as $i => $parameter) { | |
$argumentExists = array_key_exists($i, $arguments); | |
if ($type = $parameter->getType()) { | |
$expectedType = strval($type); | |
} else { | |
$expectedType = null; | |
} | |
if (!$argumentExists && !$parameter->isOptional()) { | |
if ($type) { | |
throw new InvalidArgumentException( | |
sprintf( | |
'Missing argument of type %s for %s.', | |
$expectedType, | |
var_export($parameter->getName(), true) | |
) | |
); | |
} | |
throw new InvalidArgumentException( | |
sprintf( | |
'Missing argument for %s.', | |
var_export($parameter->getName(), true) | |
) | |
); | |
} | |
if (!$type) { | |
continue; | |
} | |
switch ($expectedType) { | |
case 'array': | |
case 'callable': | |
case 'float': | |
case 'string': | |
break; | |
case 'bool': | |
$expectedType = 'boolean'; | |
break; | |
case 'int': | |
$expectedType = 'integer'; | |
break; | |
default: | |
$expectedType = 'object'; | |
} | |
if ($argumentExists) { | |
$actualType = gettype($arguments[$i]); | |
switch ($actualType) { | |
case 'double': | |
$actualType = 'float'; | |
break; | |
case 'NULL': | |
$actualType = 'null'; | |
break; | |
} | |
} else { | |
$actualType = '<none>'; | |
} | |
if ($actualType !== $expectedType) { | |
throw new InvalidArgumentException( | |
sprintf( | |
'Expected argument of type %s for %s, ' . | |
'but received %s.', | |
$expectedType, | |
var_export($parameter->getName(), true), | |
$actualType | |
) | |
); | |
} | |
} | |
} | |
} | |
throw new BadMethodCallException( | |
sprintf( | |
'Call to undefined method %s::%s().', | |
$this->class, | |
$name | |
) | |
); | |
} | |
private $object; | |
private $class; | |
private $methods; | |
} | |
class Api | |
{ | |
public function foo(string $a, int $b) | |
{ | |
return __METHOD__ . ' ' . implode(', ', func_get_args()); | |
} | |
} | |
$o = new TypeCheckingProxy(new Api()); | |
try { | |
$o->foo('a', 'b'); | |
} catch (Throwable $e) { | |
printf("Caught: %s\n", $e->getMessage()); | |
} | |
try { | |
$o->foo('a'); | |
} catch (Throwable $e) { | |
printf("Caught: %s\n", $e->getMessage()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment