I assume you need to simulate a request without actually dispatching it. With a simulated request in place, you want to probe it for parameter values and develop your testcase.
有一种没有记录的方法可以做到这一点.你会惊讶的!
问题
正如你已经知道的,拉威尔的Illuminate\Http\Request
门课是在Symfony\Component\HttpFoundation\Request
门课的基础上建立起来的.upstream 类不允许您以手动方式设置请求URI.它根据实际的请求头来计算.别无 Select .
OK, enough with the chatter. Let's try to simulate a request:
<?php
use Illuminate\Http\Request;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
As you mentioned yourself, you'll get a:
Error: Call to a member function parameter() on null
We need a Route
为什么?为什么route()
返回null
?
Have a look at its implementation as well as the implementation of its companion method; getRouteResolver()
. The getRouteResolver()
method returns an empty closure, then route()
calls it and so the $route
variable will be null
. Then it gets returned and thus... the error.
In a real HTTP request context, Laravel sets up its route resolver, so you won't get such errors. Now that you're simulating the request, you need to set up that by yourself. Let's see how.
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
请参见从Laravel's own RouteCollection
class创建Route
的另一个示例.
Empty parameters bag
So, now you won't get that error because you actually have a route with the request object bound to it. But it won't work yet. If we run phpunit at this point, we'll get a null
in the face! If you do a dd($request->route())
you'll see that even though it has the info
parameter name set up, its parameters
array is empty:
Illuminate\Routing\Route {#250
#uri: "testing/{info}"
#methods: array:2 [
0 => "GET"
1 => "HEAD"
]
#action: array:1 [
"uses" => null
]
#controller: null
#defaults: []
#wheres: []
#parameters: [] <===================== HERE
#parameterNames: array:1 [
0 => "info"
]
#compiled: Symfony\Component\Routing\CompiledRoute {#252
-variables: array:1 [
0 => "info"
]
-tokens: array:2 [
0 => array:4 [
0 => "variable"
1 => "/"
2 => "[^/]++"
3 => "info"
]
1 => array:2 [
0 => "text"
1 => "/testing"
]
]
-staticPrefix: "/testing"
-regex: "#^/testing/(?P<info>[^/]++)$#s"
-pathVariables: array:1 [
0 => "info"
]
-hostVariables: []
-hostRegex: null
-hostTokens: []
}
#router: null
#container: null
}
所以将['info' => 5]
传递到Request
构造函数没有任何效果.让我们来看看Route
类,看看它的$parameters
property是如何填充的.
当我们bind the request对象路由时,$parameters
属性由对bindParameters()
方法的后续调用填充,该方法反过来调用bindPathParameters()
来计算路径特定的参数(在本例中,我们没有主机参数).
该方法将请求者解码路径与值Symfony's Symfony\Component\Routing\CompiledRoute
的正则表达式(您也可以在上面的转储中看到)进行匹配,并返回匹配的路径参数.如果路径与模式不匹配(这就是我们的情况),则它将为空.
/**
* Get the parameter matches for the path portion of the URI.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function bindPathParameters(Request $request)
{
preg_match($this->compiled->getRegex(), '/'.$request->decodedPath(), $matches);
return $matches;
}
问题是,当没有实际请求时,$request->decodedPath()
返回与模式不匹配的/
.所以参数包无论如何都会是空的.
Spoofing the request URI
If you follow that decodedPath()
method on the Request
class, you'll go deep through a couple of methods which will finally return a value from prepareRequestUri()
of Symfony\Component\HttpFoundation\Request
. There, exactly in that method, you'll find the answer to your question.
It's figuring out the request URI by probing a bunch of HTTP headers. It first checks for X_ORIGINAL_URL
, then X_REWRITE_URL
, then a few others and finally for the REQUEST_URI
header. You can set either of these headers to actually spoof the request URI and achieve minimum simulation of a http request. Let's see.
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], [], [], [], ['REQUEST_URI' => 'testing/5']);
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
令你惊讶的是,它打印出了5
个;info
参数的值.
Cleanup
您可能希望将功能提取到帮助器simulateRequest()
方法,或者可以跨测试用例使用的SimulatesRequests
特征.
Mocking
即使完全不可能像上面的方法那样欺骗请求URI,也可以部分模拟请求类并设置预期的请求URI.大致如下:
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$requestMock = Mockery::mock(Request::class)
->makePartial()
->shouldReceive('path')
->once()
->andReturn('testing/5');
app()->instance('request', $requestMock->getMock());
$request = request();
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
This prints out 5
as well.