企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 获取用户请求[](http://www.digpage.com/request.html#id1 "Permalink to this headline") 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[](http://www.digpage.com/request.html#id2 "Permalink to this headline") 基类是对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[](http://www.digpage.com/request.html#id3 "Permalink to this headline") 命令行应用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 中: ~~~ // 定义一个常量 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_](http://www.digpage.com/web_request.html#web-request) 来讲吧。