控制器和闭包路由中的原始类型
在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); 其实没有效果。因此类型转换会发生在:
- Laravel的控制器调用方法Controller::callAction()
- 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);
// ...
}
);
大部分时候,你可能会使用路由模型绑定。但如果你有一个路由参数想要用类型提示,可以采用此法。