Yii2框架源码分析
Yii框架是一款优秀的PHP框架, 我们来分析一下它的源码。
全局辅助类Yii.php
我们在入口文件里面,已经看到引入了Yii.php,那么在这里我们把源码贴出来:
<?php
require __DIR__ . '/BaseYii.php';
class Yii extends \yii\BaseYii
{
}
//Yii全局辅助类,没啥可说的。
//注册自动加载,为Yii里面的autoload方法。(当然,autoload方法存在于Yii的基类:\yii\BaseYii)
spl_autoload_register(['Yii', 'autoload'], true, true);
// 自动加载的所有类的全局树。
Yii::$classMap = require __DIR__ . '/classes.php';
// 实例化一个容器,作为当前应用的容器。
Yii::$container = new yii\di\Container();首先,我们引入了辅助类的基类BaseYii.php (因为这个时候,自动加载还没注册,所以只能手动引入),然后注册自动加载。我们接下来讲一下BaseYii.php这个类。这个类主要实现的事情有:
- 定义一些全局常量。
- 类的自动加载
- 路径别名的设置和获取。
- 创建对象并注入到当前容器。
- 不同级别日志的记录功能。
- 语言翻译。
- 对象属性的初始设置。
当引入yii.php的时候,根据程序执行来看,做的事情有:定义常量和自动加载。我们这里顺便把BaseYii.php的所有支持的功能都列了出来。这里主要讲自动加载、对象的属性设置、以及创建类三个方法。
自动加载:
public static function autoload($className)
{
if (isset(static::$classMap[$className])) {
$classFile = static::$classMap[$className];
if ($classFile[0] === '@') {
$classFile = static::getAlias($classFile);
}
} elseif (strpos($className, '\\') !== false) {
$classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false);
if ($classFile === false || !is_file($classFile)) {
return;
}
} else {
return;
}
include $classFile;
if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && !trait_exists($className, false)) {
throw new UnknownClassException("Unable to find '$className' in file: $classFile. Namespace missing?");
}
}
public static function getAlias($alias, $throwException = true)
{
// 如果第一个字符不是@符号,代表不是Yii框架内部的命名空间,直接返回。
if (strncmp($alias, '@', 1)) {
// not an alias
return $alias;
}
// 获取根命名空间
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
// 根命名空间必须提前注册到全局注册树上。如果根命名空间不存在就直接返回false
if (isset(static::$aliases[$root])) {
if (is_string(static::$aliases[$root])) {
return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos);
}
foreach (static::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $path . substr($alias, strlen($name));
}
}
}
if ($throwException) {
throw new InvalidArgumentException("Invalid path alias: $alias");
}
return false;
}首先,autoload方法已经在Yii.php中通过spl_autoload_register()函数注册。autoload()方法存在一个参数$className,就是完整的类名称(包含命名空间),$className是php自动传入的,我们不需要做处理。
$classMap是BaseYii.php的静态属性,它是自动加载完毕的类的全局树。数组键是类名(不带前导反斜杠),数组值是对应的类文件路径。getAlias()用来获取真实类文件路径。
最后引入文件。
创建对象:
public static function createObject($type, array $params = [])
{
// 创建对象,并将其注入当前应用的容器中。
if (is_string($type)) {
return static::$container->get($type, $params);
}
if (is_callable($type, true)) {
return static::$container->invoke($type, $params);
}
if (!is_array($type)) {
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
if (isset($type['__class'])) {
$class = $type['__class'];
unset($type['__class'], $type['class']);
return static::$container->get($class, $params, $type);
}
if (isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
}
throw new InvalidConfigException('Object configuration must be an array containing a "class" or "__class" element.');
}createObject其实就是new的增强版,他可以根据配置创建一个对象。
第一个参数$type支持字符串、数组、回调函数三种类型。如果type是一个数组,该数组必须包括"__class"或者"class"键。该键对应的值是需要创建的对象对应的类名称。数组里面其他的参数是这个类的属性的初始值的设置。$params则是该类的构造方法所需要的参数。如果type是字符串,则是一个类名称,如果是回调函数,则直接出发回调。$params就是回调函数所需参数。
具体创建对象的实现逻辑,我会在后面的容器章节进行讲解。
设置对象初始属性:
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}这个方法第一个参数是需要设置属性的对象, 第二个参数是需要设置的对象的属性(键值对形式的数组)。代码及其简单,为什么单独拿出来讲呢?因为这里涉及到php魔术方法__set()的一个触发场景。
__set()当设置一个没有访问权限的属性值,或者是设置不存在的属性值的时候会触发。但是yii2源码中,为了满足编辑器的友好展示,使用注释的方式表示类的属性。(这种方式其实并没什么卵用,相当于没写)例如
/**
* @property array $prop;
* Class A
*/
class A
{
}这样可以触发__set()和__get()方法。因为属性$prop不存在呀。
容器类Container.php
首先,php中的容器是为了解决类和类之间的依赖关系的。举个栗子:
存在三个类:
class Group
{
public static $a = 0;
function __construct($a)
{
static::$a = $a;
}
}
class User
{
public function __construct(Group $group)
{
}
}
class UserList
{
public function __construct(User $user)
{
}
public function getUserList()
{
echo "this is the user-list";
}
}如果我们要调用UserList类里面的getUserList()方法,通常的做法是:
$group = new Group(1);
$user = new User($group);
$userList = new UserList($user);
$userList->getUserList();如果我们使用容器的话,需要这样做:
$container = new Container();
$container->set('Group', 'app\service\Group');
$container->set('User', 'app\service\User');
$container->set('UserList', 'app\service\UserList');
$lister = $container->get('UserList');
$lister->getUserList();我们发现:我们只需要把所有的相关联的类,全部注入到容器中,不需要关心他们之间的依赖关系。(容器帮我们完成了依赖关系的处理)
怎么实现的呢?通过php的反射。
php的反射可以获取类的相关信息。那么我们首先通过
$reflection = new \ReflectionClass($className)获取目标类的反射类。再通过
$reflection->getConstructor()获取反射类的构造方法类,再通过
$constructorParameters = $constructor->getParameters()获取构造方法所需参数类。(是一个数组)
再通过参数类的 isDefaultValueAvailable() 方法判断,参数是否有可用默认值。如果没有,则断言它是一个对象,我们就创建它。具体实现看代码:
<?php
namespace yii\di;
use ReflectionClass;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
class Container extends Component
{
private $_singletons = [];
private $_definitions = [];
private $_params = [];
private $_reflections = [];
private $_dependencies = [];
public function get($class, $params = [], $config = [])
{
// 这里是获取一个类的实例。
//$class 第一个参数是类的名称或者别名,当然,你得先使用set()方法将改类注册到容器中,才能使用get方法获取。
//$params 第二个参数是该类的构造方法所需要的参数,请在数组中按照顺序传入。
//$config 第三个参数是该类中属性的初始值得设置,是键值对形式的数组。
// Instance表示对容器中对象的引用
// 如果获取的当前类属于instance, 唯一标识就变为当前类中的id属性。
if ($class instanceof Instance) {
$class = $class->id;
}
// singleton属性是一个容器的注册树,如果说当前实例已经存在注册树上,就直接返回。
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {
// 如果不存在定义就创建实例,并返回(创建中会将其注册到全局树上)
return $this->build($class, $params, $config);
}
// 存在$definition的逻辑
// 这个属性其实就是注册类的时候(调用set()方法),传入的第二个参数。他一共有三种类型。
// 1.回调函数
// 2. 数组
// 3. 字符串。
$definition = $this->_definitions[$class];
if (is_callable($definition, true)) {
// 处理参数
$params = $this->resolveDependencies($this->mergeParams($class, $params));
// 回调函数返回值就是当前需要的实例。
$object = call_user_func($definition, $this, $params, $config);
} elseif (is_array($definition)) {
$concrete = $definition['class'];
unset($definition['class']);
// 先将参数合并
$config = array_merge($definition, $config);
// 将容器中已经注册的类参数和现在传入的参数合并。
$params = $this->mergeParams($class, $params);
if ($concrete === $class) {
// 如果名称一样,就直接创建这个对象。
$object = $this->build($class, $params, $config);
} else {
// 如果不一样,说明是别名,根据正确的类名获取实例。
$object = $this->get($concrete, $params, $config);
}
} elseif (is_object($definition)) {
// 如果是对象,直接注册到全局实例树。并返回。
return $this->_singletons[$class] = $definition;
} else {
throw new InvalidConfigException('Unexpected object definition type: ' . gettype($definition));
}
// 如果是使用setSingleton()方法注册的对象,那么将singletons赋值。
if (array_key_exists($class, $this->_singletons)) {
// singleton
$this->_singletons[$class] = $object;
}
return $object;
}
public function set($class, $definition = [], array $params = [])
{
// 将类注册到容器中。
// 设置类的定义,类的参数。
// 注销已经存在类实例。(因为重新注册之后,如果不销毁之前的实例,当类的参数不一样的时候,会导致获取的对象是之前注册的类)
// 但是有个新的问题:同时创建同一个类的两个不同实例(类参数不同),是无法实现的 0.0 或许我们需要借助一个新容器?那么这样做岂非太麻烦了?
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
unset($this->_singletons[$class]);
return $this;
}
public function setSingleton($class, $definition = [], array $params = [])
{
// 这个方法和set()方法差不多,唯一的区别就是,使用set()方法注册的类,每次get()的时候,都是重新生成一个新对象。
// 而setSingleton()方法永远存储第一次生成的对象,并将其保存在 $this->_singletons属性中。每次调用get()方法,都是获取第一次生成的实例。
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
$this->_singletons[$class] = null; // 保存键,get()方法中会根据array_key_exists()来判断
return $this;
}
public function has($class)
{
return isset($this->_definitions[$class]);
}
public function hasSingleton($class, $checkInstance = false)
{
// 是否存在实例,第二个参数表示是否已经实例化。
return $checkInstance ? isset($this->_singletons[$class]) : array_key_exists($class, $this->_singletons);
}
public function clear($class)
{
// 移除
unset($this->_definitions[$class], $this->_singletons[$class]);
}
protected function normalizeDefinition($class, $definition)
{
// 将类的定义规范化。
// $definition为空,返回类名称就是入参$class。注意类名要包含命名空间哦。
if (empty($definition)) {
return ['class' => $class];
} elseif (is_string($definition)) {
// $definition是字符串,则就是完整的正确的类名称。(包含命名空间的)
return ['class' => $definition];
} elseif ($definition instanceof Instance) {
return ['class' => $definition->id];
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
} elseif (is_array($definition)) {
if (!isset($definition['class']) && isset($definition['__class'])) {
$definition['class'] = $definition['__class'];
unset($definition['__class']);
}
if (!isset($definition['class'])) {
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
} else {
throw new InvalidConfigException('A class definition requires a "class" member.');
}
}
return $definition;
}
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
}
public function getDefinitions()
{
return $this->_definitions;
}
protected function build($class, $params, $config)
{
// 整个容器最关键的方法:创建一个类的实例。发现类的依赖关系,解决依赖。实例化依赖的相关类,并注入。
/* @var $reflection ReflectionClass */
// 这里只是获取需要生成实例的参数
list($reflection, $dependencies) = $this->getDependencies($class);
// 配置中存在“__construct()”键,覆盖实例化的初始值。
if (isset($config['__construct()'])) {
foreach ($config['__construct()'] as $index => $param) {
$dependencies[$index] = $param;
}
unset($config['__construct()']);
}
// 参数中存在值,覆盖实例化的初始值。
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
// 这里处理准备生成实例的参数。如果参数是对象,则获取实际需要的具体对象。
$dependencies = $this->resolveDependencies($dependencies, $reflection);
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
if (empty($config)) {
// 根据参数生成新的实例
return $reflection->newInstanceArgs($dependencies);
}
$config = $this->resolveDependencies($config);
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// set $config as the last parameter (existing one will be overwritten)
// 实现了接口:yii\base\Configurable,则将配置设置为最后一个参数(覆盖写),然后生成实例。
$dependencies[count($dependencies) - 1] = $config;
return $reflection->newInstanceArgs($dependencies);
}
// 其他情况,$config则是生成实例类中的属性。
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
protected function mergeParams($class, $params)
{
// 这个方法主要是将类的构造方法中的参数和用户传入的参数合并。
// $this->_params是容器中所有类的构造方法中参数的全局注册树。
if (empty($this->_params[$class])) {
return $params;
} elseif (empty($params)) {
return $this->_params[$class];
}
$ps = $this->_params[$class];
foreach ($params as $index => $value) {
$ps[$index] = $value;
}
return $ps;
}
protected function getDependencies($class)
{
// 这个方法返回了类的所有依赖。
// 返回的第一个参数是当前类的反射类,第二个参数是相关依赖。
// 如果已经存在当前类的反射,就直接返回
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
try {
$reflection = new ReflectionClass($class);
} catch (\ReflectionException $e) {
throw new InvalidConfigException('Failed to instantiate component or class "' . $class . '".', 0, $e);
}
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
// php版本大于等于5.6,并且参数是可变的,直接返回。(可变参数是最后一个参数。)
break;
} elseif ($param->isDefaultValueAvailable()) {
// 默认值可用,就将默认值存储。
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();
// 没有默认值,获取当前类名称,生成instance实例
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
// 赋值并返回。
$this->_reflections[$class] = $reflection;
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
protected function resolveDependencies($dependencies, $reflection = null)
{
// 这个方法的作用,就是将目标类中的构造方法中,依赖类的参数,替换为实际的对象实例。
// 如果当前类不属于引用对象类instance,那么就不做处理。
foreach ($dependencies as $index => $dependency) {
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
// 首先,如果存在id属性,那么就继续从容器中获取依赖类。
$dependencies[$index] = $this->get($dependency->id);
} elseif ($reflection !== null) {
// 如果存在反射类,那么就通过反射,获取构造函数类,再获取构造函数参数,再通过索引获取参数名称。
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName(); // 获取类名称
// 剖出类缺少参数的异常
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
}
}
return $dependencies;
}
public function invoke(callable $callback, $params = [])
{
// 解析函数参数中的依赖,然后调用回调函数。
return call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
}
public function resolveCallableDependencies(callable $callback, $params = [])
{
if (is_array($callback)) {
$reflection = new \ReflectionMethod($callback[0], $callback[1]);
} elseif (is_object($callback) && !$callback instanceof \Closure) {
$reflection = new \ReflectionMethod($callback, '__invoke');
} else {
$reflection = new \ReflectionFunction($callback);
}
$args = [];
// 是否是关联数组
$associative = ArrayHelper::isAssociative($params);
foreach ($reflection->getParameters() as $param) {
$name = $param->getName();
if (($class = $param->getClass()) !== null) {
$className = $class->getName();
if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
// 参数可变,合并参数。并打断。
$args = array_merge($args, array_values($params));
break;
} elseif ($associative && isset($params[$name]) && $params[$name] instanceof $className) {
$args[] = $params[$name];
unset($params[$name]);
} elseif (!$associative && isset($params[0]) && $params[0] instanceof $className) {
$args[] = array_shift($params);
} elseif (isset(Yii::$app) && Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
$args[] = $obj;
} else {
// If the argument is optional we catch not instantiable exceptions
try {
$args[] = $this->get($className);
} catch (NotInstantiableException $e) {
if ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} else {
throw $e;
}
}
}
} elseif ($associative && isset($params[$name])) {
$args[] = $params[$name];
unset($params[$name]);
} elseif (!$associative && count($params)) {
$args[] = array_shift($params);
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} elseif (!$param->isOptional()) {
$funcName = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when calling \"$funcName\".");
}
}
foreach ($params as $value) {
$args[] = $value;
}
return $args;
}
public function setDefinitions(array $definitions)
{
// 实现了 set() 的队列。
foreach ($definitions as $class => $definition) {
if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition && is_array($definition[1])) {
$this->set($class, $definition[0], $definition[1]);
continue;
}
$this->set($class, $definition);
}
}
public function setSingletons(array $singletons)
{
// 实现了 setSingleton() 的队列。
foreach ($singletons as $class => $definition) {
if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition) {
$this->setSingleton($class, $definition[0], $definition[1]);
continue;
}
$this->setSingleton($class, $definition);
}
}
}上边我贴出了完整的容器类以及相关的源码分析。其中比较重要的几个方法是:get(),set(),build()。 真正的实例的创建的方法是build(),而它是在get()方法中进行调用。set()方法只是设置需要实例化它的参数。
定位器ServiceLocator.php
$this和new static()
整个框架中,很多地方会用到$this。$this如果存在类的方法中,表示当前类。但是如果存在子类继承的情况下,则他会永远返回最终调用的子类(不管$this出现在父类或者子类),举个栗子:
class A extends B {
public function test()
{
return "A";
}
}
class B {
public function getTest()
{
echo $this->test();
}
public function test()
{
return "B";
}
}
(new A())->getTest();上边代码的运行结果是会输出"A"。
我们知道,还有两种表示方式和$this很接近。$this 和 new self()没有区别。
从英文的翻译来看,static表示静止的,所以$this 和new static()的区别是,new static() 永远返回当前类。
定位器
ServiceLocator全局的定位器。它被Module继承,Module又被应用类Application继承。
整个框架中,大量用到了注册器模式。比如应用类Application把所有用到的组件都注册到定位器(ServiceLocator)的$_components属性和$_definitions属性。$_components保存所有组件的实例,$_definitions保存所有组件的基本信息,例如类名称,参数等。而整个ServiceLocator的代码非常简单,最主要的方法是get()和set()。get()方法表示获取全局注册的组件实例(即$_components属性中的值),而set()方法则是设置组件的基本信息(即$_definitions)。
源码解析
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;
}
public function set($id, $definition)
{
// 组件的注册,注册之后使用get()方法获取这个组件。
unset($this->_components[$id]);
if ($definition === null) {
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
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));
}
}这个类中的set和get方法和容器中的get和set方法其实很相似,容器中的更为复杂。当然,定位器中的方法的创建也是在get()方法中,如果$_components属性中没有,那么就根据$_definitions属性,生成新的实例。
错误和异常处理
所有的框架的错误处理机制,都在整个框架运行的顺序中排在前列,一般错误处理机制排在常量定义、配置加载、类的自动加载之后,排在其他流程逻辑之前。
base\Application
错误处理的入口是应用类的基类base\Application的构造方法中实现
public function __construct($config = [])
{
// 当application基类被继承之后,Yii::$app就变成了继承子类。
Yii::$app = $this;
// 将当前应用注册到$this->loadedModules
static::setInstance($this);
// 表示应用已开始
$this->state = self::STATE_BEGIN;
// 下面的配置数组传引用
// 初始化准备
$this->preInit($config);
// 注册错误处理
$this->registerErrorHandler($config);
// 组件初始化
Component::__construct($config);
}
public function preInit(&$config)
{
// 配置中id 和 basePath 必传, 表示应用的id和应用的部署根路径
if (!isset($config['id'])) {
throw new InvalidConfigException('The "id" configuration for the Application is required.');
}
if (isset($config['basePath'])) {
$this->setBasePath($config['basePath']);
unset($config['basePath']);
} else {
throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
}
// 设置一些路径
if (isset($config['vendorPath'])) {
$this->setVendorPath($config['vendorPath']);
unset($config['vendorPath']);
} else {
// set "@vendor"
$this->getVendorPath();
}
if (isset($config['runtimePath'])) {
$this->setRuntimePath($config['runtimePath']);
unset($config['runtimePath']);
} else {
// set "@runtime"
$this->getRuntimePath();
}
// 设置时区
if (isset($config['timeZone'])) {
$this->setTimeZone($config['timeZone']);
unset($config['timeZone']);
} elseif (!ini_get('date.timezone')) {
$this->setTimeZone('UTC');
}
// 如果存在容器的配置,就设置容器的初始属性
if (isset($config['container'])) {
$this->setContainer($config['container']);
unset($config['container']);
}
// merge core components with custom components
// 合并核心组件的初始配置
foreach ($this->coreComponents() as $id => $component) {
if (!isset($config['components'][$id])) {
$config['components'][$id] = $component;
} elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
$config['components'][$id]['class'] = $component['class'];
}
}
}我们可以看到,在错误注册之前,做了一些对$config的一些初始化的准备操作。就是将$config里面的某些配置项,赋值到当前应用的具体属性。并且,在Component没有初始化之前,$config在preInit()和registerErrorHandler()方法中是引用传值,表示$config可以在方法体中被修改。我们接下来看下错误处理的方法registerErrorHandler()
registerErrorHandler()
protected function registerErrorHandler(&$config)
{
// 注册php错误处理
// 开启了错误处理,则进行错误处理
if (YII_ENABLE_ERROR_HANDLER) {
// 配置组件中,如果没有错误处理类,那么就直接挂
// 注意,错误处理errorHandler的默认核心类在web\Application也就是子类里面,将获取核心组件配置覆盖写,并merge了。
if (!isset($config['components']['errorHandler']['class'])) {
echo "Error: no errorHandler component is configured.\n";
exit(1);
}
// 将错误处理类,注册到全局的组件树上。
$this->set('errorHandler', $config['components']['errorHandler']);
// 组件可以全局获取了。配置中没有必要存在了。避免Component初始化的时候,设置属性报错。
unset($config['components']['errorHandler']);
// 真正的错误处理机制
$this->getErrorHandler()->register();
}
}可以看到,registerErrorHandler()方法只是将错误异常处理类加载到全局组件components上。真正执行错误处理的方法再ErrorHandler.php这个类里面。这种方式很好的实现了程序的解耦。
ErrorHandler.php
public function register()
{
// 错误和异常的注册
// 单例,避免重复执行
if (!$this->_registered) {
// 关闭错误显示,设置异常处理的方法
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
// 使用 HHVM_VERSION 判断是否已经定义,存在代表是当前运行环境是虚拟机环境。分别设置错误处理方法。
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
// 如果保留了内存(用于处理内存溢出的致命错误)
if ($this->memoryReserveSize > 0) {
// 将"x"重复写$this->memoryReserveSize次,保留起来。(即保留的内存)
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
// 程序终止,处理函数
register_shutdown_function([$this, 'handleFatalError']);
$this->_registered = true;
}
}
public function handleException($exception)
{
// 异常处理的方法。使用前得先注册。通过php的异常处理机制来实现。
// 如果应用程序正常终止,则不做处理。
if ($exception instanceof ExitException) {
return;
}
$this->exception = $exception;
// 处理异常的时候,先禁用错误的捕获,避免出现递归。
$this->unregister();
// 设置http状态码
if (PHP_SAPI !== 'cli') {
http_response_code(500);
}
try {
// 异常记录日志
$this->logException($exception);
if ($this->discardExistingOutput) {
//如果开启了“丢弃页面输出”,就清理当前页面
$this->clearOutput();
}
// 异常呈现。
$this->renderException($exception);
// 测试环境,清洗内存日志并退出。
if (!YII_ENV_TEST) {
\Yii::getLogger()->flush(true);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
} catch (\Exception $e) {
$this->handleFallbackExceptionMessage($e, $exception);
} catch (\Throwable $e) {
$this->handleFallbackExceptionMessage($e, $exception);
}
// 异常处理完了,就置空。
$this->exception = null;
}
public function unregister()
{
// 恢复php的错误处理和异常处理。即取消了当前错误类的初始化(注册)。
if ($this->_registered) {
restore_error_handler();
restore_exception_handler();
$this->_registered = false;
}
}
public function handleError($code, $message, $file, $line)
{
if (error_reporting() & $code) {
// 错误类手动导入,避免自动加载发生错误的时候,导致错误处理机制无法正常运行。
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$exception = new ErrorException($message, $code, $code, $file, $line);
if (PHP_VERSION_ID < 70400) {
// 在 PHP 7.4 之前,不能在 __toString() 内部抛出异常 - 它会导致致命错误
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace);
foreach ($trace as $frame) {
if ($frame['function'] === '__toString') {
$this->handleException($exception);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}
}
throw $exception;
}
return false;
}
public function handleFatalError()
{
// 把预先占的内存释放。
unset($this->_memoryReserve);
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$error = error_get_last();
// 这块后面的其实都是正常的流程。因为致命错误,预先保留了一点内存,所以可以单独进行处理。
if (ErrorException::isFatalError($error)) {
if (!empty($this->_hhvmException)) {
$exception = $this->_hhvmException;
} else {
$exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
}
$this->exception = $exception;
$this->logException($exception);
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
// need to explicitly flush logs because exit() next will terminate the app immediately
Yii::getLogger()->flush(true);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}可以看到register()方法中,使用了函数:set_exception_handler()、set_error_handler()、register_shutdown_function(),分别注册了异常处理,错误处理,程序终止处理。
请求和响应
应用入口
首先,yii框架的应用的整个运行过程有一个生命周期,生命周期的状态是从0到6,代表从应用的开始到结束。中间不同的生命周期,会使用框架中的事件机制触发不同的生命周期方法(这里只讲整个请求和响应的流程,具体涉及到的事件后面的博文再讲)。这里把生命周期列出来,大家有个印象。
const STATE_BEGIN = 0;
const STATE_INIT = 1;
const STATE_BEFORE_REQUEST = 2;
const STATE_HANDLING_REQUEST = 3;
const STATE_AFTER_REQUEST = 4;
const STATE_SENDING_RESPONSE = 5;
const STATE_END = 6;从英文的字面意思就能看出,不同的常量表示哪一个生命周期,这里不做过多赘述。整个应用的入口是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) {
// 整个生命周期过程中,如果出现错误,调用应用结束的方法(区别直接exit,这样会完善一个应用的生命周期)。
$this->end($e->statusCode, isset($response) ? $response : null);
return $e->statusCode;
}
}请求
我们重点看"请求之中",框架的处理:
public function handleRequest($request)
{
// 处理请求,并返回响应实例。
// 如果$this->catchAll不为空,表示单个路由操作处理所有用户的所有请求。一般用在维护模式。
// 如果$this->catchAll为空,表示正常路由解析流程。
if (empty($this->catchAll)) {
try {
list($route, $params) = $request->resolve();
} catch (UrlNormalizerRedirectException $e) {
// 发生错误,url重定向。
$url = $e->url;
if (is_array($url)) {
if (isset($url[0])) {
// ensure the route is absolute
$url[0] = '/' . ltrim($url[0], '/');
}
$url += $request->getQueryParams();
}
return $this->getResponse()->redirect(Url::to($url, $e->scheme), $e->statusCode);
}
} else {
$route = $this->catchAll[0];
$params = $this->catchAll;
unset($params[0]);
}
// 现在已经成功解析了路由和参数
try {
Yii::debug("Route requested: '$route'", __METHOD__);
$this->requestedRoute = $route;
// 构造请求控制器实例,将执行控制器方法。$result即控制器方法运行结果。
// $result如果是response的实例,就直接返回。否则将其赋值到data,再返回。
$result = $this->runAction($route, $params);
if ($result instanceof Response) {
return $result;
}
$response = $this->getResponse();
if ($result !== null) {
$response->data = $result;
}
return $response;
} catch (InvalidRouteException $e) {
throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'), $e->getCode(), $e);
}
}应用"请求之中",调用了handleRequest()方法,入参是当前请求实例。通过解析请求实例$request中的请求参数(当前请求url),获取请求路由和参数,并根据路由构造控制器实例,执行路由中对应的控制器方法。最后,返回应用响应实例。
然后我们看到,在应用结束之前,执行了响应类的send方法:
$response->send();响应
我们根据send()方法,看到响应做了哪些事情:
public function send()
{
// 向客户端响应
// 已经发送就直接返回
if ($this->isSent) {
return;
}
// 响应前的事件
$this->trigger(self::EVENT_BEFORE_SEND);
// 准备工作
$this->prepare();
// 准备工作之后的事件
$this->trigger(self::EVENT_AFTER_PREPARE);
// 发送响应头(通过header函数)
$this->sendHeaders();
// 发送响应内容(通过直接echo)
$this->sendContent();
// 发送之后的事件
$this->trigger(self::EVENT_AFTER_SEND);
$this->isSent = true;
}重点分析一下响应内容的发送:
protected function sendContent()
{
// 如果不是文件流,则直接 echo 内容。
if ($this->stream === null) {
echo $this->content;
return;
}
// 设置不超时
if (function_exists('set_time_limit')) {
set_time_limit(0); // Reset time limit for big files
} else {
Yii::warning('set_time_limit() is not available', __METHOD__);
}
$chunkSize = 8 * 1024 * 1024; // 8MB per chunk
// 如果是数组,表示固定文件读取内容的开始和结尾。
if (is_array($this->stream)) {
list($handle, $begin, $end) = $this->stream;
// fseek() 函数在打开的文件中定位。
fseek($handle, $begin); // 将文件$handle指针移动到$begin位置。
// 文件没有到达末尾,并且当前位置小于设置的文件结束位置。则循环
while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
// 如果当前位置+8M大于结束位置。表示就将输出单位变为$end - $pos + 1。这个正好表示文件需要结束总大小+1。表示不需要下次循环了,并且减少了文件的扫描范围。
if ($pos + $chunkSize > $end) {
$chunkSize = $end - $pos + 1;
}
echo fread($handle, $chunkSize);
flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
}
fclose($handle);
} else {
// eof() 函数检查是否已到达文件末尾(EOF)。
// 如果没有到达文件末尾,则以8M为一个单位,输出文件内容。
while (!feof($this->stream)) {
echo fread($this->stream, $chunkSize);
flush();
}
fclose($this->stream);
}
}可以看到,响应的内容支持文件流和字符串两种方式。那么通过阅读源码,我们知道,在控制器的方法中可以这样写:
public function actionIndex()
{
$responseContent = [
"name" => "Jone",
"pass" => "123456",
];
\Yii::$app->response->content = json_encode($responseContent);
return;
}当然,如果你return了一个字符串,则会以你return的这个字符串为主。它会作为response实例的data属性,会在返回前初始化,覆盖掉原来的content属性。
属性、行为和事件类component.php
Yii2框架中的组件类component.php。这个组件类囊括了今天要讲的主题:属性、行为和事件。
首先在这里解释一下什么是属性,什么事行为,什么是事件。
属性
属性就是指类的成员变量,Yii2框架中的组件类component.php继承了对象基础类BaseObject.php。在对象基础类里面,运用php中的魔术方法__get()和__set(),实现对类属性的赋值和获取。
行为
行为和php的trait有点类似。使用行为必须继承组件类component.php(或行为类Behavior.php),它无须改变类继承关系,即可增强一个已有的组件类功能。 当行为附加到组件后,它将“注入”它的方法和属性到组件, 然后可以像访问组件内定义的方法和属性一样访问它们。
事件
事件和thinkPHP中钩子的概念差不多。事件可以将自定义代码“注入”到现有代码中的特定执行点。 附加自定义代码到某个事件,当这个事件被触发时,这些代码就会自动执行。
源码分析
首先看下对象基础类BaseObject.php的源码,然后分析它主要的功能是什么。
BaseObject.php
<?php
class BaseObject implements Configurable
{
//这是我一个对象基础类,主要用于属性的设置和获取。
//当类的属性只有获取方法(getter),没有设置方法(setter)则认为此属性只读。
//生命周期:
//构造函数 -> 对象属性初始化 -> init函数
public static function className()
{
// 获取调用者完整类名
return get_called_class();
}
public function __construct($config = [])
{
// 存在配置,设置当前类默认属性。
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
public function init()
{
}
public function __get($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) { // 存在获取器,直接获取。
return $this->$getter();
} elseif (method_exists($this, 'set' . $name)) { //不存在获取器,但是存在设置器。那么就抛出错误。
throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
}
// 不存在获取器也不存在设置器,直接属性不存在异常。(注意:是获取不存在的属性或者是没有访问权限的属性)
throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
}
public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) { // 存在设置器,就设置属性
$this->$setter($value);
} elseif (method_exists($this, 'get' . $name)) { // 如果不存在设置器,而存在获取器,则表示属性只读。抛出错误。
throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
} else {
// 属性不存在
throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}
}
public function __isset($name)
{
// 使用isset()判断对象属性是否存在时候调用。
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// 如果存在getter,则根据获取器返回值判断是否null。是null则false。否则ture;
return $this->$getter() !== null;
}
// 不存在getter直接false;
return false;
}
public function __unset($name)
{
// 使用unset()销毁对象属性时调用。
$setter = 'set' . $name;
if (method_exists($this, $setter)) { // 存在设置器,就设置为null.
$this->$setter(null);
} elseif (method_exists($this, 'get' . $name)) { // 不存在设置器,存在获取器,返回“销毁只读属性”错误。
throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name);
}
// 否则不做处理。
}
public function __call($name, $params)
{
// 调用类的未知方法,直接抛出“调用未知方法”错误。
throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
}
public function hasProperty($name, $checkVars = true)
{
// 如果不存在获取器,并且类属性不存在。那么如果存在设置器,则返回真。
return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false);
}
public function canGetProperty($name, $checkVars = true)
{
// 是否存在获取器。如果第二参数为真,就是:是否存在类属性。
return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);
// 上面的写法相当于
/*if (method_exists($this, 'get' . $name)) {
return method_exists($this, 'get' . $name);
} else {
if ($checkVars) {
return property_exists($this, $name);
} else {
return $checkVars;
}
}*/
}
public function canSetProperty($name, $checkVars = true)
{
// 是否存在设置器。如果第二参数为真,就是:是否存在类属性。
return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name);
}
public function hasMethod($name)
{
return method_exists($this, $name);
}
}首先我们看到,这个类的构造方法,先通过配置的方式,设置了继承子类的初始属性。所以继承类不需要覆盖写当前类的构造方法,只需要覆盖init()这个方法即可。
其次,继承这个类的子类,可以添加设置器方法setter和获取器方法getter来对类的属性值进行特殊处理。整体代码也比较简单。这里不做过多赘述。
component.php
首先,yii框架的行为和事件的功能设计中,都用到了注册器模式。都是先把行为或者事件注册到组件类的全局树上($_events和$_behaviors)。然后再在程序里面,在需要的地方进行相应操作。我在博客中,把行为或者事件“注入”到全局树上的这个操作称为“绑定”,把行为或者事件“取消注入”全局树的操作称为“解绑”。这里将行为和事件分开来讲吧,我们先将实现原理,再来看源码。主要代码都在组件类component.php中。
事件的实现原理
首先来讲事件,前面已经大概讲了一下事件的概念。使用事件,必须继承component.php组件类。整个事件的使用分为2~3个步骤。
- 绑定事件到全局。
- 调用trigger方法触发事件。 3.解绑事件。
事件的全局树上保存的是事件名称,以及事件的处理器(真正的执行程序)。事件的处理器可以是以下几种类型:
- 类的静态方法
- 对象的方法
- 匿名函数
- 全局函数
实现原理是这样的:
- 绑定:将事件名称为键,事件处理器作为值(也是一个数组,可能为多个)作为值,保存到全局。
- 触发:根据传入的事件名称,从全局树上获取对应的事件处理器。利用php函数call_user_func()。触发事件处理器的执行程序。
- 解绑:正常情况下,解绑是没必要做的(事件类里面定义了事件是否触发的属性)。解绑也可以在事件触发之前调用。解绑就是把事件全局树上对应的事件销毁。(也可以销毁某一事件的某一具体的处理器)
行为的实现原理
行为的概念前面也已经讲过了。这里具体讲一下使用和框架中的实现原理。
行为的使用主要是行为的绑定和解绑。绑定就是将行为类,绑定到组件中,那么这个组件就拥有了这个行为的全部方法和属性。解绑就是解除此行为对组件的绑定。
实现原理:
解绑的原理和事件是一样的。就是注销全局行为树种的一个值。这里主要讲绑定。
绑定的实现也非常简单。首先component.php继承了BaseObject.php,BaseObject.php里面针对普通类的属性的设置和获取做了优化,可以添加设置器或者获取器,来对属性做一些特殊处理。这里component.php覆盖写了父类BaseObject.php中的所有方法,进一步进行优化,__set()、__get()等魔术方法中添加了对行为的判断。例如__get()方法这样写:
public function __get($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// read property, e.g. getName()
return $this->$getter();
}
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
}
if (method_exists($this, 'set' . $name)) {
throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
}
throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
}我们可以看到,如果从当前组件中没有获取到属性的话,就会从当前组件绑定的行为中开始找,寻找行为对象中的属性,找到并返回。实现了对当前组件类的扩展。其他方法也是一样的。
component.php的源码解析:
<?php
namespace yii\base;
use Opis\Closure\ClosureStream;
use Yii;
use yii\helpers\StringHelper;
class Component extends BaseObject
{
private $_events = [];
private $_eventWildcards = [];
private $_behaviors;
public function __get($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// read property, e.g. getName()
return $this->$getter();
}
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
}
if (method_exists($this, 'set' . $name)) {
throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
}
throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
}
public function __set($name, $value)
{
// 魔术方法。
// 如果存在设置器, 就调用设置器
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
// set property
$this->$setter($value);
return;
} elseif (strncmp($name, 'on ', 3) === 0) {
// "on "开头,表示是事件事件处理器,将事件处理器绑定到对应的时间名称
// on event: attach event handler
$this->on(trim(substr($name, 3)), $value);
return;
} elseif (strncmp($name, 'as ', 3) === 0) {
// "as "开头,表示是行为。将行为附加到行为对象全局树。
// as behavior: attach behavior
$name = trim(substr($name, 3));
$this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
return;
}
// 既不存在设置器,也不是事件和行为。那么可能是行为的属性。
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = $value;
return;
}
}
// 存在获取器,表示这个属性是只读的。
if (method_exists($this, 'get' . $name)) {
throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
}
throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}
public function __isset($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
return $this->$getter() !== null;
}
// 区别于基类BaseObject的方法,新增一层行为的属性。
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name !== null;
}
}
return false;
}
public function __unset($name)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
$this->$setter(null);
return;
}
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = null;
return;
}
}
throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name);
}
public function __call($name, $params)
{
// 调用行为的方法。
$this->ensureBehaviors();
foreach ($this->_behaviors as $object) {
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}
throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
}
public function __clone()
{
// 被克隆的时候,清空克隆对象的行为和事件。
$this->_events = [];
$this->_eventWildcards = [];
$this->_behaviors = null;
}
public function hasProperty($name, $checkVars = true, $checkBehaviors = true)
{
return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors);
}
public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
{
if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) {
return true;
} elseif ($checkBehaviors) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name, $checkVars)) {
return true;
}
}
}
return false;
}
public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
{
if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) {
return true;
} elseif ($checkBehaviors) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name, $checkVars)) {
return true;
}
}
}
return false;
}
public function hasMethod($name, $checkBehaviors = true)
{
if (method_exists($this, $name)) {
return true;
} elseif ($checkBehaviors) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->hasMethod($name)) {
return true;
}
}
}
return false;
}
public function behaviors()
{
// 返回行为列表。默认都是空,如果需要添加,需要在子类覆盖
// 覆盖需注意:
// 返回的键是行为名称(也可以不设置键),返回的值是对应的扩展行为对象实例
return [];
}
public function hasEventHandlers($name)
{
// 又是他,这块是判断事件是否绑定了处理程序
$this->ensureBehaviors();
// 通配符模式,如果存在行为名称且存在事件处理,返回真。
foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (!empty($handlers) && StringHelper::matchWildcard($wildcard, $name)) {
return true;
}
}
// 如果当前存在事件存在值,就返回真。否则在事件类里面判断。
return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
}
public function on($name, $handler, $data = null, $append = true)
{
// 将事件处理程序绑定到对应事件。时间处理程序支持以下几种方式:
// 1. 匿名函数
// 2. 函数的方法
// 3. 类的静态方法
// 4. 全局函数
$this->ensureBehaviors();
// 通配符模式
if (strpos($name, '*') !== false) {
if ($append || empty($this->_eventWildcards[$name])) { // 如果追加,或者当前事件名称的处理程序为空。就把当前处理程序给它加到最后一位。
$this->_eventWildcards[$name][] = [$handler, $data];
} else { // 如果不追加,那么就将事件处理程序加在第一位,优先处理。
array_unshift($this->_eventWildcards[$name], [$handler, $data]);
}
return;
}
// 普通模式
if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
array_unshift($this->_events[$name], [$handler, $data]);
}
}
public function off($name, $handler = null)
{
// 将事件处理程序移除对应事件
$this->ensureBehaviors();
// 如果当前事件不存在,返回false
if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
return false;
}
// 如果处理器是null,直接清除当前事件所有处理器
if ($handler === null) {
unset($this->_events[$name], $this->_eventWildcards[$name]);
return true;
}
// 是否已经移除
$removed = false;
// plain event names
// 普通事件名称清除
if (isset($this->_events[$name])) { // 这个判断多余了吧?
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
$removed = true;
}
}
if ($removed) {
// 重置键
$this->_events[$name] = array_values($this->_events[$name]);
return $removed;
}
}
// wildcard event names
// 通配符事件名称处理
if (isset($this->_eventWildcards[$name])) { // 这个判断多余了吧?
foreach ($this->_eventWildcards[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_eventWildcards[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
// remove empty wildcards to save future redundant regex checks:
if (empty($this->_eventWildcards[$name])) {
unset($this->_eventWildcards[$name]);
}
}
}
return $removed;
}
public function trigger($name, Event $event = null)
{
// 触发事件
$this->ensureBehaviors();
$eventHandlers = [];
foreach ($this->_eventWildcards as $wildcard => $handlers) {
// 如果当前事件名称,和通配符匹配了。就把通配符的处理器保存起来。
if (StringHelper::matchWildcard($wildcard, $name)) {
$eventHandlers = array_merge($eventHandlers, $handlers);
}
}
// 合并通配符匹配到的处理器,与当前事件的处理器。
if (!empty($this->_events[$name])) {
$eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
}
if (!empty($eventHandlers)) {
if ($event === null) {
$event = new Event();
}
if ($event->sender === null) {
$event->sender = $this;
}
$event->handled = false;
$event->name = $name;
foreach ($eventHandlers as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled
if ($event->handled) {
return;
}
}
}
// invoke class-level attached handlers
Event::trigger($this, $name, $event);
}
public function getBehavior($name)
{
$this->ensureBehaviors();
return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
}
public function getBehaviors()
{
$this->ensureBehaviors();
return $this->_behaviors;
}
public function attachBehavior($name, $behavior)
{
$this->ensureBehaviors();
return $this->attachBehaviorInternal($name, $behavior);
}
public function attachBehaviors($behaviors)
{
$this->ensureBehaviors();
foreach ($behaviors as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
public function detachBehavior($name)
{
$this->ensureBehaviors();
if (isset($this->_behaviors[$name])) {
$behavior = $this->_behaviors[$name];
unset($this->_behaviors[$name]);
$behavior->detach();
return $behavior;
}
return null;
}
public function detachBehaviors()
{
$this->ensureBehaviors();
foreach ($this->_behaviors as $name => $behavior) {
$this->detachBehavior($name);
}
}
public function ensureBehaviors()
{
// 确保行为已经存在当前类的$this->_behaviors属性
// 只要是涉及到行为的操作的方法,都会调用一遍这个方法。这个其实可以在构造方法里面,只调用一次就可以了。不知道作者为啥要这样写。。。
if ($this->_behaviors === null) {
$this->_behaviors = [];
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
}
private function attachBehaviorInternal($name, $behavior)
{
// 不属于行为类的对象,则生成一个实例($behavior就是需要生成行为实例的参数)
if (!($behavior instanceof Behavior)) {
$behavior = Yii::createObject($behavior);
}
// 如果整数,直接附加到行为列表(匿名)
if (is_int($name)) {
$behavior->attach($this);
$this->_behaviors[] = $behavior;
} else {
// 如果是字符串,判断存在的话,先解绑,然后再绑定。
if (isset($this->_behaviors[$name])) {
$this->_behaviors[$name]->detach();
}
$behavior->attach($this);
$this->_behaviors[$name] = $behavior;
}
return $behavior;
}
}