Behavior 的简述
行为简单来说是组件的扩展,可以对组件的属性,方法,事件 (yii2组件的三大要点)进行扩展而无需改动组件现有的代码逻辑。即此行为所拥有的属性,方法,事件,都会被绑定它的组件 "获取" 到。所以 yii2 的行为在一定程度上也是对 Event 的封装,你可以在行为里定义需要扩展的属性,方法,也可以注册事件,让组件可以做到绑定此行为,即注册了某事件的功能。
我们知道,框架在执行过程中有很多系统级别执行节点,在这些节点 yii2 使用 Event 来进实现行钩子机制。比如我们调用一个 Action 有 beforeAction 和 afterAction 的执行节点,调用 Model 的 validate 方法有 beforeValidate 和 afterValidate 的执行节点,执行到相应的节点便会触发相应的事件,事件去检查有无注册的钩子,有的话即会触发。而行为则可以为某组件方便的实现此功能。
yii\base\Model 中的 validate 方法的前后执行节点
我如果在某行为中注册了model的这两个事件,那么任何继承至 yii\base\Model的组件只要绑定了此行为,都会被注册这两个事件。
yii\base\Behavior 基类
yii\base\Behavior::$owner //行为所有者 肯定是某组件对象
yii\base\Behavior::events() // 行为扩展的事件
yii\base\Behavior::attach($owner) //当组件绑定行为时 行为会为其注册 events 中定义的事件
yii\base\Behavior::detach($owner) //此方法组要是用于组件绑定行为名重复时进行事件的解绑
yii\base\Behavior 是行为的基类,所有的行为都继承于此,比如我们常用的 yii\filter\AccessControl 和 yii\filter\VerbFilter。
行为实例
app\behaviors\CtrlBehavior
"; 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
"; 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::extendMethodForCtrlapp\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 方法
Behavior 实现机制
1、组件行为队列:$_behaviors
你必须要明确的是,组件其实并没有得到行为的属性和方法,组件行为队列:$_behaviors,这里面存放着你绑定到组件上的行为实例。组件访问这些看似自己得到属性和方法时,只不过是通过组件的 __set/__get 或者 __call 方法中的对 $_behaviors 中的行为对象进行遍历询问是否有此属性或方法,有的话则让此行为对象反馈给自己而已。
就好像老板雇佣了一批有技能的员工,对外看来这个老板会很多技能,但其实他只不过是把外界的需求分发给他的员工,找到一个能解决此需求的员工去处理这个需求而已,干活的还是员工。
2、组件的 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" ] ];}
3、组件的ensureBehaviors方法
此方法主要是将 behaviors 方法中注册的行为分配给 attachBehaviorInternal 方法进行行为绑定
yii\base\Component::ensureBehaviors(){ if ($this->_behaviors === null) { $this->_behaviors = []; //得到组件的行为注册数组 foreach ($this->behaviors() as $name => $behavior) { //为当前组件注册行为类 $this->attachBehaviorInternal($name, $behavior); } }}
4、组件的attachBehaviorInternal方法
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 方法中声明的事件
5、行为的 events attach detach 方法
//需要为组件绑定的事件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; }}
6、组件的 __set __get __call 方法
这里我只放 __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()");}
yii\filter\AccessControl / yii\filter\VerbFilter
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; }}
嗯,就这样啦
梳理下要点
1、Component 将绑定的行为实例存放在自己的 $_behaviors 队列中,看似自己 拿到 了行为的方法或属性,其实也只是配合自己的 __set __get __call 方法在寻找不到时去遍历 $_behaviors 中的行为实例,看谁有此属性或方法而已,是老板和员工的关系
2、在绑定行为的时候,Component 存放的是此行为的一个实例(绑定时会进行实例类型检测,故所有的行为都是 Behavior或子类的实例),绑定时,此行为实例会调用自己的 attach 方法,将行为中为组件定义的事件绑定至此组件,这样便实现了事件绑定。