行为简单来讲是组件的扩展,能够对组件的属性,方法,事件 (yii2组件的三大要点)进行扩展而无需改动组件现有的代码逻辑。即此行为所拥有的属性,方法,事件,都会被绑定它的组件 "获取" 到。因此 yii2 的行为在必定程度上也是对 Event 的封装,你能够在行为里定义须要扩展的属性,方法,也能够注册事件,让组件能够作到绑定此行为,即注册了某事件的功能。php
咱们知道,框架在执行过程当中有不少系统级别执行节点,在这些节点 yii2 使用 Event 来进实现行钩子机制。好比咱们调用一个 Action 有 beforeAction 和 afterAction 的执行节点,调用 Model 的 validate 方法有 beforeValidate 和 afterValidate 的执行节点,执行到相应的节点便会触发相应的事件,事件去检查有无注册的钩子,有的话即会触发。而行为则能够为某组件方便的实现此功能。html
yii\base\Model 中的 validate 方法的先后执行节点web
我若是在某行为中注册了model的这两个事件,那么任何继承至 yii\base\Model的组件只要绑定了此行为,都会被注册这两个事件。数组
yii\base\Behavior::$owner //行为全部者 确定是某组件对象yii2
yii\base\Behavior::events() // 行为扩展的事件app
yii\base\Behavior::attach($owner) //当组件绑定行为时 行为会为其注册 events 中定义的事件框架
yii\base\Behavior::detach($owner) //此方法组要是用于组件绑定行为名重复时进行事件的解绑yii
yii\base\Behavior 是行为的基类,全部的行为都继承于此,好比咱们经常使用的 yii\filter\AccessControl 和 yii\filter\VerbFilter。函数
app\behaviors\CtrlBehaviorpost
<?php /** * Created by sallency. * User: sallency * Date: 2016/5/31 0031 * Time: 16:03 */ namespace app\behaviors; use yii\base\Behavior; use yii\base\Event; use yii\rest\Controller; class CtrlBehavior extends Behavior { const PHP_WEB_EOL = "<br>"; public $param_1; public $param_2; /** * 行为是为 Controller 作的扩展 故能够注册 Controller 的事件 * @return array events for component owner */ public function events() { return [ Controller::EVENT_BEFORE_ACTION => "handlerBeforeAction", Controller::EVENT_AFTER_ACTION => "handlerAfterAction" ]; } /** * event handler * @param \yii\base\Event $event */ public function handlerBeforeAction(Event $event) { echo __METHOD__ . self::PHP_WEB_EOL; echo '由行为注册的组件事件,传递的$event->sender属性为此组件对象' . self::PHP_WEB_EOL; echo "组件的控制器和动做:" . $event->sender->uniqueId . '/' . $event->sender->action->id . self::PHP_WEB_EOL; echo self::PHP_WEB_EOL; } /** * event handler * @param \yii\base\Event $event */ public function handlerAfterAction(Event $event) { echo self::PHP_WEB_EOL; echo __METHOD__ . self::PHP_WEB_EOL; echo '由行为注册的组件事件,传递的$event->sender属性为此组件对象' . self::PHP_WEB_EOL; echo "组件的控制器和动做:" . $event->sender->uniqueId . '/' . $event->sender->action->id . self::PHP_WEB_EOL; } /** * 扩展方法 经过 __METHOD__ 我么能够看出这货被组件调用时究竟是不是组件的一个方法 */ public function extendMethodForCtrl() { echo "在行为中定义的方法:"; echo __METHOD__ . self::PHP_WEB_EOL; } }
app\controllers\BehaviorController
<?php /** * Created by sallency. * User: sallency * Date: 2016/5/31 0031 * Time: 16:23 */ namespace app\controllers; use app\behaviors\CtrlBehavior; use yii\web\Controller; class BehaviorController extends Controller { const PHP_WEB_EOL = "<br>"; public function init() { parent::init(); // TODO: Change the autogenerated stub } //绑定行为 静态绑定 还有 attachBehavior/attachBehaviors 动态绑定 public function behaviors() { return [ "ctrlBehavior" => [ "class" => CtrlBehavior::className(), "param_1" => "hello", "param_2" => "world" ] ]; } public function actionIndex() { echo "组件访问行为的属性和方法:" . __METHOD__ . self::PHP_WEB_EOL; //使用 __set __get 方法遍历访问行为队列 $_behaviors 中是否有行为对象包含如下属性 //有则经过此行为对象访问操做属性 echo "在行为中定义的属性:" . $this->param_1 . "\t" . $this->param_2 . self::PHP_WEB_EOL; //使用 __call 方法遍历访问行为队列 $_behaviors 中是否有行为对象包含如下方法 //有则经过此行为对象访问方法 $this->extendMethodForCtrl(); } }
访问 behavior/index
app\behaviors\CtrlBehavior::handlerBeforeAction 由行为注册的组件事件,传递的$event->sender属性为此组件对象 组件的控制器和动做:behavior/index 组件访问行为的属性和方法:app\controllers\BehaviorController::actionIndex 在行为中定义的属性:hello world 在行为中定义的方法:app\behaviors\CtrlBehavior::extendMethodForCtrl app\behaviors\CtrlBehavior::handlerAfterAction 由行为注册的组件事件,传递的$event->sender属性为此组件对象 组件的控制器和动做:behavior/index
能够看到,咱们并无在控制器中定义 $param_1,$param_2属性,没有定义 extendMethodForCtrl 方法,没有注册 EVENT_BEFORE_ACTION 和 EVENT_AFTER_ACTION 事件
但 actionIndex 执行前/后触发了 EVENT_BEFORE_ACTION/EVENT_AFTER_ACTION 事件,并且咱们能够访问在行为中定义的 $param_1 $param_2 和 extendMethodForCtrl 方法
你必需要明确的是,组件其实并无获得行为的属性和方法,组件行为队列:$_behaviors,这里面存放着你绑定到组件上的行为实例。组件访问这些看似本身获得属性和方法时,只不过是经过组件的 __set/__get 或者 __call 方法中的对 $_behaviors 中的行为对象进行遍历询问是否有此属性或方法,有的话则让此行为对象反馈给本身而已。
就好像老板雇佣了一批有技能的员工,对外看来这个老板会不少技能,但其实他只不过是把外界的需求分发给他的员工,找到一个能解决此需求的员工去处理这个需求而已,干活的仍是员工。
咱们经常使用的组件静态绑定行为的方法(动态绑定: attachBehavior/attachBehaviors 方法)。此方法返回须要注册的行为的yii2标准的参数数组的数组
yii\base\Component::behaviors() { return [ "myBehavior_1" => [ "class" => "app\behaviors\MyBehavior1" "param_1" => "this is my behavior_1", "param_2" => "hello world" ], "myBehavior_2" => [ "class" => "app\behaviors\MyBehavior2" "param_1" => "this is my behavior_2", "param_2" => "hello world" ] ]; }
此方法主要是将 behaviors 方法中注册的行为分配给 attachBehaviorInternal 方法进行行为绑定
yii\base\Component::ensureBehaviors() { if ($this->_behaviors === null) { $this->_behaviors = []; //获得组件的行为注册数组 foreach ($this->behaviors() as $name => $behavior) { //为当前组件注册行为类 $this->attachBehaviorInternal($name, $behavior); } } }
attachBehaviorInternal 的主要功能是
1 经过 Yii::createObject($behaviorConfig) 方法获得一个行为实例,将其存储在组件的 $_behaviors 中,这样结合 __set __get __call 方法便能直接访问此行为实例的属性和方法。
2 此行为实例同时会调用自身的 attach 方法(yii\base\Behavior::attach()),检测本身的 events 中注册的事件,将其绑定到当前的组件对象中
/** * $name 行为名 * $behavior 行为参数 用于建立一个行为实例 */ private function attachBehaviorInternal($name, $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 中经过其 events 来为当前组件注册事件行为 $behavior->attach($this); //将这个行为放入本身的行为队列 $this->_behaviors[$name] = $behavior; } return $behavior; }
能够发现,每一个行为实例在被放入组件的行为队列 $_behaviors 的同时,会去调用本身的事件注册函数 attach($this)(见 5),为当前组件注册 events 方法中声明的事件
//须要为组件绑定的事件 public function events() { return []; } //为组件 $owner 绑定事件 public function attach($owner) { $this->owner = $owner; foreach ($this->events() as $event => $handler) { $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); } } //为组件 $owner 解绑事件 public function detach() { if ($this->owner) { foreach ($this->events() as $event => $handler) { $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); } $this->owner = null; } }
这里我只放 __call 方法的实现,代码一看便知,当组件访问一个自身没有定义的方法时会触发__call方法,yii2这里的处理逻辑即是去行为队列$_behaviors 中检索是否存在某个含有此方法的行为
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()"); }
yii2最经常使用的两个系统行为,这两个行为做为过滤器是给 yii\web\Controller 组件用的,绑定方法以下
public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'only' => ['login', 'logout'], //只对此处声明的Action生效 'rules' => [ [ 'actions' => ['logout'], 'allow' => true, 'roles' => ['@'],//认证用户 ], [ 'actions' => ['login'], 'allow' => true, 'roles' => ['?'],//游客 'denyCallback' => function($rule, $action) {//若是不是游客则无权访问 throw new \Exception("not allowed to access this page" . $action->id); } ], ], ], 'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'logout' => ['post'], 'login' => ['post'], 'index' => ['get', 'post', 'put', 'delete', 'head', 'option'] ], ], ]; }
AccessControl 主要是对访问控制,VerbFilter 则是对http的动词进行访问控制
简单看一下 VerbFilter 的源码
class VerbFilter extends Behavior { public $actions = []; //咱们绑定行为时传递的参数 public function events() //为组件注册的事件 能够看到是控制器调用Action前节点的事件 { return [Controller::EVENT_BEFORE_ACTION => 'beforeAction']; } //事件的handler public function beforeAction($event) { //获得本次请求的action $action = $event->action->id; if (isset($this->actions[$action])) { $verbs = $this->actions[$action]; } elseif (isset($this->actions['*'])) { $verbs = $this->actions['*']; } else { return $event->isValid; } //获得本次请求的 http 动词 $verb = Yii::$app->getRequest()->getMethod(); $allowed = array_map('strtoupper', $verbs); if (!in_array($verb, $allowed)) { $event->isValid = false; Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed)); throw new MethodNotAllowedHttpException('Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed) . '.'); } return $event->isValid; } }
此行为会为 Controller 组件注册 EVENT_BEFORE_ACTION 的事件,这样便会在 Action 调用前去校验本次的 http 动词是否符合规则。
AccessControl 并无直接继承 Behavior,而是经过继承 yii\base\ActionFilter 间接继承,同时 ActionFilter 对 Behavior 的 attach/detach 进行了重写,并不去调用 events 中为组件声明的事件(其实他就没声明...),而是固定的在 attach 绑定 EVENT_BEFORE_ACTION,在 detach 中定 EVENT_AFTER_ACTION
/** * @inheritdoc */ public function attach($owner) { $this->owner = $owner; $owner->on(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']); } /** * @inheritdoc */ public function detach() { if ($this->owner) { $this->owner->off(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']); $this->owner->off(Controller::EVENT_AFTER_ACTION, [$this, 'afterFilter']); $this->owner = null; } }
嗯,就这样啦
一、Component 将绑定的行为实例存放在本身的 $_behaviors 队列中,看似本身 拿到 了行为的方法或属性,其实也只是配合本身的 __set __get __call 方法在寻找不到时去遍历 $_behaviors 中的行为实例,看谁有此属性或方法而已,是老板和员工的关系
二、在绑定行为的时候,Component 存放的是此行为的一个实例(绑定时会进行实例类型检测,故全部的行为都是 Behavior或子类的实例),绑定时,此行为实例会调用本身的 attach 方法,将行为中为组件定义的事件绑定至此组件,这样便实现了事件绑定。