'Type checking a PHP function before calling it
I have a webservice that will be used something like that:
GET http://localhost/services/sum?a=1&b=2
This will resolve directly (ignore details like authorization) on a function call defined something like this:
class Services {
public function sum(int $a, int $b) {
return $a + $b;
}
}
Now, if they user calls GET http://localhost/services/sum?a=abc&b=2, this is a PHP type error. Before calling the sum function, I want to "type check" the arguments and report what's wrong. In this case, the response would be something like
"errors" {
"a": {
"type_mismatch": {
"expected": "int",
"received": "string",
}
}
}
For this purpose, I wrote this function:
function buildArguments(array $arguments, $service)
{
$reflectionMethod = new \ReflectionFunction($service);
$reflectionParameters = $reflectionMethod->getParameters();
$missingArguments = [];
$typeMismatch = [];
foreach ($reflectionParameters as $reflectionParameter) {
$name = $reflectionParameter->getName();
if (!array_key_exists($name, $arguments) && !$reflectionParameter->isOptional()) {
$missingArguments[] = $reflectionParameter->getName();
} else if ((is_null($arguments[$name] ?? null) && !$reflectionParameter->getType()->allowsNull()) ||
!($reflectionParameter->getType()->getName() == gettype($arguments[$name]))) {
$typeMismatch[$name] = [
'received' => gettype($arguments[$name]),
'expected' => $reflectionParameter->getType()->getName()
];
}
}
$errors = [];
if (!empty($missingArguments)) {
$errors['missing_argument'] = $missingArguments;
}
if (!empty($typeMismatch)) {
$errors['type_mismatch'] = $typeMismatch;
}
if (empty($errors)) {
return true;
} else {
var_dump($errors);
return false;
}
}
It works well for strings:
function concat(string $a, string $b) {
return $a . $b;
}
buildArguments(['a' => 'x', 'b' => 'y'], 'concat'); //ok!
buildArguments(['a' => 'x'], 'concat'); // missing_argument: b
buildArguments(['a' => 1, 'b' => 'y'], 'concat'); //type mismatch: a (expected integer, got string)
It immediately falls apart for int:
function sum(int $a, int $b): int
{
return $a + $b;
}
buildArguments(['a' => 1, 'b' => 2], 'sum');
//type mismatch! expected "int" received "integer"
I only need this to work for simple structures: ints, string, untyped arrays, no need to check object, inheritances, interfaces and whatnot. I could just add a "if int then integer" but I have a feeling there will be a bunch of gotchas regarding nullables and optionals. Is there a clever way of achieving this?
The TypeError doesn't offer any help in that regard, only a stringified message, maybe I can "manually" call whatever procedure PHP calls that throws the TypeError?
Solution 1:[1]
As Stevish pointed out in a comment, your current approach won't work once you apply it to URLs rather than hard-coded test values, because $_GET['a'] will only ever be a string (or not set).
Instead, you'll need to look at the expected type, and choose an appropriate validation function. Unfortunately, PHP doesn't have a nice built-in function for "is-integer-ish string" so you'll probably need to use a regex (ctype_digit() is close, but won't allow negative numbers, since '-' is not a digit).
To avoid too much spaghetti, I'd break the code for validating out of the loop, and have something like this:
$reflectionMethod = new \ReflectionFunction($service);
$reflectionParameters = $reflectionMethod->getParameters();
$errors = [];
foreach ($reflectionParameters as $reflectionParameter) {
$name = $reflectionParameter->getName();
$errors[] = $this->validateParameter(
$name,
$reflectionParameter->getType(),
$arguments[$name] ?? null
);
}
and then
private function validateParameter(string $name, \ReflectionType $expectedType, ?string $input): ?string {
if ( $input === null )
if ( !$expectedType->allowsNull() ) {
return "Missing required $name";
}
else {
// null allowed, no error
return null;
}
}
switch ( $expectedType->getName() ) {
case 'string':
// everything's a string
// unless you want to assume empty string is always an error?
return null;
break;
case 'integer':
if ( ! preg_match('/^-?[0-9]+$/', $input) ) {
return "Bad integer for $name";
}
break;
// Other cases ...
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
