mercredi 23 février 2022

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?





Aucun commentaire:

Enregistrer un commentaire