Yii2 將控制器動作封裝為獨立 Action 類別,支援獨立複用、參數綁定、事件觸發與過濾器攔截。本文基於原始碼解析 yii\base\Action、ActionEvent、ActionFilter 三大核心類別的設計與協作機制。
<?php
namespace yii\base;
class Action extends Component
{
public $id; // 動作 ID (如 'create')
public $controller; // 所屬控制器實例
public function __construct($id, $controller, $config = [])
{
$this->id = $id;
$this->controller = $controller;
parent::__construct($config);
}
// 唯一 ID:模組/控制器/動作
public function getUniqueId()
{
return $this->controller->getUniqueId() . '/' . $this->id;
}
// 核心執行入口
public function runWithParams($params)
{
// 1. 檢查 run() 方法存在
if (!method_exists($this, 'run')) {
throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.');
}
// 2. 參數綁定:控制器負責將 $params 映射到 run() 參數
$args = $this->controller->bindActionParams($this, $params);
// 3. 記錄請求參數供應用層存取
if (Yii::$app->requestedParams === null) {
Yii::$app->requestedParams = $args;
}
// 4. 執行前置/後置鉤子
if ($this->beforeRun()) {
$result = call_user_func_array([$this, 'run'], $args);
$this->afterRun();
return $result;
}
return null;
}
// 可覆寫:返回 false 取消執行
protected function beforeRun() { return true; }
protected function afterRun() { }
}
ReflectionFunction 分析 run() 簽名。$params (來自 UrlManager 解析) 按參數名匹配值。...$args。BadRequestHttpException。<?php
namespace yii\base;
class ActionEvent extends Event
{
public $action; // 正在執行的 Action 實例
public $result; // 動作執行結果,可被事件處理器修改
public $isValid = true; // 是否繼續執行,EVENT_BEFORE_ACTION 可設為 false 取消
public function __construct($action, $config = [])
{
$this->action = $action;
parent::__construct($config);
}
}
EVENT_BEFORE_ACTION,傳遞 ActionEvent。$event->isValid = false 取消後續執行。isValid 為 true,執行 Action::runWithParams()。EVENT_AFTER_ACTION,可修改 $event->result。<?php
namespace yii\base;
class ActionFilter extends Behavior
{
public $only = []; // 僅套用的動作 ID 列表
public $except = []; // 排除的動作 ID 列表
// 綁定事件
public function attach($owner)
{
$this->owner = $owner;
$owner->on(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']);
}
public function detach()
{
if ($this->owner) {
$owner->off(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']);
$owner->off(Controller::EVENT_AFTER_ACTION, [$this, 'afterFilter']);
$this->owner = null;
}
}
// EVENT_BEFORE_ACTION 處理器
public function beforeFilter($event)
{
if (!$this->isActive($event->action)) return;
$event->isValid = $this->beforeAction($event->action);
if ($event->isValid) {
// 成功才掛載 afterFilter (確保配對)
$this->owner->on(Controller::EVENT_AFTER_ACTION, [$this, 'afterFilter'], null, false);
} else {
$event->handled = true; // 停止事件傳播
}
}
// EVENT_AFTER_ACTION 處理器
public function afterFilter($event)
{
$event->result = $this->afterAction($event->action, $event->result);
$this->owner->off(Controller::EVENT_AFTER_ACTION, [$this, 'afterFilter']);
}
// 可覆寫:返回 false 取消動作
public function beforeAction($action) { return true; }
// 可覆寫:處理結果
public function afterAction($action, $result) { return $result; }
// 判斷是否套用於該動作
protected function isActive($action)
{
$id = $action->id;
if ($this->owner instanceof Module) {
// 模組層級:轉換為相對模組的 ID
$mid = $this->owner->getUniqueId();
$uid = $action->getUniqueId();
if ($mid && strpos($uid, $mid) === 0) {
$id = substr($uid, strlen($mid) + 1);
}
}
return !in_array($id, $this->except, true) && (empty($this->only) || in_array($id, $this->only, true));
}
}
| 過濾器 | 用途 | 關鍵邏輯 |
|---|---|---|
| AccessControl | RBAC/角色存取控制 | beforeAction 檢查規則,失敗拋 ForbiddenHttpException |
| PageCache | 整頁快取 | beforeAction 檢查快取命中,命中直接返回、設 isValid=false;afterAction 存入快取 |
| HttpCache | HTTP 條件快取 (ETag/Last-Modified) | 設定回應標頭,beforeAction 檢查 If-None-Match 返回 304 |
| VerbFilter | HTTP 方法限制 | beforeAction 驗證 Yii::$app->request->method |
| RateLimiter | API 速率限制 | 實作 RateLimitInterface,beforeAction 檢查配額 |
<?php
namespace app\components;
use yii\base\ActionFilter;
use Yii;
class ActionLogFilter extends ActionFilter
{
public function beforeAction($action)
{
// 記錄開始時間
Yii::info("Action started: {$action->getUniqueId()}", __METHOD__);
return true;
}
public function afterAction($action, $result)
{
$duration = microtime(true) - YII_BEGIN_TIME;
$userId = Yii::$app->user->isGuest ? 'guest' : Yii::$app->user->id;
$ip = Yii::$app->request->userIP;
Yii::info(
"Action finished: {$action->getUniqueId()} | User: $userId | IP: $ip | Duration: {$duration}s | Result: " . json_encode($result),
__METHOD__
);
return $result;
}
}
<?php
namespace app\controllers;
use yii\web\Controller;
use app\components\ActionLogFilter;
use yii\filters\AccessControl;
class PostController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
['allow' => true, 'actions' => ['index', 'view'], 'roles' => ['?']],
['allow' => true, 'actions' => ['create', 'update', 'delete'], 'roles' => ['@']],
],
],
'log' => [
'class' => ActionLogFilter::class,
'only' => ['create', 'update', 'delete'], // 僅寫入操作記錄
],
];
}
public function actionCreate()
{
// ...
}
}
runAction() 建立 Action 實例。EVENT_BEFORE_ACTION → 所有掛載過濾器的 beforeFilter() 執行 (按 behaviors() 順序)。isValid=true,執行 Action::runWithParams() → run()。EVENT_AFTER_ACTION → 過濾器 afterFilter() 反向執行 (後掛載先執行)。