/ yii2  

Yii2 源码阅读 02 - Configurable BaseObject Component

书接上文 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\base\Configurable

是一个 interface。实现此接口意味着:这些类支持通过其 __constructor 的最后一个数组参数,设置其 properties。例如:

public function __constructor($param1, $param2, ..., $config = [])

通过 Yii::configure($this, $config); 实现:

public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}

return $object;
}

该接口没有声明任何一个方法。主要给 yii\di\Container 使用,以便它可以将 对象配置 作为最后一个参数传递给实现类的构造函数,后文再研究。

参见:配置(Configurations)

yii\base\BaseObject

是个基类;实现了 property初始化生命周期 相关功能。

几乎每个 Yii 框架的核心类都继承自 yii\base\BaseObject 或其子类。

property 属性

通过重写 __get __set 实现属性由 getter/setter 定义。

private $_label;

public function getLabel()
{
return $this->_label;
}

public function setLabel($value)
{
$this->_label = trim($value);
}

getter/setter 定义的 属性 与 类成员变量 区别是:当这种属性被读取时,对应的 getter 方法将被调用;而当属性被赋值时,对应的 setter 方法就调用。如:

// equivalent to $label = $object->getLabel();
$label = $object->label;

// equivalent to $object->setLabel('abc');
$object->label = 'abc';

这里有个技巧,在类上可以通过注释帮助 IDE 提示属性:

/**
* @property-read Behavior[] $behaviors ..
*/
class Component extends BaseObject
{
}

如果一个属性只有一个 getter 方法而没有 setter 方法,那么它就被认为是 只读的。在这种情况下,试图修改属性值将导致 InvalidCallException

可以调用 hasProperty()canGetProperty()canSetProperty() 来检查属性是否存在。依托的方法 method_existsproperty_exists

通过 getter/setter 定义的 属性 也有一些特殊规则和限制:

  • 这类属性的名字是 不区分大小写 case-insensitive 的。如,$object->label$object->Label 是同一个属性。 因为 PHP 方法名是不区分大小写的。(所以内部函数是下划线命名法?)
  • 如果这类属性的名字和类成员变量相同,以后者为准。例如,假设以上 Foo 类有个 label 成员变量,然后给 $object->label = 'abc' 赋值,将赋给成员变量而不是 setter setLabel() 方法。这是因为 __set 是在没有此成员变量是触发。
  • 这些属性不支持可见性。如果属性是公共的、受保护的或私有的,它与定义的 getter/setter 方法没有区别。
  • 这类属性只能由非静态 getter/setter 定义,静态方法不会以相同的方式处理。
  • 对 property_exists() 的调用,无法判断魔法属性。应该调用 hasProperty()。

🤔 思考:这里增强了类的属性的操作,是否不利于面向对象编程与理解,因为看起来方便的直接的属性操作是否破坏了封装性?

详细参见:属性(Properties)

initialization life cycle

实现了对象初始化的生命周期 initialization life cycle

  1. __construct 被调用。
  2. 对象属性将根据 Configurable 被初始化。
  3. init() 被调用。

2、3 发生在构造函数的尾部,推荐在 init() 再进行相关初始化操作,因为此时对象属性已经完成了设置。

为保证以上生命周期,BaseObject 的子类应该像如下方式重新构造方法:

public function __construct($param1, $param2, ..., $config = [])
{
...
parent::__construct($config);
}

yii\base\Component

参见:组件(Components)

是个基类;除了在父类 BaseObject 中实现的 property 特性外,还提供了 eventbehavior 特性。

  • private $_events the attached event handlers (event name => handlers)
  • private $_eventWildcards (event name wildcard => handlers)
  • private $_behaviors the attached behaviors (behavior name => behavior). This is null when not initialized.

Events 事件

参见:事件(Events)

Event is a way to “inject” custom code into existing code at certain places.

事件称标识在定义它的类中应该是唯一的。事件名称 区分大小写 case-sensitive

调用 on() 附加到一个事件。一个或多个 PHP 回调函数,称为 event handlers 事件处理器

/**
* Attaches an event handler to an event.
*
* The event handler must be a valid PHP callback.
*
* function ($event) { ... } // anonymous function
* [$object, 'handleClick'] // $object->handleClick()
* ['Page', 'handleClick'] // Page::handleClick()
* 'handleClick' // global function handleClick()
*
* 事件处理程序必须使用以下签名定义,
*
* function ($event)
*
* Since 2.0.14 you can specify event name as a wildcard pattern:
*
* $component->on('event.group.*', function ($event) {
* Yii::trace($event->name . ' is triggered.');
* });
*
* @param string $name the event name
* @param callable $handler the event handler
* @param mixed $data 事件触发时要传递给事件处理程序的数据。
* 当事件处理器被调用时,可以通过 event::data 访问该数据。
* @param bool $append 是否将 新的事件处理程序 追加到现有处理程序列表的 末尾。
* 如果为 false,新的处理程序 将插入到现有处理程序列表的开头。
* @see off()
*/
public function on($name, $handler, $data = null, $append = true)
{
// 确保在 behaviors() 中声明的行为被附加到这个组件。
$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])) {
// append = true 插入尾部
$this->_events[$name][] = [$handler, $data];
} else {
// 插入头部
array_unshift($this->_events[$name], [$handler, $data]);
}
}

调用 trigger() 来 raise 一个事件:

/**
* Triggers an event.
* 它调用事件的所有附加处理程序,包括 class-level 处理程序。
*
* @param string $name the event name
* @param Event $event the event parameter. 如果不设置,将创建一个默认的 Event 对象。
*/
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) {
// $handler is [callable $handler, $data]
$event->data = $handler[1];
// 调用
call_user_func($handler[0], $event);
// 如果事件 handled = true,停止进一步的处理
if ($event->handled) {
return;
}
}
}

// 调用 class-level attached 处理程序
// static $_eventWildcards and $_events
// 待研究
Event::trigger($this, $name, $event);
}

推荐使用类常量来表示事件名:

  1. 它可以防止拼写错误并支持 IDE 的自动完成。
  2. 只要简单检查常量声明就能了解一个类支持哪些事件。

类级别的事件处理器:

/**
* Attaches an event handler to a class-level event.
*
* When a class-level event is triggered, event handlers attached
* to that class and all parent classes will be invoked.
*
* @param string $class the fully qualified class name to which the event handler needs to attach
* @param string $name the event name
* @param callable $handler the event handler
* @param mixed $data 事件触发时要传递给事件处理程序的数据。
* When the event handler is invoked, this data can be accessed via [[Event::data]].
* @param bool $append whether to append new event handler to the end of the existing
* handler list. If `false`, the new handler will be inserted at the beginning of the existing handler list.
*
* @see off()
*/
public static function on($class, $name, $handler, $data = null, $append = true) {
...
if ($append || empty(self::$_events[$name][$class])) {
self::$_events[$name][$class][] = [$handler, $data];
} else {
array_unshift(self::$_events[$name][$class], [$handler, $data]);
}
}

例子:

Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
Yii::trace(get_class($event->sender) . ' is inserted.');
});

// Since 2.0.14 you can specify either class name or event name as a wildcard pattern:

Event::on('app\models\db\*', '*Insert', function ($event) {
Yii::trace(get_class($event->sender) . ' is inserted.');
});

每当 ActiveRecord 或其子类的实例触发 EVENT_AFTER_INSERT 事件时, 这个事件处理器都会执行。在这个处理器中,可以通过 $event->sender 获取触发事件的对象。

You can also attach a handler to an event when configuring a component with a configuration array.
The syntax is like the following:

[
'on add' => function ($event) { ... }
]

where on add stands for attaching an event to the add event.

其他官方文档:

Behaviors 行为

参见:行为(Behaviors)

行为是 yii\base\Behavior 或其子类的实例。行为,也称为 mixins,可以无须改变类继承关系即可增强一个已有的 Component 类功能。

它可以将自己的方法和属性 注入 到组件中,并通过组件直接访问它们。它还可以响应组件中触发的事件,从而拦截正常的代码执行。

定义行为,yii\base\Behavior

  • public $owner Component|null,行为的所有者。
  • private $_attachedEvents Attached events handlers。

yii\base\Behavior 为 owner 的 events 声明事件处理器:

/**
* 为 owner 的 events 声明事件处理器。
*
* 子类可以重写这个方法 来声明哪些 PHP 回调函数应该附加到 owner 组件的事件上。
*
* The callbacks can be any of the following:
*
* - method in this behavior: `'handleClick'`, equivalent to `[$this, 'handleClick']`
* - object method: `[$object, 'handleClick']`
* - static method: `['Page', 'handleClick']`
* - anonymous function: `function ($event) { ... }`
*
* The following is an example:
*
* [
* Model::EVENT_BEFORE_VALIDATE => 'myBeforeValidate',
* Model::EVENT_AFTER_VALIDATE => 'myAfterValidate',
* ]
*
* @return array 事件名 (array keys) and 相应的事件处理器方法 (array values).
*/
public function events()
{
return [];
}

yii\base\Component 访问行为中的 属性 的逻辑:

public function __get($name)
{
...
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
}
...
}

yii\base\Component 访问行为中的 方法 的逻辑:

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()");
}

yii\base\Component 附加行为:

/**
* Attaches a behavior to this component.
* This method will create the behavior object based on the given
* configuration. After that, the behavior object will be attached to
* this component by calling the [[Behavior::attach()]] method.
*
* @param string $name the name of the behavior
* @param string|array|Behavior $behavior the behavior configuration.
* This can be one of the following:
*
* - a [[Behavior]] object
* - a string specifying the behavior class
* - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object.
*
* @return Behavior the behavior object
*
* @see detachBehavior()
*/
public function attachBehavior($name, $behavior)
{
$this->ensureBehaviors();

return $this->attachBehaviorInternal($name, $behavior);
}

/**
* Makes sure that the behaviors declared in [[behaviors()]] are attached to this component.
*/
public function ensureBehaviors()
{
// 如属性的注释,This is `null` when not initialized.
if ($this->_behaviors === null) {
// 未初始化
$this->_behaviors = [];
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
}

/**
* Attaches a behavior to this component.
* @param string|int $name the name of the behavior. 如果这是一个整数,这意味着该行为是匿名的。
* 否则,该行为是命名行为,任何具有相同名称的现有行为将首先分离。
* @param string|array|Behavior $behavior the behavior to be attached
* @return Behavior the attached behavior.
*/
private function attachBehaviorInternal($name, $behavior)
{
if (!($behavior instanceof Behavior)) {
// 当是 string or array 时
$behavior = Yii::createObject($behavior);
}
if (is_int($name)) {
// name 是数字
// 1. 绑定 behavior 的 owner 为 $this
// 2. 在 events 里 声明附加事件处理器
$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;
}

yii\behaviors 层级结构:

Behavior
|---- AttributeBehavior
|---- BlameableBehavior 自动使用当前用户 ID 填充指定的属性。
|---- OptimisticLockBehavior 乐观锁 使用列名自动升级模型的锁版本。
|---- SluggableBehavior 自动用一个值填充指定的属性,该值可以在URL中使用。
|---- TimestampBehavior 自动用当前时间戳填充指定的属性。

|---- AttributesBehavior
|---- AttributeTypecastBehavior 提供自动模型属性类型转换的能力。
|---- CacheableWidgetBehavior 可缓存小部件行为根据指定的持续时间和依赖关系自动缓存小部件内容。

yii\behaviors\AttributeBehavior 当特定事件发生时,自动将指定的值赋给 ActiveRecord 对象的一个或多个属性。

  • public $attributes = []; 要用 value 指定的值自动填充的属性列表。
  • public $value; 将分配给当前属性的值。这可以是匿名函数。
  • public $skipUpdateOnClean = true; 是否在 $owner 未被修改时跳过此行为。
  • public $preserveNonEmptyValues = false; 是否保留原来非空属性值。
/**
* Evaluates 属性值并将其分配给当前属性。
* @param Event $event
*/
public function evaluateAttributes($event)
{
if ($this->skipUpdateOnClean
&& $event->name == ActiveRecord::EVENT_BEFORE_UPDATE
// 返回自最近加载或保存以来已被修改的属性值 如果没有修改返回 []
&& empty($this->owner->dirtyAttributes)
) {
return;
}

// $event->name eg ActiveRecord::EVENT_BEFORE_INSERT
if (!empty($this->attributes[$event->name])) {
// str -> [str]
$attributes = (array) $this->attributes[$event->name];
$value = $this->getValue($event);
foreach ($attributes as $attribute) {
// ignore attribute names which are not string (e.g. when set by TimestampBehavior::updatedAtAttribute)
// 确保 attribute 是字符串
if (is_string($attribute)) {
if ($this->preserveNonEmptyValues && !empty($this->owner->$attribute)) {
// 保留原来非空属性值
continue;
}
$this->owner->$attribute = $value;
}
}
}
}

AttributeBehavior 使用例子:

public function behaviors()
{
return [
[
'class' => AttributeBehavior::class,
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => 'attribute1',
ActiveRecord::EVENT_BEFORE_UPDATE => 'attribute2',
],
'value' => function ($event) {
return 'some value';
},
],
];
}

yii\behaviors\OptimisticLockBehavior 乐观锁 使用列名自动升级模型的锁版本。

乐观锁定允许多个用户访问同一记录进行编辑,从而避免潜在的冲突。如果用户试图在一些过期数据上保存记录(因为另一个用户修改了数据),则会抛出 StaleObjectException 异常,并跳过更新或删除操作。

– EOF –