请求(Reqeust)
获取用户请求
PHP并未提供集中的、统一的界面以获取用户请求,而是分散在 $_SERVER $_POST
等变量和其他代码中。 万能的Yii怎么会允许群雄割据这种局面出现呢?他肯定是要一统江湖的。 那么对于任何Yii应用而言,初始化后第一件正事,就是获取用户请求。 这个代码在 yii\base\Application::run()
中:
public function run()
{
try {
$this->state = self::STATE_BEFORE_REQUEST;
$this->trigger(self::EVENT_BEFORE_REQUEST);
$this->state = self::STATE_HANDLING_REQUEST;
// 获取用户请求,并进行处理,处理的过程也是产生响应内容的过程
$response = $this->handleRequest($this->getRequest());
$this->state = self::STATE_AFTER_REQUEST;
$this->trigger(self::EVENT_AFTER_REQUEST);
$this->state = self::STATE_SENDING_RESPONSE;
// 将响应内容发送回用户
$response->send();
$this->state = self::STATE_END;
return $response->exitStatus;
} catch (ExitException $e) {
$this->end($e->statusCode, isset($response) ? $response : null);
return $e->statusCode;
}
}
上面的代码主要看注释的两个地方,聪明的读者朋友们一定都猜出来了, $this->getRequest()
就是用于获取用户请求的嘛。
其实这是一个getter,用于获取Application的request组件 (component) 。Yii用这个组件来代表用户请求, 他承载着所有的用户输入信息。
我们知道,Yii应用有命令行(Console)应用和Web应用之分。因此,这个Request类其实涉及到了以下的类:
- yii\base\Request Request类基类
- yii\console\Request 表示Console应用的的Request
- yii\web\Request 表示Web应用的Request
下面我们逐一进行讲解。
基类Request
基类是对Console应用和Web应用Request的抽象,他仅仅定义了两个属性和一个虚函数:
abstract class Request extends Component
{
// 属性scriptFile,用于表示入口脚本
private $_scriptFile;
// 属性isConsoleRequest,用于表示是否是命令行应用
private $_isConsoleRequest;
// 虚函数,要求子类来实现
// 这个函数的功能主要是为了把Request解析成路由和相应的参数
abstract public function resolve();
// isConsoleRequest属性的getter函数
// 使用 PHP_SAPI 常量判断当前应用是否是命令行应用
public function getIsConsoleRequest()
{
// 一切 PHP_SAPI 不为 'cli' 的,都不是命令行
return $this->_isConsoleRequest !== null ?
$this->_isConsoleRequest : PHP_SAPI === 'cli';
}
// isConsoleRequest属性的setter函数
public function setIsConsoleRequest($value)
{
$this->_isConsoleRequest = $value;
}
// scriptFile属性的getter函数
// 通过 $_SERVER['SCRIPT_FILENAME'] 来获取入口脚本名
public function getScriptFile()
{
if ($this->_scriptFile === null) {
if (isset($_SERVER['SCRIPT_FILENAME'])) {
$this->setScriptFile($_SERVER['SCRIPT_FILENAME']);
} else {
throw new InvalidConfigException(
'Unable to determine the entry script file path.');
}
}
return $this->_scriptFile;
}
// scriptFile属性的setter函数
public function setScriptFile($value)
{
$scriptFile = realpath(Yii::getAlias($value));
if ($scriptFile !== false && is_file($scriptFile)) {
$this->_scriptFile = $scriptFile;
} else {
throw new InvalidConfigException(
'Unable to determine the entry script file path.');
}
}
}
yii\base\Request
通过getter和setter提供了两个可读写的属性, isConsoleRequest 和 scriptFile 。 同时,要求子类实现一个 resolve() 方法。
基类的代码相对简单,主要涉及到PHP的一些知识,如 PHP_SAPI $_SERVER[‘SCRIPT_FILENAME’] 等, 读者朋友们可以通过搜索引擎或PHP手册了解下相关的知识,相信上面的代码难不倒你们的。
命令行应用Request
命令行应用Request由 yii\console\Request 负责实现,相比较于 yii\base\Request 稍有丰富:
class Request extends \yii\base\Request
{
// 属性 params,用于表示命令行参数
private $_params;
// params属性的getter函数
// 通过 $_SERVER['argv'] 来获取命令行参数
public function getParams()
{
if (!isset($this->_params)) {
if (isset($_SERVER['argv'])) {
$this->_params = $_SERVER['argv'];
// 删除数组的第一个元素,这个元素是PHP脚本名。
// 因此,属性params中全部是参数,不带脚本名
array_shift($this->_params);
} else {
$this->_params = [];
}
}
return $this->_params;
}
// params属性的setter函数
public function setParams($params)
{
$this->_params = $params;
}
// 父类虚函数的实现
public function resolve()
{
// 获取全部的命令行参数
$rawParams = $this->getParams();
// 第一个命令行参数作为路由
if (isset($rawParams[0])) {
$route = $rawParams[0];
array_shift($rawParams);
} else {
$route = '';
}
$params = [];
// 遍历剩余的全部命令行参数
foreach ($rawParams as $param) {
// 正则匹配每一个参数
if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) {
// 参数名
$name = $matches[1];
// yii\console\Application::OPTION_APPCONFIG = 'appconfig'
if ($name !== Application::OPTION_APPCONFIG) {
$params[$name] = isset($matches[3]) ? $matches[3] : true;
}
// 无名参数,直接作为参数值
} else {
$params[] = $param;
}
}
return [$route, $params];
}
}
相比较于 yii\base\Request , yii\console\Request 提供了一个 params 属性, 该属性以数组形式保存了入口脚本 Yii 的命令行参数。这是通过 $_SERVER[‘argv’] 获取的。 注意 params 属性不保存入口脚本名。入口脚本名由基类的 scriptName 属性保存。
同时, yii\console\Request
还实现了父类的 resolve() 虚函数, 这个函数主要做了这么几件事:
将 params 属性的第一个元素作为路由。如果入口脚本未提供任何参数,也即 params 是个空数组, 那么将路由置为一个空字符串。
遍历 params 中剩余的参数,使用正则匹配Yii应用的参数名和参数值,看看是不是 –参数名=参数值 形式。 其中,以 – 打头的任意字母、数字、下划线的组合,就是参数名。 紧跟参数名的 = 后面的内容,则为参数值。 对于仅有参数名,没有参数值的,视参数值为 true 。
如果正则匹配不成功,则将这个命令行参数作为Yii应用的一个无名参数的值。
如果第二步中的参数名为 appconfig 则忽略该参数,Console Application会专门针对该参数进行处理。
上面步骤中的参数和参数值,被保存进一个数组中。数组的键表示参数名,数组的值表示参数值。
最终 resolve() 返回一个数组,第一个元素是一个表示路由的字符串,第二元素则是参数数组。 该方法由Application在处理Request时调用。
关于 appconfig 参数的问题,只要在调用 yii 时,指定了 appconfig 参数, 就表明不使用默认的参数配置文件,而使用该参数所指定的配置文件。相关的代码在 yii\console\Application 中:
39
// 定义一个常量
const OPTION_APPCONFIG = 'appconfig';
// yii\console\Application类的构造函数
public function __construct($config = [])
{
// 重点看这句,会调用loadConfig() 成员函数
$config = $this->loadConfig($config);
parent::__construct($config);
}
// 如果指定的配置文件存在,那么返回其配置数组
// 否则,返回构造函数调用时的数组
protected function loadConfig($config)
{
if (!empty($_SERVER['argv'])) {
// 设定了一个字符串 "--appconfig="
$option = '--' . self::OPTION_APPCONFIG . '=';
// 遍历所有命令行参数,看看能不能找到上面说的这个字符串
foreach ($_SERVER['argv'] as $param) {
if (strpos($param, $option) !== false) {
// 截取参数值部分
$path = substr($param, strlen($option));
if (!empty($path) && is_file($file = Yii::getAlias($path))) {
// 将指定文件的内容引入进来
return require($file);
} else {
die("The configuration file does not exist: $path\n");
}
}
}
}
return $config;
}
讲完了Request基类和命令行应用的Request只是热身而已,接下来要讲的Web应用Request才是重头。 毕竟最最主要的,还是Web开发嘛。考虑到Web Request的内容较多,还是单独成 Web应用Request 来讲吧。