/ yii2  

Yii2 源码阅读 03 - ServiceLocator Module

书接上文 yii\web\Application 类的层级结构:

yii\base\Configurable
|--- yii\base\BaseObject
|--- yii\base\Component
|--- yii\di\ServiceLocator
|--- yii\base\Module
|--- yii\base\Application
|--- yii\web\Application

yii\di\ServiceLocator

参见:服务定位器(Service Locator)

ServiceLocator implements a service locator.

在首次请求某个服务时,服务定位器在 JNDI 中查找服务,并缓存该服务对象。当再次请求相同的服务时,服务定位器会在它的缓存中查找,这样可以在很大程度上提高应用程序的性能。

要使用 ServiceLocator,首先需要通过调用 set() 或 setComponents() 将 component IDs 注册到定位器的相应组件定义中。

然后可以调用 get() 来检索具有指定 ID 的 component。定位器将根据定义自动实例化和配置 component。

$locator = new \yii\di\ServiceLocator;
$locator->setComponents([
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'sqlite:path/to/file.db',
],
'cache' => [
'class' => 'yii\caching\DbCache',
'db' => 'db',
],
'search' => SolrServiceBuilder::build('127.0.0.1'),
]);

$db = $locator->get('db'); // or $locator->db
$cache = $locator->get('cache'); // or $locator->cache

因为 \yii\base\Module 继承自 ServiceLocator,所以 modules 和 application 都是 service locators。

  • @property array $components 组件定义或已加载的组件实例的列表 (ID => definition or instance).
  • private array $_components 单例组件 实例 的 id 索引。
  • private array $_definitions 组件 定义 的 id 索引。
/**
* 向此定位器注册组件定义。
*
* For example,
*
* // a class name
* $locator->set('cache', 'yii\caching\FileCache');
*
* // a configuration array
* $locator->set('db', [
* 'class' => 'yii\db\Connection',
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
* 'username' => 'root',
* 'password' => '',
* 'charset' => 'utf8',
* ]);
*
* // an anonymous function
* $locator->set('cache', function ($params) {
* return new \yii\caching\FileCache;
* });
*
* // an instance
* $locator->set('cache', new \yii\caching\FileCache);
*
* If a component definition with the same ID already exists, it will be overwritten.
*
* @param string $id component ID (e.g. `db`).
* @param mixed $definition 要注册到此定位器的组件定义。
*
* @throws InvalidConfigException if the definition is an invalid configuration array
*/
public function set($id, $definition)
{
// 移除之前的 component 实例
unset($this->_components[$id]);

if ($definition === null) {
// 移除之前的 component 定义
unset($this->_definitions[$id]);
return;
}

if (is_object($definition) || is_callable($definition, true)) {
// an object, a class name, or a PHP callable
$this->_definitions[$id] = $definition;
} elseif (is_array($definition)) {
// a configuration array
// 数组中必须有 class
if (isset($definition['__class'])) {
$this->_definitions[$id] = $definition;
$this->_definitions[$id]['class'] = $definition['__class'];
unset($this->_definitions[$id]['__class']);
} elseif (isset($definition['class'])) {
$this->_definitions[$id] = $definition;
} else {
throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
}
} else {
throw new InvalidConfigException("Unexpected configuration type for the \"$id\" component: " . gettype($definition));
}
}

知识点:

is_callable($definition, true) 也就是第二个参数为 true 时,只检查格式(字符串或者数组),不检测内容(是否真的存在,是否真的可被调用)。

var_dump(is_callable('不是方法名的字符串', false)); // bool(false)
var_dump(is_callable('不是方法名的字符串', true)); // bool(true)

var_dump(is_callable(['随便写,不是对象', '随便写,不是对象的方法名'], false)); // bool(false)
var_dump(is_callable(['随便写,不是对象', '随便写,不是对象的方法名'], true)); // bool(true)
/**
* Registers a set of component definitions in this locator.
*/
public function setComponents($components)
{
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
/**
* 返回具有指定 ID 的组件实例。
*
* @param string $id component ID (e.g. `db`).
* @param bool $throwException whether to throw an exception if `$id` is not registered with the locator before
*
* @return object|null the component of the specified ID. If `$throwException` is false and `$id` is not registered before, null will be returned.
*
* @throws InvalidConfigException if `$id` refers to a nonexistent component ID
*
* @see has()
* @see set()
*/
public function get($id, $throwException = true)
{
// 已实例化过
if (isset($this->_components[$id])) {
return $this->_components[$id];
}

if (isset($this->_definitions[$id])) {
// 已定义 未实例化
$definition = $this->_definitions[$id];
if (is_object($definition) && !$definition instanceof Closure) {
// 是对象 & 非闭包 直接赋值
return $this->_components[$id] = $definition;
}
// 实例化核心方法
return $this->_components[$id] = Yii::createObject($definition);
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
}

return null;
}

Yii 应用程序本质上是一个模块树。

参见:遍历树(Tree traversal)

yii\base\Module

Module 是模块和应用程序类的基类。

Module 代表一个子应用程序,它本身包含 MVC 元素,如模型、视图、控制器等。

一个模块可以由 modules|sub-modules 组成。

参见:模块

  • @property-write array $aliases 被定义的别名数组
  • @property string $basePath module 的根目录
  • @property string $layoutPath 布局文件的根目录 默认值 {viewPath}/layouts
  • @property array $modules 模块(索引是 IDs)
  • @property-read string $uniqueId modules 的唯一 ID
  • @property string $version module 的版本
  • @property string $viewPath view files 的根目录 默认值 {basePath}/views

属性:

  • public $params = []; 自定义的模块参数 (name => value)
  • public string $id; 唯一 ID
  • public Module|null $module; 该模块的父模块。null 这个模块没有父模块。
  • public string|bool|null $layout;
  • public $controllerMap = [];
  • public string|null $controllerNamespace; if the namespace of this module is foo\bar default foo\bar\controllers
  • public $defaultRoute = ‘default’; The route may consist of child module ID, controller ID, and/or action ID.
  • private $_version;

参见:类自动加载(Autoloading)

事件:

  • EVENT_BEFORE_ACTION before executing a controller action
  • EVENT_AFTER_ACTION after executing a controller action
public function __construct($id, $parent = null, $config = [])
{
$this->id = $id;
$this->module = $parent;
parent::__construct($config);
}

知识点:

方法重载(overload)是类的多态的一种实现。方法在被调用的时候,虽然方法名字相同,但根据参数的不同可以自动调用相应的函数。但是 PHP 并不直接支持,只用通过 __call 或者 func_get_args() func_num_args() + call_user_func_array() 实现。

方法重写(override)否重写父类方法只会根据方法名是否一致判断(5.3 以后重写父类方法参数个数必须一致)。访问级别只可以等于或者宽松于父类(private 的重写可以是 private protected public 佛是重新定义了一个方法)。final 修饰的类方法不可被子类重写。

__construct 的参数子类可以与父类不同。

/**
* 返回当前请求的这个模块类的实例。
* 如果当前未请求模块类,则返回 null。
* 提供此方法是为了让您从模块内的任何位置访问模块实例。
* @return static|null
*/
public static function getInstance()
{
$class = get_called_class();
return isset(Yii::$app->loadedModules[$class]) ? Yii::$app->loadedModules[$class] : null;
}

get_called_class(); PHP 5.5 后等于 static::class the “Late Static Binding” class name.

/**
* 返回一个 ID,该 ID 在当前应用程序的所有模块中唯一标识此模块。
* 注意,如果模块是一个 application,将返回一个空字符串。
* @return string the unique ID of the module.
*/
public function getUniqueId()
{
// 判断是否有父模块
return $this->module ? ltrim($this->module->getUniqueId() . '/' . $this->id, '/') : $this->id;
}
/**
* 返回模块的根目录。
* 它默认是包含模块类文件的目录。
* @return string the root directory of the module.
*/
public function getBasePath()
{
if ($this->_basePath === null) {
// 反射
$class = new \ReflectionClass($this);
// $class->getFileName() 获取类文件的全路径
// dirname 获取文件的目录
$this->_basePath = dirname($class->getFileName());
}

return $this->_basePath;
}
/**
* 设置模块的根目录。
* 此方法只能在 构造函数 的开头调用。
* @param string $path the root directory of the module.
* @throws InvalidArgumentException if the directory does not exist.
*/
public function setBasePath($path)
{
$path = Yii::getAlias($path);
$p = strncmp($path, 'phar://', 7) === 0 ? $path : realpath($path);
if (is_string($p) && is_dir($p)) {
$this->_basePath = $p;
} else {
throw new InvalidArgumentException("The directory does not exist: $path");
}
}
$dir = './../yii2-app-basic/../';
var_dump(dirname($dir));
var_dump(realpath($dir));

// realpath 返回规范化的绝对路径名
// string(19) "./../yii2-app-basic"
// string(23) "/Users/bob/web"

// 二进制安全的前 n 个字符的字符串比较
// strncmp($path, 'phar://', 7) === 0
/**
* Returns current module version.
* 如果version未显式设置,将使用 defaultVersion() 方法确定其值。
* @return string the version of this module.
* @since 2.0.11
*/
public function getVersion()
{
if ($this->_version === null) {
$this->_version = $this->defaultVersion();
} else {
// 判断 _version 是否是标量
if (!is_scalar($this->_version)) {
// 对闭包的支持
$this->_version = call_user_func($this->_version, $this);
}
}

return $this->_version;
}
/**
* 运行由路指定的控制器动作。
* 这个方法解析指定的路由,并创建相应的子模块、控制器和动作实例。
* 然后调用 Controller::runAction() 以使用给定的参数运行操作。
* 如果路由为空,该方法将使用 defaultRoute。
* @param string $route the route that specifies the action.
* @param array $params the parameters to be passed to the action
* @return mixed the result of the action.
* @throws InvalidRouteException if the requested route cannot be resolved into an action successfully.
*/
public function runAction($route, $params = [])
{
// 创建 ctrl 见下文
$parts = $this->createController($route);
if (is_array($parts)) {
/* @var $controller Controller */
list($controller, $actionID) = $parts;
// 获取旧的
$oldController = Yii::$app->controller;
// 绑定新的
Yii::$app->controller = $controller;
// 运行 ctrl action
$result = $controller->runAction($actionID, $params);
// 如果旧的不等于空,回绑定旧的
if ($oldController !== null) {
Yii::$app->controller = $oldController;
}

// 返回结果
return $result;
}

// 获取标示
$id = $this->getUniqueId();
throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
}

/**
* 基于给定的路由创建一个控制器实例。
*
* 路由应该是相对于这个模块的。该方法实现了以下算法来解析给定的路由:
*
* 1. 如果路由为空,则使用 defaultRoute;
* 2. 如果在 controllerMap 中找到路由的第一个段,则根据 controllerMap 中找到的相应配置创建一个控制器;
* 3. 如果路由的第一个片段是一个有效的模块 ID,如 modules 中声明的,用路由的其余部分调用模块的 createController();
* 4. 给定的路由格式为 `abc/def/xyz`. 尝试 `abc\DefController` 或者 `abc\def\XyzController` class within the [[controllerNamespace|controller namespace]].
*
* 如果上面的任何一个步骤解析为一个控制器,它将与路由的其余部分一起返回,后者将被视为动作 ID。否则,将返回 false。
*
* @param string $route the route consisting of module, controller and action IDs.
* @return array|bool 如果控制器创建成功,它将与请求的操作ID一起返回。否则将返回 false。
* @throws InvalidConfigException if the controller class and its file do not match.
*/
public function createController($route)
{
if ($route === '') {
$route = $this->defaultRoute;
}

// double slashes or leading/ending slashes may cause substr problem
$route = trim($route, '/');
if (strpos($route, '//') !== false) {
return false;
}

if (strpos($route, '/') !== false) {
// abc/def/xyz
// id: abc
// route: def/xyz
list($id, $route) = explode('/', $route, 2);
} else {
$id = $route;
$route = '';
}

// module and controller map take precedence
if (isset($this->controllerMap[$id])) {
// 通过 controllerMap 创建
$controller = Yii::createObject($this->controllerMap[$id], [$id, $this]);
return [$controller, $route];
}
// 获取父级模块
$module = $this->getModule($id);
if ($module !== null) {
// 通过父级模块创建
return $module->createController($route);
}

// id abc
// route def/xyz
if (($pos = strrpos($route, '/')) !== false) {
$id .= '/' . substr($route, 0, $pos);
$route = substr($route, $pos + 1);
}
// id abc/def
// route xyz

// 创建 ctrl 见下文
$controller = $this->createControllerByID($id);
if ($controller === null && $route !== '') {
$controller = $this->createControllerByID($id . '/' . $route);
$route = '';
}

return $controller === null ? false : [$controller, $route];
}


/**
* 通过给定的 controller ID 创建 controller
*
* 控制器 ID 与该模块相关。控制器类的命名空间应该在 controllerNamespace 下。
*
* 注意,此方法不检查 modules 或 controllerMap。
*
* @param string $id the controller ID.
* @return Controller|null 如果控制器 ID 无效,则为 null。
* @throws InvalidConfigException if the controller class and its file name do not match. 此异常只会在 debug 模式抛出。
*/
public function createControllerByID($id)
{
// id abc/def
$pos = strrpos($id, '/');
if ($pos === false) {
$prefix = '';
$className = $id;
} else {
$prefix = substr($id, 0, $pos + 1);
$className = substr($id, $pos + 1);
}
// prefix abc
// className def

// Checks if class name or prefix is incorrect
if ($this->isIncorrectClassNameOrPrefix($className, $prefix)) {
return null;
}
// https://www.pagecolumn.com/tool/pregtest.htm
// 中划线转大驼峰
$className = preg_replace_callback('%-([a-z0-9_])%i', function ($matches) {
return ucfirst($matches[1]);
}, ucfirst($className)) . 'Controller';
$className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\');
if (strpos($className, '-') !== false || !class_exists($className)) {
return null;
}

// one of its parents or implements it
// 与 instanceof 的区别是:instanceof 可以是此类的实例,is_subclass_of 要是此类的子类
if (is_subclass_of($className, 'yii\base\Controller')) {
$controller = Yii::createObject($className, [$id, $this]);
return get_class($controller) === $className ? $controller : null;
} elseif (YII_DEBUG) {
throw new InvalidConfigException('Controller class must extend from \\yii\\base\\Controller.');
}

return null;
}

/**
* Checks if class name or prefix is incorrect
*
* @param string $className
* @param string $prefix
* @return bool
*/
private function isIncorrectClassNameOrPrefix($className, $prefix)
{
if (!preg_match('%^[a-z][a-z0-9\\-_]*$%', $className)) {
return true;
}
if ($prefix !== '' && !preg_match('%^[a-z0-9_/]+$%i', $prefix)) {
return true;
}

return false;
}

References

– EOF –