控制器和闭包路由中的原始类型
在 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 类型提示可以安全进行类型转换了。尽管用例作用有限,
我想这些细小差别还是能有助于理解路由参数的类型提示。
严格类型(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);
// ...
}
);
大部分时候,你可能会使用路由模型绑定。但如果你有一个路由参数想要用类型提示,可以采用此法。