编程

控制器和闭包路由中的原始类型

974 2022-01-01 01:41:10

在laravel控制器中,我过去通常未考虑使用原始类型的类型提示。PHP只有四个标量原始类型,bool, int, float 和 string 。对于路由,string和int可能是最常用的。 而通常喔不会在控制器中使用标量原始类型的类型提示。

我最近发现一个控制器类型提示导致的TypeError, 因此我想举例说明你怎么在控制器中安全使用int的类型提示。

想一想以下路由调用时中的$orderId会是什么类型:

Route::get('/order/{order_id}', function ($orderId) {
    return [
        'type' => gettype($orderId),
        'value' => $orderId,
    ];
});

闭包调用时,$orderId 是一个字符串。如果是写一个简单测试,你会看到:

/**
 * A basic test example.
 *
 * @return void
 */
public function test_example()
{
    $response = $this->get('/order/123');
 
    $response->assertJson([
        'type' => 'string',
        'value' => '123',
    ]);
}

假如你希望orderId是整型,你这样可以使用类型提示:

Route::get('/order/{order_id}', function (int $orderId) {
    return [
        'type' => gettype($orderId),
        'value' => $orderId,
    ];
});

回到测试,会出现错误:

--- Expected
+++ Actual
@@ @@
 array (
-  'type' => 'string',
-  'value' => '123',
+  'type' => 'integer',
+  'value' => 123,
 )

虽然技术上我们传的是字符串123,   PHP默认会通过类型转换处理。换句话说,调用路由函数时, PHP会尝试将值从字符串转换成整型。尽管技术上这个方法是可行的且能保证整型类型。我们还是会面临一个问题:万一用户传的值不能转换成整型呢?

如果我们更新测试用例, 会出现TypeError报错:

public function test_example()
{
    $this->withoutExceptionHandling();
 
    $response = $this->get('/order/ABC-123');
 
    $response->assertJson([
        'type' => 'integer',
        'value' => 123,
    ]);
}

运行测试会出现如下报错:

TypeError: Illuminate\Routing\RouteFileRegistrar::{closure}():
Argument #1 ($orderId) must be of type int, string given, called in .../vendor/laravel/framework/src/Illuminate/Routing/Route.php on line 238

如果我们要在路由中还用类型提示,我们应该确保路由的正则表达式约束:

Route::get('/order/{order_id}', function (int $orderId) {
    return [
        'type' => gettype($orderId),
        'value' => $orderId,
    ];
})->where('order_id', '[0-9]+');

添加了路由约束之后,只有数值型参数会匹配路由,这样保证类型转换如预期那样。以下测试更准确地确保非数值型参数不会匹配路由:

public function test_example()
{
    $response = $this->get('/order/ABC-123');
 
    $response->assertNotFound();
}

现在你可以认为闭包路由中的int类型提示可以安全进行类型转换了。尽管用例作用有限,

我想这些细小差别还是能有助于理解路由参数的类型提示。

How is This Affected By Strict Types?

我想指出的是,由于在laravel框架中代码并没有使用strict_ypes, declare(strict_types=1); 其实没有效果。因此类型转换会发生在:

  1. Laravel的控制器调用方法Controller::callAction() 
  2. Laravel的路由调用闭包Route::runCallable() 

PHP类型声明文档中,关于严格类型如何工作:

Strict typing applies to function calls made from within the file with strict typing enabled, not to the functions declared within that file. If a file without strict typing enabled makes a call to a function defined in a file with strict typing, the caller’s preference (coercive typing) will be respected, and the value will be coerced.

其他方案

如果你在控制器和闭包中有标量类型路由参数,你可以忽视标量类型,在控制器中进行类型造型:

Route::get(
    '/order/{order_id}',
    function ($orderId, SomeService $someService) {
        // Cast for a method that strictly type-hints an integer
        $result = $someService->someMethod((int) $orderId);
        // ...
    }
);

大部分时候,你可能会使用路由模型绑定。但如果你有一个路由参数想要用类型提示,可以采用此法。