调用 PHP 函数之前对其进行类型检查

发布于 2025-01-09 12:13:32 字数 2799 浏览 0 评论 0原文

我有一个将使用类似以下内容的网络服务:

GET http://localhost/services/sum?a=1&b=2

这将直接解析函数(忽略授权等详细信息) call 定义如下:

class Services {
    public function sum(int $a, int $b) {
        return $a + $b;
    }
}

现在,如果用户调用 GET http://localhost/services/sum?a=abc&b=2,这是一个 PHP 类型错误。在调用 sum 函数之前,我想对参数进行“类型检查”并报告错误。在这种情况下,响应将类似于

"errors" {
    "a": {
        "type_mismatch": {
            "expected": "int",
            "received": "string",
        }
    }
}

为此目的,我编写了这个函数:

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;
    }
}

它适用于字符串:

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)

它立即崩溃为整数:

function sum(int $a, int $b): int
{
    return $a + $b;
}
buildArguments(['a' => 1, 'b' => 2], 'sum');
//type mismatch! expected "int" received "integer"

我只需要它适用于简单的结构:整数,字符串,无类型数组,无需要检查对象、继承、接口等等。 我可以添加一个“if int then integer”,但我有一种感觉,会有很多关于可空值和可选值的问题。 有没有一种聪明的方法来实现这一目标?

TypeError 在这方面没有提供任何帮助,只有字符串化消息,也许我可以“手动”调用 PHP 调用的引发 TypeError 的任何过程?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

梦醒时光 2025-01-16 12:13:32

正如 Stevish 在评论中指出的那样,一旦将其应用于 URL 而不是硬编码,您当前的方法将不起作用测试值,因为 $_GET['a'] 永远只是一个字符串(或未设置)。

相反,您需要查看预期的类型,并选择适当的验证函数。不幸的是,PHP 没有一个很好的内置函数来处理“is-integer-ish string”,因此您可能需要使用正则表达式(ctype_digit() 很接近,但不允许负数,因为“-”不是数字)。

为了避免太多意大利面条,我会破坏循环外验证的代码,并有这样的东西:

    $reflectionMethod = new \ReflectionFunction($service);
    $reflectionParameters = $reflectionMethod->getParameters();
    $errors = [];
    foreach ($reflectionParameters as $reflectionParameter) {
        $name = $reflectionParameter->getName();

        $errors[] = $this->validateParameter(
            $name,
            $reflectionParameter->getType(),
            $arguments[$name] ?? null
        );
    }

然后

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 ...
    }
}

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 ...
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文