Bootstrap 文件中使用 $_SERVER ['REQUEST_URI'] 遇到的一个小坑
接手了别人的代码,框架是 lumen,发现做单元测试的时候,报错如下:
[ErrorException]
Undefined index: REQUEST_URI
查了一下代码发现 bootstrap/app.php
中有这么一段代码:
if (strpos($_SERVER['REQUEST_URI'], 'backend') === 0) {
$app->register(App\Providers\PermissionServiceProvider::class);
$app->group(['middleware' => ['operation', 'permission'], 'namespace' => 'App\Http\Controllers\Backend'], function() use ($app) {
require __DIR__ . '/../routes/backend.php';
});
} else {
$app->register(App\Providers\AuthServiceProvider::class);
require __DIR__ . '/../routes/frontend.php';
}
在 phpunit 这种命令行方式运行的情况下,$_SERVER['REQUEST_URI']
的确是找不到的。但是为什么单元测试可以模拟发送请求呢?为什么query路径可以被单元测试模拟呢?小小的挖一下不难发现,在单元测试中使用的get,post,put,delete 等之类的方法,是对 call 方法的包装,这里就直接贴一下 部分源码:
举例 get 方法源码:
public function get($uri, array $headers = [])
{
$server = $this->transformHeadersToServerVars($headers);
$this->call('GET', $uri, [], [], [], $server);
return $this;
}
发现其中核心部分就是 $this->call() 这句,继续追 call:
public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
{
$this->currentUri = $this->prepareUrlForRequest($uri);
$symfonyRequest = SymfonyRequest::create(
$this->currentUri, $method, $parameters,
$cookies, $files, $server, $content
);
return $this->response = $this->app->prepareResponse(
$this->app->handle(Request::createFromBase($symfonyRequest))
);
}
会发现其中,就到了SymfonyRequest::create() 这句($this->currentUri 这里比较简单,有兴趣过程自己追下),于是我们就来看看这个 create 是如何工作的(这下要追到这里了 \vendor\symfony\http-foundation\Request.php):
照例还是先贴源码,
public static function create($uri, $method = 'GET', $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null)
{
$server = array_replace(array(
'SERVER_NAME' => 'localhost',
'SERVER_PORT' => 80,
'HTTP_HOST' => 'localhost',
'HTTP_USER_AGENT' => 'Symfony/3.X',
'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5',
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
'REMOTE_ADDR' => '127.0.0.1',
'SCRIPT_NAME' => '',
'SCRIPT_FILENAME' => '',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'REQUEST_TIME' => time(),
), $server);
$server['PATH_INFO'] = '';
$server['REQUEST_METHOD'] = strtoupper($method);
$components = parse_url($uri);
if (isset($components['host'])) {
$server['SERVER_NAME'] = $components['host'];
$server['HTTP_HOST'] = $components['host'];
}
if (isset($components['scheme'])) {
if ('https' === $components['scheme']) {
$server['HTTPS'] = 'on';
$server['SERVER_PORT'] = 443;
} else {
unset($server['HTTPS']);
$server['SERVER_PORT'] = 80;
}
}
if (isset($components['port'])) {
$server['SERVER_PORT'] = $components['port'];
$server['HTTP_HOST'] = $server['HTTP_HOST'].':'.$components['port'];
}
if (isset($components['user'])) {
$server['PHP_AUTH_USER'] = $components['user'];
}
if (isset($components['pass'])) {
$server['PHP_AUTH_PW'] = $components['pass'];
}
if (!isset($components['path'])) {
$components['path'] = '/';
}
switch (strtoupper($method)) {
case 'POST':
case 'PUT':
case 'DELETE':
if (!isset($server['CONTENT_TYPE'])) {
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
}
// no break
case 'PATCH':
$request = $parameters;
$query = array();
break;
default:
$request = array();
$query = $parameters;
break;
}
$queryString = '';
if (isset($components['query'])) {
parse_str(html_entity_decode($components['query']), $qs);
if ($query) {
$query = array_replace($qs, $query);
$queryString = http_build_query($query, '', '&');
} else {
$query = $qs;
$queryString = $components['query'];
}
} elseif ($query) {
$queryString = http_build_query($query, '', '&');
}
$server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : '');
$server['QUERY_STRING'] = $queryString;
return self::createRequestFromFactory($query, $request, array(), $cookies, $files, $server, $content);
}
有趣的地方来了,先随便浏览下发现了一个好像不太对的地方:不是说没有那个啥 REQUEST_URI
,怎么看起来好像这里有,那再看清楚一点这里是 $server['REQUEST_URI']
,之前那个地方是 $_SERVER['REQUEST_URI']
,一个是普通变量,一个是超全局变量。那么为什么 symfony 不去使用 PHP 自带的超全局变量,而非要自己搞个 $server
呢?
这个类里面还有一个方法解释了这个疑问:
/**
* Creates a new request with values from PHP's super globals.
*
* @return static
*/
public static function createFromGlobals()
{
// With the php's bug #66606, the php's built-in web server
// stores the Content-Type and Content-Length header values in
// HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH fields.
$server = $_SERVER;
if ('cli-server' === PHP_SAPI) {
if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) {
$server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH'];
}
if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) {
$server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE'];
}
}
$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server);
if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}
return $request;
}
重点在 $server = $_SERVER
这句,回答了刚才的疑问,这句就说明 $server
本质上是对 $_SERVER
的包装,在没有对它进行后面的处理之前,他们基本是一样的。(注释中的 bug ,有兴趣就可以看下。)
回到完毕这个问题,我们继续跳回,刚才那个 create
方法, symfony 自己通过 http_build_query
对 query path 进行了重新包装。所以在使用phpunit的时候,不需要依赖于超全局的系统变量就可以模拟 HTTP 请求了。
开头的那个坑其实也很好填,加个prefix就行了,无需自己再去判断路径:
//backend
{
$app->register(App\Providers\PermissionServiceProvider::class);
$app->group(['middleware' => ['operation', 'permission'],
'namespace' => 'App\Http\Controllers\Backend',
'prefix' => 'backend',
], function() use ($app) {
require __DIR__ . '/../routes/backend.php';
});
}
//frontend
{
$app->register(App\Providers\AuthServiceProvider::class);
require __DIR__ . '/../routes/frontend.php';
}
水平有限,如有不妥之处,欢迎指正。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: