组件(Component):
组件是 Yii 应用的主要基石。是 [[yiiaseComponent]] 类或其子类的实例。三个用以区分它和其它类的主要功能有:
- 属性(Property)
- 事件(Event)
- 行为(Behavior)
或单独使用,或彼此配合,这些功能的应用让 Yii 的类变得更加灵活和易用。以小部件 [[yiijuiDatePicker|日期选择器]] 来举例,这是个方便你在 视图 中生成一个交互式日期选择器的 UI 组件:
- use yiijuiDatePicker;
-
- echo DatePicker::widget([
- 'language' => 'zh-CN',
- 'name' => 'country',
- 'clientOptions' => [
- 'dateFormat' => 'yy-mm-dd',
- ],
- ]);
这个小部件继承自 [[yiiaseComponent]],它的各项属性改写起来会很容易。
正是因为组件功能的强大,他们比常规的对象(Object)稍微重量级一点,因为他们要使用额外的内存和 CPU 时间来处理 事件 和 行为 。如果你不需要这两项功能,可以继承 [[yiiaseObject]] 而不是 [[yiiaseComponent]]。这样组件可以像普通 PHP 对象一样高效,同时还支持属性(Property)功能。
当继承 [[yiiaseComponent]] 或 [[yiiaseObject]] 时,推荐你使用如下的编码风格:
- 若你需要重写构造方法(Constructor),传入 $config 作为构造器方法最后一个参数,然后把它传递给父类的构造方法。
- 永远在你重写的构造方法结尾处调用一下父类的构造方法。
- 如果你重写了 [[yiiaseObject::init()]] 方法,请确保你在 init 方法的开头处调用了父类的 init 方法。
例子如下:
- namespace yiicomponentsMyClass;
-
- use yiiaseObject;
-
- class MyClass extends Object
- {
- public $prop1;
- public $prop2;
-
- public function __construct($param1, $param2, $config = [])
- {
- // ... 配置生效前的初始化过程
-
- parent::__construct($config);
- }
-
- public function init()
- {
- parent::init();
-
- // ... 配置生效后的初始化过程
- }
- }
另外,为了让组件可以在创建实例时能被正确配置,请遵照以下操作流程:
- $component = new MyClass(1, 2, ['prop1' => 3, 'prop2' => 4]);
- // 方法二:
- $component = Yii::createObject([
- 'class' => MyClass::className(),
- 'prop1' => 3,
- 'prop2' => 4,
- ], [1, 2]);
补充:尽管调用 [[Yii::createObject()]] 的方法看起来更加复杂,但这主要因为它更加灵活强大,它是基于依赖注入容器实现的。
[[yiiaseObject]] 类执行时的生命周期如下:
- 构造方法内的预初始化过程。你可以在这儿给各属性设置缺省值。
- 通过 $config 配置对象。配置的过程可能会覆盖掉先前在构造方法内设置的默认值。
- 在 [[yiiaseObject::init()|init()]] 方法内进行初始化后的收尾工作。你可以通过重写此方法,进行一些良品检验,属性的初始化之类的工作。
- 对象方法调用。
前三步都是在对象的构造方法内发生的。这意味着一旦你获得了一个对象实例,那么它就已经初始化就绪可供使用。
属性(Property):
在 PHP 中,类的成员变量也被称为属性(properties)。它们是类定义的一部分,用来表现一个实例的状态(也就是区分类的不同实例)。在具体实践中,常常会想用一个稍微特殊些的方法实现属性的读写。例如,要对label
属性执行 trim 操作,可以用以下代码实现:
- $object->label = trim($label);
上述代码的缺点是只要修改 label
属性就必须再次调用 trim()
函数。若将来需要用其它方式处理 label
属性,比如首字母大写,就不得不修改所有给 label
属性赋值的代码。这种代码的重复会导致 bug,这种实践显然需要尽可能避免。
为解决该问题,Yii 引入了一个名为 [[yii\base\Object]] 的基类,它支持基于类内的 getter 和 setter(读取器和设定器)方法来定义属性。如果某类需要支持这个特性,只需要继承 [[yii\base\Object]] 或其子类即可。
补充:几乎每个 Yii 框架的核心类都继承自 [[yii\base\Object]] 或其子类。这意味着只要在核心类中见到 getter 或 setter 方法,就可以像调用属性一样调用它。
getter 方法是名称以 get
开头的方法,而 setter 方法名以 set
开头。方法名中 get
或 set
后面的部分就定义了该属性的名字。如下面代码所示,getter 方法 getLabel()
和 setter 方法 setLabel()
操作的是 label
属性,:
- namespace app\\components;
-
- use yii\\base\\Object;
-
- class Foo extend Object
- {
- private $_label;
-
- public function getLabel()
- {
- return $this->_label;
- }
-
- public function setLabel($value)
- {
- $this->_label = trim($value);
- }
- }
(详细解释:getter 和 setter 方法创建了一个名为 label
的属性,在这个例子里,它指向一个私有的内部属性_label
。)
getter/setter 定义的属性用法与类成员变量一样。两者主要的区别是:当这种属性被读取时,对应的 getter 方法将被调用;而当属性被赋值时,对应的 setter 方法就调用。如:
- // 等效于 $label = $object->getLabel();
- $label = $object->label;
-
- // 等效于 $object->setLabel('abc');
- $object->label = 'abc';
只定义了 getter 没有 setter 的属性是只读属性。尝试赋值给这样的属性将导致 [[yii\base\InvalidCallException|InvalidCallException]] (无效调用)异常。类似的,只有 setter 方法而没有 getter 方法定义的属性是只写属性,尝试读取这种属性也会触发异常。使用只写属性的情况几乎没有。
通过 getter 和 setter 定义的属性也有一些特殊规则和限制:
- 这类属性的名字是不区分大小写的。如,$object->label 和 $object->Label 是同一个属性。因为 PHP 方法名是不区分大小写的。
- 如果此类属性名和类成员变量相同,以后者为准。例如,假设以上 Foo 类有个 label 成员变量,然后给$object->label = 'abc' 赋值,将赋给成员变量而不是 setter setLabel() 方法。
- 这类属性不支持可见性(访问限制)。定义属性的 getter 和 setter 方法是 public、protected 还是 private 对属性的可见性没有任何影响。
- 这类属性的 getter 和 setter 方法只能定义为非静态的,若定义为静态方法(static)则不会以相同方式处理。
回到开头提到的问题,与其处处要调用 trim()
函数,现在我们只需在 setter setLabel()
方法内调用一次。如果 label 首字母变成大写的新要求来了,我们只需要修改setLabel()
方法,而无须接触任何其它代码。
事件:
事件可以将自定义代码“注入”到现有代码中的特定执行点。附加自定义代码到某个事件,当这个事件被触发时,这些代码就会自动执行。例如,邮件程序对象成功发出消息时可触发 messageSent
事件。如想追踪成功发送的消息,可以附加相应追踪代码到 messageSent
事件。
Yii 引入了名为 [[yii\base\Component]] 的基类以支持事件。如果一个类需要触发事件就应该继承 [[yii\base\Component]] 或其子类。
事件处理器(Event Handlers)
事件处理器是一个PHP 回调函数,当它所附加到的事件被触发时它就会执行。可以使用以下回调函数之一:
- 字符串形式指定的 PHP 全局函数,如 'trim' ;
- 对象名和方法名数组形式指定的对象方法,如 [$object, $method] ;
- 类名和方法名数组形式指定的静态类方法,如 [$class, $method] ;
- 匿名函数,如 function ($event) { ... } 。
事件处理器的格式是:
- function ($event) {
- // $event 是 yii\\base\\Event 或其子类的对象
- }
通过 $event
参数,事件处理器就获得了以下有关事件的信息:
- [[yii\base\Event::name|event name]]:事件名
- [[yii\base\Event::sender|event sender]]:调用 trigger() 方法的对象
- [[yii\base\Event::data|custom data]]:附加事件处理器时传入的数据,默认为空,后文详述
附加事件处理器
调用 [[yii\base\Component::on()]] 方法来附加处理器到事件上。如:
- $foo = new Foo;
-
- // 处理器是全局函数
- $foo->on(Foo::EVENT_HELLO, 'function_name');
-
- // 处理器是对象方法
- $foo->on(Foo::EVENT_HELLO, [$object, 'methodName']);
-
- // 处理器是静态类方法
- $foo->on(Foo::EVENT_HELLO, ['app\\components\\Bar', 'methodName']);
-
- // 处理器是匿名函数
- $foo->on(Foo::EVENT_HELLO, function ($event) {
- //事件处理逻辑
- });
附加事件处理器时可以提供额外数据作为 [[yii\base\Component::on()]] 方法的第三个参数。数据在事件被触发和处理器被调用时能被处理器使用。如:
- // 当事件被触发时以下代码显示 "abc"
- // 因为 $event->data 包括被传递到 "on" 方法的数据
- $foo->on(Foo::EVENT_HELLO, function ($event) {
- echo $event->data;
- }, 'abc');
时间处理器顺序
可以附加一个或多个处理器到一个事件。当事件被触发,已附加的处理器将按附加次序依次调用。如果某个处理器需要停止其后的处理器调用,可以设置 $event
参数的 [yii\base\Event::handled]] 属性为真,如下:
- $foo->on(Foo::EVENT_HELLO, function ($event) {
- $event->handled = true;
- });
默认新附加的事件处理器排在已存在处理器队列的最后。因此,这个处理器将在事件被触发时最后一个调用。在处理器队列最前面插入新处理器将使该处理器最先调用,可以传递第四个参数 $append
为假并调用 [[yii\base\Component::on()]] 方法实现:
``php $foo->on(Foo::EVENT_HELLO, function ($event) { // 这个处理器将被插入到处理器队列的第一位... }, $data, false);
触发事件
----------
事件通过调用 [[yii\base\Component::trigger()]] 方法触发,此方法须传递**事件名**,还可以传递一个事件对象,用来传递参数到事件处理器。如:
```php
namespace app\components;
use yii\base\Component;
use yii\base\Event;
class Foo extends Component
{
const EVENT_HELLO = 'hello';
public function bar()
{
$this->trigger(self::EVENT_HELLO);
}
}
以上代码当调用 bar()
,它将触发名为 hello
的事件。
提示:推荐使用类常量来表示事件名。上例中,常量 EVENT_HELLO
用来表示 hello
。这有两个好处。第一,它可以防止拼写错误并支持 IDE 的自动完成。第二,只要简单检查常量声明就能了解一个类支持哪些事件。
有时想要在触发事件时同时传递一些额外信息到事件处理器。例如,邮件程序要传递消息信息到 messageSent
事件的处理器以便处理器了解哪些消息被发送了。为此,可以提供一个事件对象作为 [[yii\base\Component::trigger()]] 方法的第二个参数。这个事件对象必须是 [[yii\base\Event]] 类或其子类的实例。如:
- namespace app\\components;
-
- use yii\\base\\Component;
- use yii\\base\\Event;
-
- class MessageEvent extends Event
- {
- public $message;
- }
-
- class Mailer extends Component
- {
- const EVENT_MESSAGE_SENT = 'messageSent';
-
- public function send($message)
- {
- // ...发送 $message 的逻辑...
-
- $event = new MessageEvent;
- $event->message = $message;
- $this->trigger(self::EVENT_MESSAGE_SENT, $event);
- }
- }
当 [[yii\base\Component::trigger()]] 方法被调用时,它将调用所有附加到命名事件(trigger 方法第一个参数)的事件处理器。
移除事件处理器
从事件移除处理器,调用 [[yii\base\Component::off()]] 方法。如:
- // 处理器是全局函数
- $foo->off(Foo::EVENT_HELLO, 'function_name');
-
- // 处理器是对象方法
- $foo->off(Foo::EVENT_HELLO, [$object, 'methodName']);
-
- // 处理器是静态类方法
- $foo->off(Foo::EVENT_HELLO, ['app\\components\\Bar', 'methodName']);
-
- // 处理器是匿名函数
- $foo->off(Foo::EVENT_HELLO, $anonymousFunction);
注意当匿名函数附加到事件后一般不要尝试移除匿名函数,除非你在某处存储了它。以上示例中,假设匿名函数存储为变量 $anonymousFunction
。
移除事件的全部处理器,简单调用 [[yii\base\Component::off()]] 即可,不需要第二个参数:
- $foo->off(Foo::EVENT_HELLO);
类级别的事件处理器
以上部分,我们叙述了在实例级别如何附加处理器到事件。有时想要一个类的所有实例而不是一个指定的实例都响应一个被触发的事件,并不是一个个附加事件处理器到每个实例,而是通过调用静态方法 [[yii\base\Event::on()]] 在类级别附加处理器。
例如,活动记录对象要在每次往数据库新增一条新记录时触发一个 [[yii\base\ActiveRecord::EVENT_AFTER_INSERT]] 事件。要追踪每个活动记录对象的新增记录完成情况,应如下写代码:
- use Yii;
- use yii\\base\\Event;
- use yii\\db\\ActiveRecord;
-
- Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
- Yii::trace(get_class($event->sender) . ' is inserted');
- });
每当 [[yii\base\ActiveRecord|ActiveRecord]] 或其子类的实例触发 [[yii\base\ActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] 事件时,这个事件处理器都会执行。在这个处理器中,可以通过 $event->sender
获取触发事件的对象。
当对象触发事件时,它首先调用实例级别的处理器,然后才会调用类级别处理器。
可调用静态方法[[yii\base\Event::trigger()]]来触发一个类级别事件。类级别事件不与特定对象相关联。因此,它只会引起类级别事件处理器的调用。如:
- use yii\\base\\Event;
-
- Event::on(Foo::className(), Foo::EVENT_HELLO, function ($event) {
- echo $event->sender; // 显示 "app\\models\\Foo"
- });
-
- Event::trigger(Foo::className(), Foo::EVENT_HELLO);
注意这种情况下 $event->sender
指向触发事件的类名而不是对象实例。
注意:因为类级别的处理器响应类和其子类的所有实例触发的事件,必须谨慎使用,尤其是底层的基类,如 [[yii\base\Object]]。
移除类级别的事件处理器只需调用[[yii\base\Event::off()]],如:
- // 移除 $handler
- Event::off(Foo::className(), Foo::EVENT_HELLO, $handler);
-
- // 移除 Foo::EVENT_HELLO 事件的全部处理器
- Event::off(Foo::className(), Foo::EVENT_HELLO);
全局事件
所谓全局事件实际上是一个基于以上叙述的事件机制的戏法。它需要一个全局可访问的单例,如应用实例。
事件触发者不调用其自身的 trigger()
方法,而是调用单例的 trigger()
方法来触发全局事件。类似地,事件处理器被附加到单例的事件。如:
- use Yii;
- use yii\\base\\Event;
- use app\\components\\Foo;
-
- Yii::$app->on('bar', function ($event) {
- echo get_class($event->sender); // 显示 "app\\components\\Foo"
- });
-
- Yii::$app->trigger('bar', new Event(['sender' => new Foo]));
全局事件的一个好处是当附加处理器到一个对象要触发的事件时,不需要产生该对象。相反,处理器附加和事件触发都通过单例(如应用实例)完成。
然而,因为全局事件的命名空间由各方共享,应合理命名全局事件,如引入一些命名空间(例:"frontend.mail.sent", "backend.mail.sent")。
行为:
行为是 [[yii\base\Behavior]] 或其子类的实例。行为,也称为 mixins,可以无须改变类继承关系即可增强一个已有的 [[yii\base\Component|组件]] 类功能。当行为附加到组件后,它将“注入”它的方法和属性到组件,然后可以像访问组件内定义的方法和属性一样访问它们。此外,行为通过组件能响应被触发的事件,从而自定义或调整组件正常执行的代码。
定义行为
要定义行为,通过继承 [[yii\base\Behavior]] 或其子类来建立一个类。如:
- namespace app\\components;
-
- use yii\\base\\Model;
- use yii\\base\\Behavior;
-
- class MyBehavior extends Behavior
- {
- public $prop1;
-
- private $_prop2;
-
- public function getProp2()
- {
- return $this->_prop2;
- }
-
- public function setProp2($value)
- {
- $this->_prop2 = $value;
- }
-
- public function foo()
- {
- // ...
- }
- }
以上代码定义了行为类 app\components\MyBehavior
并为要附加行为的组件提供了两个属性 prop1
、prop2
和一个方法 foo()
。注意属性 prop2
是通过 getter getProp2()
和 setter setProp2()
定义的。能这样用是因为 [[yii\base\Object]] 是 [[yii\base\Behavior]] 的祖先类,此祖先类支持用 getter 和 setter 方法定义属性
提示:在行为内部可以通过 [[yii\base\Behavior::owner]] 属性访问行为已附加的组件。
处理事件
如果要让行为响应对应组件的事件触发,就应覆写 [[yii\base\Behavior::events()]] 方法,如:
- namespace app\\components;
-
- use yii\\db\\ActiveRecord;
- use yii\\base\\Behavior;
-
- class MyBehavior extends Behavior
- {
- // 其它代码
-
- public function events()
- {
- return [
- ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
- ];
- }
-
- public function beforeValidate($event)
- {
- // 处理器方法逻辑
- }
- }
[[yii\base\Behavior::events()|events()]] 方法返回事件列表和相应的处理器。上例声明了 [[yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]] 事件和它的处理器beforeValidate()
。当指定一个事件处理器时,要使用以下格式之一:
- 指向行为类的方法名的字符串,如上例所示;
- 对象或类名和方法名的数组,如 [$object, 'methodName'];
- 匿名方法。
处理器的格式如下,其中 $event 指向事件参数。关于事件的更多细节请参考事件:
- function ($event) {
- }
附加行为
可以静态或动态地附加行为到[[yii\base\Component|组件]]。前者在实践中更常见。
要静态附加行为,覆写行为要附加的组件类的 [[yii\base\Component::behaviors()|behaviors()]] 方法即可。[[yii\base\Component::behaviors()|behaviors()]] 方法应该返回行为配置列表。每个行为配置可以是行为类名也可以是配置数组。如:
- namespace app\\models;
-
- use yii\\db\\ActiveRecord;
- use app\\components\\MyBehavior;
-
- class User extends ActiveRecord
- {
- public function behaviors()
- {
- return [
- // 匿名行为,只有行为类名
- MyBehavior::className(),
-
- // 命名行为,只有行为类名
- 'myBehavior2' => MyBehavior::className(),
-
- // 匿名行为,配置数组
- [
- 'class' => MyBehavior::className(),
- 'prop1' => 'value1',
- 'prop2' => 'value2',
- ],
-
- // 命名行为,配置数组
- 'myBehavior4' => [
- 'class' => MyBehavior::className(),
- 'prop1' => 'value1',
- 'prop2' => 'value2',
- ]
- ];
- }
- }
通过指定行为配置数组相应的键可以给行为关联一个名称。这种行为称为命名行为。上例中,有两个命名行为:myBehavior2
和 myBehavior4
。如果行为没有指定名称就是匿名行为。
要动态附加行为,在对应组件里调用 [[yii\base\Component::attachBehavior()]] 方法即可,如:
- use app\\components\\MyBehavior;
-
- // 附加行为对象
- $component->attachBehavior('myBehavior1', new MyBehavior);
-
- // 附加行为类
- $component->attachBehavior('myBehavior2', MyBehavior::className());
-
- // 附加配置数组
- $component->attachBehavior('myBehavior3', [
- 'class' => MyBehavior::className(),
- 'prop1' => 'value1',
- 'prop2' => 'value2',
- ]);
可以通过 [[yii\base\Component::attachBehaviors()]] 方法一次附加多个行为:
- $component->attachBehaviors([
- 'myBehavior1' => new MyBehavior, // 命名行为
- MyBehavior::className(), // 匿名行为
- ]);
还可以通过配置去附加行为:
- [
- 'as myBehavior2' => MyBehavior::className(),
-
- 'as myBehavior3' => [
- 'class' => MyBehavior::className(),
- 'prop1' => 'value1',
- 'prop2' => 'value2',
- ],
- ]
详情请参考配置章节。
使用行为
使用行为,必须像前文描述的一样先把它附加到 [[yii\base\Component|component]] 类或其子类。一旦行为附加到组件,就可以直接使用它。
行为附加到组件后,可以通过组件访问一个行为的公共成员变量或 getter 和 setter 方法定义的属性:
- // "prop1" 是定义在行为类的属性
- echo $component->prop1;
- $component->prop1 = $value;
类似地也可以调用行为的*公共方法:
- // bar() 是定义在行为类的公共方法
- $component->bar();
如你所见,尽管 $component
未定义 prop1
和 bar()
,它们用起来也像组件自己定义的一样。
如果两个行为都定义了一样的属性或方法,并且它们都附加到同一个组件,那么首先附加上的行为在属性或方法被访问时有优先权。
附加行为到组件时的命名行为,可以使用这个名称来访问行为对象,如下所示:
- $behavior = $component->getBehavior('myBehavior');
也能获取附加到这个组件的所有行为:
- $behaviors = $component->getBehaviors();
移除行为
要移除行为,可以调用 [[yii\base\Component::detachBehavior()]] 方法用行为相关联的名字实现:
- $component->detachBehavior('myBehavior1');
也可以移除全部行为:
- $component->detachBehaviors();
使用 TimestampBehavior
最后以 [[yii\behaviors\TimestampBehavior]] 的讲解来结尾,这个行为支持在 [[yii\db\ActiveRecord|Active Record]] 存储时自动更新它的时间戳属性。
首先,附加这个行为到计划使用该行为的 [[yii\db\ActiveRecord|Active Record]] 类:
- namespace app\\models\\User;
-
- use yii\\db\\ActiveRecord;
- use yii\\behaviors\\TimestampBehavior;
-
- class User extends ActiveRecord
- {
- // ...
-
- public function behaviors()
- {
- return [
- [
- 'class' => TimestampBehavior::className(),
- 'attributes' => [
- ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
- ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
- ],
- ],
- ];
- }
- }
以上指定的行为数组:
- 当记录插入时,行为将当前时间戳赋值给 created_at 和 updated_at 属性;
- 当记录更新时,行为将当前时间戳赋值给 updated_at 属性。
保存 User 对象,将会发现它的 created_at 和 updated_at 属性自动填充了当前时间戳:
``php $user = new User; $user->email = '[email protected]'; $user->save(); echo $user->created_at; // 显示当前时间戳
[[yii\behaviors\TimestampBehavior|TimestampBehavior]] 行为还提供了一个有用的方法 [[yii\behaviors\TimestampBehavior::touch()|touch()]],这个方法能将当前时间戳赋值给指定属性并保存到数据库:
```php
$user->touch('login_time');
与 PHP traits 的比较
尽管行为在 "注入" 属性和方法到主类方面类似于 traits ,它们在很多方面却不相同。如上所述,它们各有利弊。它们更像是互补的而不是相互替代。
行为的优势
行为类像普通类支持继承。另一方面,traits 可以视为 PHP 语言支持的复制粘贴功能,它不支持继承。
行为无须修改组件类就可动态附加到组件或移除。要使用 traits,必须修改使用它的类。
行为是可配置的而 traits 不能。
行为以响应事件来自定义组件的代码执行。
当不同行为附加到同一组件产生命名冲突时,这个冲突通过先附加行为的优先权自动解决。而由不同 traits 引发的命名冲突需要通过手工重命名冲突属性或方法来解决。
traits 的优势
traits 比起行为更高效,因为行为是对象,消耗时间和内存。
IDE 对 traits 更友好,因为它们是语言结构。
配置:
在 Yii 中,创建新对象和初始化已存在对象时广泛使用配置。配置通常包含被创建对象的类名和一组将要赋值给对象属性的初始值。还可能包含一组将被附加到对象事件上的句柄。和一组将被附加到对象上的行为。
以下代码中的配置被用来创建并初始化一个数据库连接:
- $config = [
- 'class' => 'yii\\db\\Connection',
- 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
- 'username' => 'root',
- 'password' => '',
- 'charset' => 'utf8',
- ];
-
- $db = Yii::createObject($config);
[[Yii::createObject()]] 方法接受一个配置并根据配置中指定的类名创建对象。对象实例化后,剩余的参数被用来初始化对象的属性,事件处理和行为。
对于已存在的对象,可以使用 [[Yii::configure()]] 方法根据配置去初始化其属性,就像这样:
- Yii::configure($object, $config);
请注意,如果配置一个已存在的对象,那么配置数组中不应该包含指定类名的 class
元素。
配置的格式
一个配置的格式可以描述为以下形式:
- [
- 'class' => 'ClassName',
- 'propertyName' => 'propertyValue',
- 'on eventName' => $eventHandler,
- 'as behaviorName' => $behaviorConfig,
- ]
其中
- class 元素指定了将要创建的对象的完全限定类名。
- propertyName 元素指定了对象属性的初始值。键名是属性名,值是该属性对应的初始值。只有公共成员变量以及通过 getter/setter 定义的属性可以被配置。
- on eventName 元素指定了附加到对象事件上的句柄是什么。请注意,数组的键名由 on 前缀加事件名组成。请参考事件章节了解事件句柄格式。
- as behaviorName 元素指定了附加到对象的行为。请注意,数组的键名由 as 前缀加行为名组成。$behaviorConfig 表示创建行为的配置信息,格式与我们现在总体叙述的配置格式一样。
下面是一个配置了初始化属性值,事件句柄和行为的示例:
- [
- 'class' => 'app\\components\\SearchEngine',
- 'apiKey' => 'xxxxxxxx',
- 'on search' => function ($event) {
- Yii::info("搜索的关键词: " . $event->keyword);
- },
- 'as indexer' => [
- 'class' => 'app\\components\\IndexerBehavior',
- // ... 初始化属性值 ...
- ],
- ]
使用配置
Yii 中的配置可以用在很多场景。本章开头我们展示了如何使用 [[Yii::creatObject()]] 根据配置信息创建对象。本小节将介绍配置的两种主要用法 —— 配置应用与配置小部件。
应用的配置
应用的配置可能是最复杂的配置之一。因为 [[yii\web\Application|application]] 类拥有很多可配置的属性和事件。更重要的是它的 [[yii\web\Application::components|components]] 属性可以接收配置数组并通过应用注册为组件。以下是一个针对基础应用模板的应用配置概要:
- $config = [
- 'id' => 'basic',
- 'basePath' => dirname(__DIR__),
- 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'),
- 'components' => [
- 'cache' => [
- 'class' => 'yii\\caching\\FileCache',
- ],
- 'mailer' => [
- 'class' => 'yii\\swiftmailer\\Mailer',
- ],
- 'log' => [
- 'class' => 'yii\\log\\Dispatcher',
- 'traceLevel' => YII_DEBUG ? 3 : 0,
- 'targets' => [
- [
- 'class' => 'yii\\log\\FileTarget',
- ],
- ],
- ],
- 'db' => [
- 'class' => 'yii\\db\\Connection',
- 'dsn' => 'mysql:host=localhost;dbname=stay2',
- 'username' => 'root',
- 'password' => '',
- 'charset' => 'utf8',
- ],
- ],
- ];
配置中没有 class
键的原因是这段配置应用在下面的入口脚本中,类名已经指定了。
- (new yii\\web\\Application($config))->run();
更多关于应用 components
属性配置的信息可以查阅应用以及服务定位器章节。
小部件的配置
使用小部件时,常常需要配置以便自定义其属性。 [[yii\base\Widget::widget()]] 和 [[yii\base\Widget::beginWidget()]] 方法都可以用来创建小部件。它们可以接受配置数组:
- use yii\\widgets\\Menu;
-
- echo Menu::widget([
- 'activateItems' => false,
- 'items' => [
- ['label' => 'Home', 'url' => ['site/index']],
- ['label' => 'Products', 'url' => ['product/index']],
- ['label' => 'Login', 'url' => ['site/login'], 'visible' => Yii::$app->user->isGuest],
- ],
- ]);
上述代码创建了一个小部件 Menu
并将其 activateItems
属性初始化为 false。item
属性也配置成了将要显示的菜单条目。
请注意,代码中已经给出了类名 yii\widgets\Menu',配置数组**不应该**再包含
class` 键。
配置文件
当配置的内容十分复杂,通用做法是将其存储在一或多个 PHP 文件中,这些文件被称为配置文件。一个配置文件返回的是 PHP 数组。例如,像这样把应用配置信息存储在名为 web.php
的文件中:
- return [
- 'id' => 'basic',
- 'basePath' => dirname(__DIR__),
- 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'),
- 'components' => require(__DIR__ . '/components.php'),
- ];
鉴于 components
配置也很复杂,上述代码把它们存储在单独的 components.php
文件中,并且包含在web.php
里。components.php
的内容如下:
- return [
- 'cache' => [
- 'class' => 'yii\\caching\\FileCache',
- ],
- 'mailer' => [
- 'class' => 'yii\\swiftmailer\\Mailer',
- ],
- 'log' => [
- 'class' => 'yii\\log\\Dispatcher',
- 'traceLevel' => YII_DEBUG ? 3 : 0,
- 'targets' => [
- [
- 'class' => 'yii\\log\\FileTarget',
- ],
- ],
- ],
- 'db' => [
- 'class' => 'yii\\db\\Connection',
- 'dsn' => 'mysql:host=localhost;dbname=stay2',
- 'username' => 'root',
- 'password' => '',
- 'charset' => 'utf8',
- ],
- ];
仅仅需要 “require”,就可以取得一个配置文件的配置内容,像这样:
- $config = require('path/to/web.php');
- (new yii\\web\\Application($config))->run();
默认配置
[[Yii::createObject()]] 方法基于依赖注入容器实现。使用 [[Yii::creatObject()]] 创建对象时,可以附加一系列默认配置到指定类的任何实例。默认配置还可以在入口脚本中调用 Yii::$container->set()
来定义。
例如,如果你想自定义 [[yii\widgets\LinkPager]] 小部件,以便让分页器最多只显示 5 个翻页按钮(默认是 10 个),你可以用下述代码实现:
- \\Yii::$container->set('yii\\widgets\\LinkPager', [
- 'maxButtonCount' => 5,
- ]);
不使用默认配置的话,你就得在任何使用分页器的地方,都配置 maxButtonCount
的值。
环境常量
配置经常要随着应用运行的不同环境更改。例如在开发环境中,你可能使用名为 mydb_dev
的数据库,而生产环境则使用 mydb_prod
数据库。为了便于切换使用环境,Yii 提供了一个定义在入口脚本中的 YII_ENV
常量。如下:
- defined('YII_ENV') or define('YII_ENV', 'dev');
你可以把 YII_ENV
定义成以下任何一种值:
- prod:生产环境。常量 YII_ENV_PROD 将被看作 true。如果你没修改过,这就是 YII_ENV 的默认值。
- dev:开发环境。常量 YII_ENV_DEV 将被看作 true。
- test:测试环境。常量 YII_ENV_TEST 将被看作 true。
有了这些环境常量,你就可以根据当下应用运行环境的不同,进行差异化配置。例如,应用可以包含下述代码只在开发环境中开启调试工具。
- $config = [...];
-
- if (YII_ENV_DEV) {
- // 根据 `dev` 环境进行的配置调整
- $config['bootstrap'][] = 'debug';
- $config['modules']['debug'] = 'yii\\debug\\Module';
- }
-
- return $config;
别名(Aliases):
别名用来表示文件路径和 URL,这样就避免了在代码中硬编码一些绝对路径和 URL。一个别名必须以 @
字符开头,以区别于传统的文件路径和 URL。Yii 预定义了大量可用的别名。例如,别名 @yii
指的是 Yii 框架本身的安装目录,而 @web
表示的是当前运行应用的根 URL。
定义别名
你可以调用 [[Yii::setAlias()]] 来给文件路径或 URL 定义别名:
- // 文件路径的别名
- Yii::setAlias('@foo', '/path/to/foo');
-
- // URL 的别名
- Yii::setAlias('@bar', 'http://www.example.com');
注意:别名所指向的文件路径或 URL 不一定是真实存在的文件或资源。
可以通过在一个别名后面加斜杠 /
和一至多个路径分段生成新别名(无需调用 [[Yii::setAlias()]])。我们把通过 [[Yii::setAlias()]] 定义的别名称为根别名,而用他们衍生出去的别名成为衍生别名。例如,@foo
就是跟别名,而 @foo/bar/file.php
是一个衍生别名。
你还可以用别名去定义新别名(根别名与衍生别名均可):
- Yii::setAlias('@foobar', '@foo/bar');
根别名通常在引导阶段定义。比如你可以在入口脚本里调用 [[Yii::setAlias()]]。为了方便起见,应用提供了一个名为 aliases
的可写属性,你可以在应用配置中设置它,就像这样:
- return [
- // ...
- 'aliases' => [
- '@foo' => '/path/to/foo',
- '@bar' => 'http://www.example.com',
- ],
- ];
解析别名
你可以调用 [[Yii::getAlias()]] 命令来解析根别名到对应的文件路径或 URL。同样的页面也可以用于解析衍生别名。例如:
- echo Yii::getAlias('@foo'); // 输出:/path/to/foo
- echo Yii::getAlias('@bar'); // 输出:http://www.example.com
- echo Yii::getAlias('@foo/bar/file.php'); // 输出:/path/to/foo/bar/file.php
由衍生别名所解析出的文件路径和 URL 是通过替换掉衍生别名中的根别名部分得到的。
注意:[[Yii::getAlias()]] 并不检查结果路径/URL 所指向的资源是否真实存在。
根别名可能也会包含斜杠 /
。[[Yii::getAlias()]] 足够智能到判断一个别名中的哪部分是根别名,因此能正确解析文件路径/URL。例如:
- Yii::setAlias('@foo', '/path/to/foo');
- Yii::setAlias('@foo/bar', '/path2/bar');
- echo Yii::getAlias('@foo/test/file.php'); // 输出:/path/to/foo/test/file.php
- echo Yii::getAlias('@foo/bar/file.php'); // 输出:/path2/bar/file.php
若 @foo/bar
未被定义为根别名,最后一行语句会显示为 /path/to/foo/bar/file.php
。
使用别名
别名在 Yii 的很多地方都会被正确识别,无需调用 [[Yii::getAlias()]] 来把它们转换为路径/URL。例如,[[yii\caching\FileCache::cachePath]] 能同时接受文件路径或是指向文件路径的别名,因为通过 @
前缀能区分它们。
- use yii\\caching\\FileCache;
-
- $cache = new FileCache([
- 'cachePath' => '@runtime/cache',
- ]);
请关注 API 文档了解特定属性或方法参数是否支持别名。
预定义的别名
Yii 预定义了一系列别名来简化常用路径和 URL的使用:
- @yii - BaseYii.php 文件所在的目录(也被称为框架安装目录)
- @app - 当前运行的应用 [[yii\base\Application::basePath|根路径(base path)]]
- @runtime - 当前运行的应用的 [[yii\base\Application::runtimePath|运行环境(runtime)路径]]
- @vendor - [[yii\base\Application::vendorPath|Composer 供应商目录]]
- @webroot - 当前运行应用的 Web 入口目录
- @web - 当前运行应用的根 URL
@yii
别名是在入口脚本里包含 Yii.php
文件时定义的,其他的别名都是在配置应用的时候,于应用的构造方法内定义的。
扩展的别名
每一个通过 Composer 安装的 扩展 都自动添加了一个别名。该别名会以该扩展在 composer.json
文件中所声明的根命名空间为名,且他直接代指该包的根目录。例如,如果你安装有 yiisoft/yii2-jui
扩展,会自动得到 @yii/jui
别名,它定义于引导启动阶段:
- Yii::setAlias('@yii/jui', 'VendorPath/yiisoft/yii2-jui');
服务定位器:
服务定位器是一个了解如何提供各种应用所需的服务(或组件)的对象。在服务定位器中,每个组件都只有一个单独的实例,并通过ID 唯一地标识。用这个 ID 就能从服务定位器中得到这个组件。
在 Yii 中,服务定位器是 [[yii\di\ServiceLocator]] 或其子类的一个实例。
最常用的服务定位器是application(应用)对象,可以通过 \Yii::$app
访问。它所提供的服务被称为application components(应用组件),比如:request
、response
、urlManager
组件。可以通过服务定位器所提供的功能,非常容易地配置这些组件,或甚至是用你自己的实现替换掉他们。
除了 application 对象,每个模块对象本身也是一个服务定位器。
要使用服务定位器,第一步是要注册相关组件。组件可以通过 [[yii\di\ServiceLocator::set()]] 方法进行注册。以下的方法展示了注册组件的不同方法:
- use yii\\di\\ServiceLocator;
- use yii\\caching\\FileCache;
-
- $locator = new ServiceLocator;
-
- // 通过一个可用于创建该组件的类名,注册 "cache" (缓存)组件。
- $locator->set('cache', 'yii\\caching\\ApcCache');
-
- // 通过一个可用于创建该组件的配置数组,注册 "db" (数据库)组件。
- $locator->set('db', [
- 'class' => 'yii\\db\\Connection',
- 'dsn' => 'mysql:host=localhost;dbname=demo',
- 'username' => 'root',
- 'password' => '',
- ]);
-
- // 通过一个能返回该组件的匿名函数,注册 "search" 组件。
- $locator->set('search', function () {
- return new app\\components\\SolrService;
- });
-
- // 用组件注册 "pageCache" 组件
- $locator->set('pageCache', new FileCache);
一旦组件被注册成功,你可以任选以下两种方式之一,通过它的 ID 访问它:
- $cache = $locator->get('cache');
- // 或者
- $cache = $locator->cache;
如上所示, [[yii\di\ServiceLocator]] 允许通过组件 ID 像访问一个属性值那样访问一个组件。当你第一次访问某组件时,[[yii\di\ServiceLocator]] 会通过该组件的注册信息创建一个该组件的实例,并返回它。之后,如果再次访问,则服务定位器会返回同一个实例。
你可以通过 [[yii\di\ServiceLocator::has()]] 检查某组件 ID 是否被注册。若你用一个无效的 ID 调用 [[yii\di\ServiceLocator::get()]],则会抛出一个异常。
因为服务定位器,经常会在创建时附带配置信息,因此我们提供了一个可写的属性,名为 [[yii\di\ServiceLocator::setComponents()|components]],这样就可以配置该属性,或一次性注册多个组件。下面的代码展示了如何用一个配置数组,配置一个应用并注册"db","cache" 和 "search" 三个组件:
- return [
- // ...
- 'components' => [
- 'db' => [
- 'class' => 'yii\\db\\Connection',
- 'dsn' => 'mysql:host=localhost;dbname=demo',
- 'username' => 'root',
- 'password' => '',
- ],
- 'cache' => 'yii\\caching\\ApcCache',
- 'search' => function () {
- return new app\\components\\SolrService;
- },
- ],
- ];
依赖注入容器:
依赖注入(Dependency Injection,DI)容器就是一个对象,它知道怎样初始化并配置对象及其依赖的所有对象。Martin 的文章 已经解释了 DI 容器为什么很有用。这里我们主要讲解 Yii 提供的 DI 容器的使用方法。
依赖注入
Yii 通过 [[yii\di\Container]] 类提供 DI 容器特性。它支持如下几种类型的依赖注入:
- 构造方法注入;
- Setter 和属性注入;
- PHP 回调注入.
构造方法注入
在参数类型提示的帮助下,DI 容器实现了构造方法注入。当容器被用于创建一个新对象时,类型提示会告诉它要依赖什么类或接口。容器会尝试获取它所依赖的类或接口的实例,然后通过构造器将其注入新的对象。例如:
- class Foo
- {
- public function __construct(Bar $bar)
- {
- }
- }
-
- $foo = $container->get('Foo');
- // 上面的代码等价于:
- $bar = new Bar;
- $foo = new Foo($bar);
Setter 和属性注入
Setter 和属性注入是通过配置提供支持的。当注册一个依赖或创建一个新对象时,你可以提供一个配置,该配置会提供给容器用于通过相应的 Setter 或属性注入依赖。例如:
- use yii\\base\\Object;
-
- class Foo extends Object
- {
- public $bar;
-
- private $_qux;
-
- public function getQux()
- {
- return $this->_qux;
- }
-
- public function setQux(Qux $qux)
- {
- $this->_qux = $qux;
- }
- }
-
- $container->get('Foo', [], [
- 'bar' => $container->get('Bar'),
- 'qux' => $container->get('Qux'),
- ]);
PHP 回调注入
这种情况下,容器将使用一个注册过的 PHP 回调创建一个类的新实例。回调负责解决依赖并将其恰当地注入新创建的对象。例如:
- $container->set('Foo', function () {
- return new Foo(new Bar);
- });
-
- $foo = $container->get('Foo');
注册依赖关系
可以用 [[yii\di\Container::set()]] 注册依赖关系。注册会用到一个依赖关系名称和一个依赖关系的定义。依赖关系名称可以是一个类名,一个接口名或一个别名。依赖关系的定义可以是一个类名,一个配置数组,或者一个 PHP 回调。
- $container = new \\yii\\di\\Container;
-
- // 注册一个同类名一样的依赖关系,这个可以省略。
- $container->set('yii\\db\\Connection');
-
- // 注册一个接口
- // 当一个类依赖这个接口时,相应的类会被初始化作为依赖对象。
- $container->set('yii\\mail\\MailInterface', 'yii\\swiftmailer\\Mailer');
-
- // 注册一个别名。
- // 你可以使用 $container->get('foo') 创建一个 Connection 实例
- $container->set('foo', 'yii\\db\\Connection');
-
- // 通过配置注册一个类
- // 通过 get() 初始化时,配置将会被使用。
- $container->set('yii\\db\\Connection', [
- 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
- 'username' => 'root',
- 'password' => '',
- 'charset' => 'utf8',
- ]);
-
- // 通过类的配置注册一个别名
- // 这种情况下,需要通过一个 “class” 元素指定这个类
- $container->set('db', [
- 'class' => 'yii\\db\\Connection',
- 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
- 'username' => 'root',
- 'password' => '',
- 'charset' => 'utf8',
- ]);
-
- // 注册一个 PHP 回调
- // 每次调用 $container->get('db') 时,回调函数都会被执行。
- $container->set('db', function ($container, $params, $config) {
- return new \\yii\\db\\Connection($config);
- });
-
- // 注册一个组件实例
- // $container->get('pageCache') 每次被调用时都会返回同一个实例。
- $container->set('pageCache', new FileCache);
Tip: 如果依赖关系名称和依赖关系的定义相同,则不需要通过 DI 容器注册该依赖关系。
通过 set()
注册的依赖关系,在每次使用时都会产生一个新实例。可以使用 [[yii\di\Container::setSingleton()]] 注册一个单例的依赖关系:
- $container->setSingleton('yii\\db\\Connection', [
- 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
- 'username' => 'root',
- 'password' => '',
- 'charset' => 'utf8',
- ]);
解决依赖关系
注册依赖关系后,就可以使用 DI 容器创建新对象了。容器会自动解决依赖关系,将依赖实例化并注入新创建的对象。依赖关系的解决是递归的,如果一个依赖关系中还有其他依赖关系,则这些依赖关系都会被自动解决。
可以使用 [[yii\di\Container::get()]] 创建新的对象。该方法接收一个依赖关系名称,它可以是一个类名,一个接口名或一个别名。依赖关系名或许是通过 set()
或 setSingleton()
注册的。你可以随意地提供一个类的构造器参数列表和一个configuration 用于配置新创建的对象。例如:
- // "db" 是前面定义过的一个别名
- $db = $container->get('db');
-
- // 等价于: $engine = new \\app\\components\\SearchEngine($apiKey, ['type' => 1]);
- $engine = $container->get('app\\components\\SearchEngine', [$apiKey], ['type' => 1]);
代码背后,DI 容器做了比创建对象多的多的工作。容器首先将检查类的构造方法,找出依赖的类或接口名,然后自动递归解决这些依赖关系。
如下代码展示了一个更复杂的示例。UserLister
类依赖一个实现了 UserFinderInterface
接口的对象;UserFinder
类实现了这个接口,并依赖于一个 Connection
对象。所有这些依赖关系都是通过类构造器参数的类型提示定义的。通过属性依赖关系的注册,DI 容器可以自动解决这些依赖关系并能通过一个简单的get('userLister')
调用创建一个新的 UserLister
实例。
- namespace app\\models;
-
- use yii\\base\\Object;
- use yii\\db\\Connection;
- use yii\\di\\Container;
-
- interface UserFinderInterface
- {
- function findUser();
- }
-
- class UserFinder extends Object implements UserFinderInterface
- {
- public $db;
-
- public function __construct(Connection $db, $config = [])
- {
- $this->db = $db;
- parent::__construct($config);
- }
-
- public function findUser()
- {
- }
- }
-
- class UserLister extends Object
- {
- public $finder;
-
- public function __construct(UserFinderInterface $finder, $config = [])
- {
- $this->finder = $finder;
- parent::__construct($config);
- }
- }
-
- $container = new Container;
- $container->set('yii\\db\\Connection', [
- 'dsn' => '...',
- ]);
- $container->set('app\\models\\UserFinderInterface', [
- 'class' => 'app\\models\\UserFinder',
- ]);
- $container->set('userLister', 'app\\models\\UserLister');
-
- $lister = $container->get('userLister');
-
- // 等价于:
-
- $db = new \\yii\\db\\Connection(['dsn' => '...']);
- $finder = new UserFinder($db);
- $lister = new UserLister($finder);
实践中的运用
当在应用程序的入口脚本中引入 Yii.php
文件时,Yii 就创建了一个 DI 容器。这个 DI 容器可以通过 [[Yii::$container]] 访问。当调用 [[Yii::createObject()]] 时,此方法实际上会调用这个容器的 [[yii\di\Container::get()|get()]] 方法创建新对象。如上所述,DI 容器会自动解决依赖关系(如果有)并将其注入新创建的对象中。因为 Yii 在其多数核心代码中都使用了 [[Yii::createObject()]] 创建新对象,所以你可以通过 [[Yii::$container]] 全局性地自定义这些对象。
例如,你可以全局性自定义 [[yii\widgets\LinkPager]] 中分页按钮的默认数量:
- \\Yii::$container->set('yii\\widgets\\LinkPager', ['maxButtonCount' => 5]);
这样如果你通过如下代码在一个视图里使用这个挂件,它的 maxButtonCount
属性就会被初始化为 5 而不是类中定义的默认值 10。
- echo \\yii\\widgets\\LinkPager::widget();
然而你依然可以覆盖通过 DI 容器设置的值:
- echo \\yii\\widgets\\LinkPager::widget(['maxButtonCount' => 20]);
另一个例子是借用 DI 容器中自动构造方法注入带来的好处。假设你的控制器类依赖一些其他对象,例如一个旅馆预订服务。你可以通过一个构造器参数声明依赖关系,然后让 DI 容器帮你自动解决这个依赖关系。
- namespace app\\controllers;
-
- use yii\\web\\Controller;
- use app\\components\\BookingInterface;
-
- class HotelController extends Controller
- {
- protected $bookingService;
-
- public function __construct($id, $module, BookingInterface $bookingService, $config = [])
- {
- $this->bookingService = $bookingService;
- parent::__construct($id, $module, $config);
- }
- }
如果你从浏览器中访问这个控制器,你将看到一个报错信息,提醒你 BookingInterface
无法被实例化。这是因为你需要告诉 DI 容器怎样处理这个依赖关系。
- \\Yii::$container->set('app\\components\\BookingInterface', 'app\\components\\BookingService');
现在如果你再次访问这个控制器,一个 app\components\BookingService
的实例就会被创建并被作为第三个参数注入到控制器的构造器中。
什么时候注册依赖关系
由于依赖关系在创建新对象时需要解决,因此它们的注册应该尽早完成。如下是推荐的实践:
- 如果你是一个应用程序的开发者,你可以在应用程序的入口脚本或者被入口脚本引入的脚本中注册依赖关系。
- 如果你是一个可再分发扩展的开发者,你可以将依赖关系注册到扩展的引导类中。
总结
依赖注入和服务定位器都是流行的设计模式,它们使你可以用充分解耦且更利于测试的风格构建软件。强烈推荐你阅读 Martin 的文章 ,对依赖注入和服务定位器有个更深入的理解。
Yii 在依赖住入(DI)容器之上实现了它的服务定位器。当一个服务定位器尝试创建一个新的对象实例时,它会把调用转发到 DI 容器。后者将会像前文所述那样自动解决依赖关系。