Yii2 有個很重要的特性是對 Restful API
的默認支持, 通過短短的幾個配置就可以實現簡單的對現有Model
的RESTful API
參考另一篇文章: http://www.cnblogs.com/ganiks/p/yii2-restful-api-dev.html
本文通過分析rest部分源碼,簡單剖析下yii2 實現 restful 的原理,並通過一些定制實現 對 關聯模型的RESTful api 操作。
原創文章, 轉載請注明 http://www.cnblogs.com/ganiks/
~
代表extends from
的關系
| | rest/
| | |-Action.php ~ `\yii\base\Action`
| | |-Controller.php ~ `\yii\web\Controller`
| | | |-ActiveController.php ~ `rest\Controller`
| | |-Serializer.php ~ `yii\base\Component`
| | |-UrlRule.php ~ `yii\web\CompositeUrlRule`
| | |-CreateAction.php ~ `rest\Action`
| | |-DeleteAction.php ~ `rest\Action`
| | |-IndexAction.php ~ `rest\Action`
| | |-OptionsAction.php ~ `rest\Action`
| | |-UpdateAction.php ~ `rest\Action`
| | |-ViewAction.php ~ `rest\Action`
- rest/Controller ~
\yii\web\Controller
Controller
是 RESTful API 控制器類的基類
它在一個API請求的控制周期中一次實現了下面的步驟 1~5
:
- 解析響應的內容格式
- 校驗請求方法
- 檢驗用戶權限
- 限制速度
- 格式化響應數據
use yii\filters\auth\CompositeAuth;
use yii\filters\ContentNegotiator;
use yii\filters\RateLimiter;
use yii\web\Response;
use yii\filters\VerbFilter;
/**
* Controller is the base class for RESTful API controller classes.
*
* Controller implements the following steps in a RESTful API request handling cycle
* 1. Resolving response format (see [[ContentNegotiator]]);
* 2. Validating request method (see [[verbs()]]).
* 3. Authenticating user (see [[\yii\filters\auth\AuthInterface]]);
* 4. Rate limiting (see [[RateLimiter]]);
* 5. Formatting response data (see [[serializeData()]])
- behaviors
- contentNegotiator
- verbFilter
- authenticator
- rateLimiter
- afterAction
- serializeData
Yii::createObject($this->serializer)->serialize($data)
- serializeData
- verbs
[]
class Controller extends \yii\web\Controller
{
public $serializer = 'yii\rest\Serializer';
public $enableCsrfValidation = false;
public function behaviors()
{
return [
'contentNegotiator' => [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON,
'application/xml' => Response::FORMAT_XML,
],
],
'verbFilter' => [
'class' => VerbFilter::className(),
'actions' => $this->verbs(),
],
'authenticator' => [
'class' => CompositeAuth::className(),
],
'rateLimiter' => [
'class' => RateLimiter::className(),
],
]
}
public function verbs()
{
return [];
}
public function serializeData($data)
{
return Yii::createObject($this->serializer)->serialize($data);
}
public function afterAction($action, $result)
{
$result = parent::afterAction($action, $result);
return $this->serializeData($result);
}
}
- rest/ActiveController ~
rest/Controller
ActiveController
實現了一系列的和 ActiveRecord
互通數據的RESTful方法
ActiveRecord
的類名由 modelClass
變量指明, yii\db\ActiveRecordInterface
???
默認的, 支持下面的方法:
* - `index`: list of models
* - `view`: return the details of a model
* - `create`: create a new model
* - `update`: update an existing model
* - `delete`: delete an existing model
* - `options`: return the allowed HTTP methods
可以通過覆蓋 actions()
並且 unsetting
響應的 action
來禁用這些默認的動作。
要增加一個新的動作, 覆蓋 actions()
向其末尾增加一個新的 action class
或者 是一個新的 action method
注意一點,確保你同時也覆蓋了 verbs()
方法來聲明這個新的動作支持那些HTTP Method
也需要覆蓋checkAccess()
來檢查當前用戶是否有權限來執行響應的某個動作。
根據上面的說明再寫一遍 Controller
class ActiveController extends Controller
{
public #modelClass;
public $updateScenario = Model::SCENARIO_DEFAULT;
public $createScenario = Model::SCENARIO_DEFAULT;
public function init()
{
parent::init();
if($this->modelClass == null){
throw new InvalidConfigException('The "modelClass" property must be set.');
}
}
public function actions()
{
return [
'index' => [
'class' => 'app\controllers\rest\IndexAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'view'...
'create'...
'update'...
'delete'...
'options'...
];
}
protected function verbs()
{
return [
'index' => ['GET', 'HEAD'],
'view' =>['GET', 'HEAD'],
'create' =>['POST'],
'update' =>['PUT', 'PATCH'],
'delete' =>['DELETE'],
];
}
public function checkAccess($action, $model=null, $params = [])
{
}
}
下面來實現一個繼承自 這個rest\ActiveController
的 News
控制器:
namespace app\controllers;
use app\controllers\rest\ActiveController; #剛才這個AC,我從yii/rest下面拷貝了一份出來
class NewsController extends ActiveController
{
public $modelClass ='app\models\News';
}
定義到這里就足夠實現 rest\ActiveController 里面的默認方法了
下面來覆蓋下,實現一些定制的方法
class NewsController extends ActiveController
{
public $modelClass = 'app\models\News';
#定制serializer
#public $serializer = 'yii\rest\Serializer';
public $serializer = [
'class' => 'app\controllers\rest\Serializer',
'collectionEnvelope' => 'items',
];
public function behaviors()
{
$be = ArrayHelper::merge(
parent::behaviors(),
[
'verbFilter' => [
'class' => VerbFilter::className(),
'actions' => [
'index' => ['get'],
...
]
],
'authenticator' => [
'class' => CompositeAuth::className(),
'authMethods' => [
HttpBasicAuth::className(),
HttpBearerAuth::className(),
QueryParamAuth::className(),
]
],
'contentNegotiator' => [
'class' => ContentNegotiator::className(),
'formats' => [
'text/html' => Response::FORMAT_HTML,
]
],
'access' => [
'class' => AccessControl::className(),
'only' => ['view'],
'rules' => [
[
'actions' => ['view'],
'allow' => false,
'roles' => ['@'],
],
],
]
],
);
return $be;
}
public function checkAccess()
{
}
}
- 定制Actions
如果要對 Actions 進行大的改動,建議拷貝一份出來,不要使用原始的
yii\rest\XXXAction
命名空間
我這里以要實現對related models
進行 CURD 操作為目標進行大的改動
Action
在定制各個action
之前, 先看看它們的基類 rest\Action
, 主要是一個 findModel
的方法
class Action extend \yii\base\Action
{
public $modelClass;
public $findModel;
public $checkAccess;
public function init()
{
if($this->modelClass == null) {
throw new InvalidConfigException(get_class($this). '::$modelClass must be set');
}
}
public function findModel($id)
{
if($this->findModel !== null) {
return call_user_func($this->findModel, $id, $this);
}
$modelClass = $this->modelClass;
$keys = $modelClass::primaryKey();
if(count($keys) > 1) {
$values = explode(',', $id);
if..
} elseif($id !== null) {
$model = $modelClass::findOne($id);
}
if(isset($model)){
return $model;
}else {
throw new NotFoundHttpException("Object not found: $id");
}
}
}
view
view
動作不需要改動,因為 model
有 getRelated
的自有機制
class ViewAction extend Action
{
public function run($id)
{
$model = $this->findModel($id);
if($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
}
}
原創文章, 轉載請注明 http://www.cnblogs.com/ganiks/
update
public function run($id)
{
/* @var $model ActiveRecord */
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
$model->load(Yii::$app->getRequest()->getBodyParams(), '');
$model->save();
return $model;
}
經過改造后,需要滿足對關聯模型的update動作
public function run($id)
{
/* @var $model ActiveRecord */
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
/*
*
* x-www-form-urlencoded key=>value
* image mmmmmmmm
* link nnnnnnnnnn
* newsItem[title]=>ttttttttttt , don't use newsItem["title"]
* newsItem[body]=>bbbbbbbbbbb
* don't use newsItem=>array("title":"tttttt","body":"bbbbbbb")
* don't use newsItem=>{"title":"ttttttt","body":"bbbbbbbb"}
*
*/
$newsItem = Yii::$app->getRequest()->getBodyParams()['newsItem'];
/*
Array
(
[title] => ttttttttttt
[body] => bbbbbbbbbbb
)
*/
$model->newsItem->load($newsItem, '');
#$model->newsItem->load(Yii::$app->getRequest()->getBodyParams(), '');
#print_R($model->newsItem);exit;
#print_R($model->newsItem);exit;
if($model->save())
{
$model->load(Yii::$app->getRequest()->getBodyParams(), '');
$model->newsItem->save();
}
return $model;
}
這里還應該對
newsItem
save 失敗 的情況進行處理,暫且不處理。