Commit 18c7c63e by Qiang Xue

rest WIP

parent 3f42d582
......@@ -147,54 +147,16 @@ While not required, it is recommended that you develop your RESTful APIs as an a
your Web front end and back end.
Adding or Removing Endpoints
----------------------------
Creating Resource Classes
-------------------------
As explained above, controllers and actions are used to implement API endpoints.
RESTful APIs are all about accessing and manipulating resources. In Yii, a resource can be an object of any class.
However, if your resource classes extend from [[yii\base\Model]] or its child classes (e.g. [[yii\db\ActiveRecord]]),
you may enjoy the following benefits:
To add an API endpoint servicing a new kind of model (resource), create a new controller class by extending
[[yii\rest\ActiveController]] or [[yii\rest\Controller]]. The difference between these two base controller
classes is that the former is a subclass of the latter and implements a commonly needed actions to deal
with ActiveRecord. The controller class should be named after the model class with the `Controller` suffix.
For example, for the `Post` model, you would create a `PostController` class.
If your new controller class extends from [[yii\rest\ActiveController]], you already have a whole set of
endpoints available out of box, as shown in the quick example. You may want to disable some of the actions
or customize them. This can be easily done by overriding the `actions()` method, like the following,
```php
public function actions()
{
$actions = parent::actions();
// disable the "delete" and "create" actions
unset($actions['delete'], $actions['create']);
// customize the data provider preparation with the "prepareDataProvider()" method
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
public function prepareDataProvider()
{
// prepare and return a data provider for the "index" action
}
```
You can certainly create new actions like you do with regular controllers. The only difference is that
instead of calling [[yii\base\Controller::render()]] to render views, you directly return the data
in your action. For example,
```php
public function actionSearch($keyword)
{
$result = SolrService::search($keyword);
return $result;
}
```
The data will be automatically formatted and sent to the client, as we will explain in the next section.
* Input data validation;
* Query, create, update and delete data, if extending from [[yii\db\ActiveRecord]];
* Customizable data formatting (to be explained in the next section).
Formatting Response Data
......@@ -203,7 +165,7 @@ Formatting Response Data
By default, Yii supports two response formats for RESTful APIs: JSON and XML. If you want to support
other formats, you should configure [[yii\rest\Controller::supportedFormats]] and also [[yii\web\Response::formatters]].
The data formatting is in general a two-step process:
Formatting response data in general involves two steps:
1. The objects (including embedded objects) in the response data are converted into arrays by [[yii\rest\Serializer]];
2. The array data are converted into different formats (e.g. JSON, XML) by [[yii\web\ResponseFormatterInterface|response formatters]].
......@@ -213,32 +175,46 @@ Step 1 involves some major development effort as explained below.
When the [[yii\rest\Serializer|serializer]] converts an object into an array, it will call the `toArray()` method
of the object if it implements [[yii\base\ArrayableInterface]]. If an object does not implement this interface,
an array consisting of all its public properties will be returned.
its public properties will be returned instead.
For classes extending from [[yii\base\Model]] or [[yii\db\ActiveRecord]], besides directly overriding `toArray()`,
you may also override the `fields()` method and/or the `extraFields()` method to customize the data to be returned.
you may also override the `fields()` method and/or the `extraFields()` method to customize the data being returned.
The method [[yii\base\Model::fields()]] declares a set of fields of an object that should be included in the result.
The default implementation returns all attributes of a model as the output fields. You can customize it to add,
remove, rename or reformat the fields. For example,
The method [[yii\base\Model::fields()]] declares a set of *fields* that should be included in the result.
A field is simply a named data item. In a result array, the array keys are the field names, and the array values
are the corresponding field values. The default implementation of [[yii\base\Model::fields()]] is to return
all attributes of a model as the output fields; for [[yii\db\ActiveRecord::fields()]], by default it will return
the names of the attributes whose values have been populated into the object.
You can override the `fields()` method to add, remove, rename or redefine fields. For example,
```php
class User extends \yii\db\ActiveRecord
// explicitly list every field, best used when you want to make sure the changes
// in your DB table or model attributes do not cause your field changes (to keep API backward compatibility).
public function fields()
{
public function fields()
{
$fields = parent::fields();
return [
// field name is the same as the attribute name
'id',
// field name is "email", the corresponding attribute name is "email_address"
'email' => 'email_address',
// field name is "name", its value is defined by a PHP callback
'name' => function () {
return $this->first_name . ' ' . $this->last_name;
},
];
}
// remove fields that contain sensitive information
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
// filter out some fields, best used when you want to inherit the parent implementation
// and blacklist some sensitive fields.
public function fields()
{
$fields = parent::fields();
// add a new field "full_name" defined as the concatenation of "first_name" and "last_name"
$fields['full_name'] = function () {
return $this->first_name . ' ' . $this->last_name;
};
// remove fields that contain sensitive information
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
return $fields;
}
```
......@@ -274,30 +250,335 @@ For example, `http://localhost/users?fields=id,email&expand=profile` may return
]
```
You may wonder who triggers the conversion from objects to arrays when an action returns an object or object collection.
The answer is that this is done by [[yii\rest\Controller::serializer]] in the [[yii\base\Controller::afterAction()|afterAction()]]
method. By default, [[yii\rest\Serializer]] is used as the serializer that can recognize resource objects extending from
[[yii\base\Model]] and collection objects implementing [[yii\data\DataProviderInterface]]. The serializer
will call the `toArray()` method of these objects and pass the `fields` and `expand` user parameters to the method.
If there are any embedded objects, they will also be converted into arrays recursively.
If all your resource objects are of [[yii\base\Model]] or its child classes, such as [[yii\db\ActiveRecord]],
and you only use [[yii\data\DataProviderInterface]] as resource collections, the default data formatting
implementation should work very well. However, if you want to introduce some new resource classes that do not
extend from [[yii\base\Model]], or if you want to use some new collection classes, you will need to
customize the serializer class and configure [[yii\rest\Controller::serializer]] to use it.
You new resource classes may use the trait [[yii\base\ArrayableTrait]] to support selective field output
as explained above.
### HATEOAS Support
TBD
Creating Controllers and Actions
--------------------------------
So you have the resource data and you have specified how the resource data should be formatted, the next thing
to do is to create controller actions to expose the resource data to end users.
Yii provides two base controller classes to simplify your work of creating RESTful actions:
[[yii\rest\Controller]] and [[yii\rest\ActiveController]]. The difference between these two controllers
is that the latter provides a default set of actions that are specified designed to deal with
resources represented as ActiveRecord. So if you are using ActiveRecord and you are comfortable with
the provided built-in actions, you may consider creating your controller class by extending from
the latter. Otherwise, extending from [[yii\rest\Controller]] will allow you to develop actions
from scratch.
Both [[yii\rest\Controller]] and [[yii\rest\ActiveController]] provide the following features which will
be described in detail in the next few sections:
* Response format negotiation;
* API version negotiation;
* HTTP method validation;
* User authentication;
* Rate limiting.
[[yii\rest\ActiveController]] in addition provides the following features specifically for working
with ActiveRecord:
* A set of commonly used actions: `index`, `view`, `create`, `update`, `delete`, `options`;
* User authorization in regard to the requested action and resource.
When creating a new controller class, a convention in naming the controller class is to use
the type name of the resource and use singular form. For example, to serve user information,
the controller may be named as `UserController`.
Creating a new action is similar to creating an action for a Web application. The only difference
is that instead of rendering the result using a view by calling the `render()` method, for RESTful actions
you directly return the data. The [[yii\rest\Controller::serializer|serializer]] and the
[[yii\web\Response|response object]] will handle the conversion from the original data to the requested
format. For example,
```php
public function actionSearch($keyword)
{
$result = SolrService::search($keyword);
return $result;
}
```
If your controller class extends from [[yii\rest\ActiveController]], you should set
its [[yii\rest\ActiveController::modelClass||modelClass]] property to be the name of the resource class
that you plan to serve through this controller. The class must implement [[yii\db\ActiveRecordInterface]].
With [[yii\rest\ActiveController]], you may want to disable some of the built-in actions or customize them.
To do so, override the `actions()` method like the following:
```php
public function actions()
{
$actions = parent::actions();
// disable the "delete" and "create" actions
unset($actions['delete'], $actions['create']);
// customize the data provider preparation with the "prepareDataProvider()" method
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
public function prepareDataProvider()
{
// prepare and return a data provider for the "index" action
}
```
The following list summarizes the built-in actions supported by [[yii\rest\ActiveController]]:
* [[yii\rest\IndexAction|index]]: list resources page by page;
* [[yii\rest\ViewAction|view]]: return the details of a specified resource;
* [[yii\rest\CreateAction|create]]: create a new resource;
* [[yii\rest\UpdateAction|update]]: update an existing resource;
* [[yii\rest\DeleteAction|delete]]: delete the specified resource;
* [[yii\rest\OptionsAction|options]]: return the supported HTTP methods.
Routing
-------
With resource and controller classes ready, you can access the resources using the URL like
`http://localhost/index.php?r=user/create`. As you can see, the format of the URL is the same as that
for Web applications.
In practice, you usually want to enable pretty URLs and take advantage of HTTP verbs.
For example, a request `POST /users` would mean accessing the `user/create` action.
This can be done easily by configuring the `urlManager` application component in the application
configuration like the following:
```php
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
],
]
```
Compared to the URL management for Web applications, the main new thing above is the use of
[[yii\rest\UrlRule]] for routing RESTful API requests. This special URL rule class will
create a whole set of child URL rules to support routing and URL creation for the specified controller(s).
For example, the above code is roughly equivalent to the following rules:
```php
[
'PUT,PATCH users/<id>' => 'user/update',
'DELETE users/<id>' => 'user/delete',
'GET,HEAD users/<id>' => 'user/view',
'POST users' => 'user/create',
'GET,HEAD users' => 'user/index',
'users/<id>' => 'user/options',
'users' => 'user/options',
]
```
And the following API endpoints are supported by this rule:
* `GET /users`: list all users page by page;
* `HEAD /users`: show the overview information of user listing;
* `POST /users`: create a new user;
* `GET /users/123`: return the details of the user 123;
* `HEAD /users/123`: show the overview information of user 123;
* `PATCH /users/123` and `PUT /users/123`: update the user 123;
* `DELETE /users/123`: delete the user 123;
* `OPTIONS /users`: show the supported verbs regarding endpoint `/users`;
* `OPTIONS /users/123`: show the supported verbs regarding endpoint `/users/123`.
You may configure the `only` and `except` options to explicitly list which actions to support or which
actions should be disabled, respectively. For example,
```php
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'except' => ['delete', 'create', 'update'],
],
```
You may also configure `patterns` or `extra` to redefine existing patterns or add new patterns supported by this rule.
For example, to support a new action `search` by the endpoint `GET /users/search`, configure the `extra` option as follows,
```php
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'extra' => [
'GET search' => 'search',
],
```
You may have noticed that the controller ID `user` appears in plural form as `users` in the endpoints.
This is because [[yii\rest\UrlRule]] automatically pluralizes controller IDs for them to use in endpoints.
You may disable this behavior by setting [[yii\rest\UrlRule::pluralize]] to be false, or if you want
to use some special names you may configure the [[yii\rest\UrlRule::controller]] property.
Authentication
--------------
Unlike Web applications, RESTful APIs should be stateless, which means sessions or cookies should not
be used. Therefore, each request should come with some sort of authentication credentials because
the user authentication status may not be maintained by sessions or cookies. A common practice is
to send a secret access token with each request to authenticate the user. Since an access token
can be used to uniquely identify and authenticate a user, **the API requests should always be sent
via HTTPS to prevent from man-in-the-middle (MitM) attacks**.
There are different ways to send an access token:
* [HTTP Basic Auth](http://en.wikipedia.org/wiki/Basic_access_authentication): the access token
is sent as the username. This is should only be used when an access token can be safely stored
on the API consumer side. For example, the API consumer is a program running on a server.
* Query parameter: the access token is sent as a query parameter in the API URL, e.g.,
`https://example.com/users?access-token=xxxxxxxx`. Because most Web servers will keep query
parameters in server logs, this approach should be mainly used to serve `JSONP` requests which
cannot use HTTP headers to send access tokens.
* [OAuth 2](http://oauth.net/2/): the access token is obtained by the consumer from an authorization
server and sent to the API server via [HTTP Bearer Tokens](http://tools.ietf.org/html/rfc6750),
according to the OAuth2 protocol.
Yii supports all of the above authentication methods and can be further extended to support other methods.
To enable authentication for your APIs, do the following two steps:
1. Configure [[yii\rest\Controller::authMethods]] with the authentication methods you plan to use.
2. Implement [[yii\web\IdentityInterface::findIdentityByAccessToken()]] in your [[yii\web\User::identityClass|user identity class]].
For example, to enable all three authentication methods explained above, you would configure `authMethods`
as follows,
```php
class UserController extends ActiveController
{
public $authMethods = [
'yii\rest\HttpBasicAuth',
'yii\rest\QueryParamAuth',
'yii\rest\HttpBearerAuth',
];
}
```
Each element in `authMethods` should be an auth class name or a configuration array. An auth class
must implement [[yii\rest\AuthInterface]].
Implementation of `findIdentityByAccessToken()` is application specific. For example, in simple scenarios
when each user can only have one access token, you may store the access token in an `access_token` column
in the user table. The method can then be readily implemented in the `User` class as follows,
```php
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
class User extends ActiveRecord implements IdentityInterface
{
public static function findIdentityByAccessToken($token)
{
return static::find(['access_token' => $token]);
}
}
```
After authentication is enabled as described above, for every API request, the requested controller
will try to authenticate the user in its `beforeAction()` step.
If authentication succeeds, the controller will perform other checks (such as rate limiting, authorization)
and then run the action. The authenticated user identity information can be retrieved via `Yii::$app->user->identity`.
If authentication fails, a response with HTTP status 401 will be sent back together with other appropriate headers
(such as a `WWW-Authenticate` header for HTTP Basic Auth).
Authorization
-------------
After a user is authenticated, you probably want to check if he has the permission to perform the requested
action for the requested resource. This process is called *authorization* which is covered in detail in
the [Authorization chapter](authorization.md).
Versioning
----------
You may use the [[yii\web\AccessControl]] filter and/or the Role-Based Access Control (RBAC) component
to implementation authorization.
To simplify the authorization check, you may also override the [[yii\rest\Controller::checkAccess()]] method
and then call this method in places where authorization is needed. By default, the built-in actions provided
by [[yii\rest\ActiveController]] will call this method when they are about to run.
Caching
-------
```php
/**
* Checks the privilege of the current user.
*
* This method should be overridden to check whether the current user has the privilege
* to run the specified action against the specified data model.
* If the user does not have access, a [[ForbiddenHttpException]] should be thrown.
*
* @param string $action the ID of the action to be executed
* @param \yii\base\Model $model the model to be accessed. If null, it means no specific model is being accessed.
* @param array $params additional parameters
* @throws ForbiddenHttpException if the user does not have access
*/
public function checkAccess($action, $model = null, $params = [])
{
}
```
Rate Limiting
-------------
To prevent abuse, you should consider adding rate limiting to your APIs. For example, you may limit the API usage
of each user to be at most 100 API calls within a period of 10 minutes. If too many requests are received from a user
within the period of the time, a response with status code 429 (meaning Too Many Requests) should be returned.
To enable rate limiting, the [[yii\web\User::identityClass|user identity class]] should implement [[yii\rest\RateLimitInterface]].
This interface requires implementation of the following three methods:
* `getRateLimit()`: returns the maximum number of allowed requests and the time period, e.g., `[100, 600]` means
at most 100 API calls within 600 seconds.
* `loadAllowance()`: returns the number of remaining requests allowed and the corresponding UNIX timestamp
when the rate limit is checked last time.
* `saveAllowance()`: saves the number of remaining requests allowed and the current UNIX timestamp.
You may use two columns in the user table to record the allowance and timestamp information.
And `loadAllowance()` and `saveAllowance()` can then be implementation by reading and saving the values
of the two columns corresponding to the current authenticated user. To improve performance, you may also
consider storing these information in cache or some NoSQL storage.
Once the identity class implements the required interface, Yii will automatically use the rate limiter
as specified by [[yii\rest\Controller::rateLimiter]] to perform rate limiting check. The rate limiter
will thrown a [[yii\web\TooManyRequestsHttpException]] if rate limit is exceeded.
When rate limiting is enabled, every response will be sent with the following HTTP headers containing
the current rate limiting information:
* `X-Rate-Limit-Limit`: The maximum number of requests allowed with a time period;
* `X-Rate-Limit-Remaining`: The number of remaining requests in the current time period;
* `X-Rate-Limit-Reset`: The number of seconds to wait in order to get the maximum number of allowed requests.
Caching
-------
Error Handling
--------------
......@@ -309,7 +590,7 @@ Error Handling
* `304`: Resource was not modified. You can use the cached version.
* `400`: Bad request. This could be caused by various reasons from the user side, such as invalid JSON
data in the request body, invalid action parameters, etc.
* `401`: No valid API access token is provided.
* `401`: Authentication failed.
* `403`: The authenticated user is not allowed to access the specified API endpoint.
* `404`: The requested resource does not exist.
* `405`: Method not allowed. Please check the `Allow` header for allowed HTTP methods.
......@@ -319,6 +600,10 @@ Error Handling
* `500`: Internal server error. This could be caused by internal program errors.
Versioning
----------
Documentation
-------------
......
......@@ -9,7 +9,6 @@ namespace yii\rest;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\web\ForbiddenHttpException;
/**
* ActiveController implements a common set of actions for supporting RESTful access to ActiveRecord.
......@@ -124,20 +123,4 @@ class ActiveController extends Controller
'delete' => ['DELETE'],
];
}
/**
* Checks the privilege of the current user.
*
* This method should be overridden to check whether the current user has the privilege
* to run the specified action against the specified data model.
* If the user does not have access, a [[ForbiddenHttpException]] should be thrown.
*
* @param \yii\base\Action $action the action to be executed
* @param \yii\base\Model $model the model to be accessed. If null, it means no specific model is being accessed.
* @param array $params additional parameters
* @throws ForbiddenHttpException if the user does not have access
*/
public function checkAccess($action, $model = null, $params = [])
{
}
}
......@@ -14,6 +14,7 @@ use yii\web\UnauthorizedHttpException;
use yii\web\UnsupportedMediaTypeHttpException;
use yii\web\TooManyRequestsHttpException;
use yii\web\VerbFilter;
use yii\web\ForbiddenHttpException;
/**
* Controller is the base class for RESTful API controller classes.
......@@ -227,4 +228,20 @@ class Controller extends \yii\web\Controller
{
return Yii::createObject($this->serializer)->serialize($data);
}
/**
* Checks the privilege of the current user.
*
* This method should be overridden to check whether the current user has the privilege
* to run the specified action against the specified data model.
* If the user does not have access, a [[ForbiddenHttpException]] should be thrown.
*
* @param string $action the ID of the action to be executed
* @param object $model the model to be accessed. If null, it means no specific model is being accessed.
* @param array $params additional parameters
* @throws ForbiddenHttpException if the user does not have access
*/
public function checkAccess($action, $model = null, $params = [])
{
}
}
......@@ -41,7 +41,7 @@ class CreateAction extends Action
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this);
call_user_func($this->checkAccess, $this->id);
}
/**
......
......@@ -32,7 +32,7 @@ class DeleteAction extends Action
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this, $model);
call_user_func($this->checkAccess, $this->id, $model);
}
if ($this->transactional && $model instanceof ActiveRecord) {
......
......@@ -38,7 +38,7 @@ class IndexAction extends Action
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this);
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
......
......@@ -41,7 +41,7 @@ class UpdateAction extends Action
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this, $model);
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
......
......@@ -93,6 +93,12 @@ class UrlRule extends CompositeUrlRule
*/
public $except = [];
/**
* @var array patterns for supporting extra actions in addition to those listed in [[patterns]].
* The keys are the patterns and the values are the corresponding action IDs.
* These extra patterns will take precedence over [[patterns]].
*/
public $extra = [];
/**
* @var array list of tokens that should be replaced for each pattern. The keys are the token names,
* and the values are the corresponding replacements.
* @see patterns
......@@ -117,9 +123,19 @@ class UrlRule extends CompositeUrlRule
'{id}' => 'options',
'' => 'options',
];
/**
* @var array the default configuration for creating each URL rule contained by this rule.
*/
public $ruleConfig = [
'class' => 'yii\web\UrlRule',
];
/**
* @var boolean whether to automatically pluralize the URL names for controllers.
* If true, a controller ID will appear in plural form in URLs. For example, `user` controller
* will appear as `users` in URLs.
* @see controllers
*/
public $pluralize = true;
/**
......@@ -134,7 +150,7 @@ class UrlRule extends CompositeUrlRule
$controllers = [];
foreach ((array)$this->controller as $urlName => $controller) {
if (is_integer($urlName)) {
$urlName = Inflector::pluralize($controller);
$urlName = $this->pluralize ? Inflector::pluralize($controller) : $controller;
}
$controllers[$urlName] = $controller;
}
......@@ -152,10 +168,11 @@ class UrlRule extends CompositeUrlRule
{
$only = array_flip($this->only);
$except = array_flip($this->except);
$patterns = array_merge($this->patterns, $this->extra);
$rules = [];
foreach ($this->controller as $urlName => $controller) {
$prefix = trim($this->prefix . '/' . $urlName, '/');
foreach ($this->patterns as $pattern => $action) {
foreach ($patterns as $pattern => $action) {
if (!isset($except[$action]) && (empty($only) || isset($only[$action]))) {
$rules[$urlName][] = $this->createRule($pattern, $prefix, $controller . '/' . $action);
}
......
......@@ -26,7 +26,7 @@ class ViewAction extends Action
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this, $model);
call_user_func($this->checkAccess, $this->id, $model);
}
return $model;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment