Commit 2c1bd7f2 by Qiang Xue

Merge pull request #2599 from yiisoft/feature-restapi

RESTful API support
parents 5433aecc 42ba8fe6
<?php
namespace common\models;
use yii\base\NotSupportedException;
use yii\db\ActiveRecord;
use yii\helpers\Security;
use yii\web\IdentityInterface;
......@@ -72,6 +73,14 @@ class User extends ActiveRecord implements IdentityInterface
}
/**
* @inheritdoc
*/
public static function findIdentityByAccessToken($token)
{
throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
}
/**
* Finds user by username
*
* @param string $username
......
......@@ -8,6 +8,7 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
public $username;
public $password;
public $authKey;
public $accessToken;
private static $users = [
'100' => [
......@@ -15,12 +16,14 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
'accessToken' => '100-token',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
'accessToken' => '101-token',
],
];
......@@ -33,6 +36,19 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
}
/**
* @inheritdoc
*/
public static function findIdentityByAccessToken($token)
{
foreach (self::$users as $user) {
if ($user['accessToken'] === $token) {
return new static($user);
}
}
return null;
}
/**
* Finds user by username
*
* @param string $username
......
......@@ -5,7 +5,7 @@ Authentication is the act of verifying who a user is, and is the basis of the lo
In Yii, this entire process is performed semi-automatically, leaving the developer to merely implement [[yii\web\IdentityInterface]], the most important class in the authentication system. Typically, implementation of `IdentityInterface` is accomplished using the `User` model.
You can find a full featured example of authentication in the
You can find a fully featured example of authentication in the
[advanced application template](installation.md). Below, only the interface methods are listed:
```php
......@@ -25,6 +25,17 @@ class User extends ActiveRecord implements IdentityInterface
}
/**
* Finds an identity by the given token.
*
* @param string $token the token to be looked for
* @return IdentityInterface|null the identity object that matches the given token.
*/
public static function findIdentityByAccessToken($token)
{
return static::find(['access_token' => $token]);
}
/**
* @return int|string current user ID
*/
public function getId()
......
Implementing RESTful Web Service APIs
=====================================
Yii provides a whole set of tools to greatly simplify the task of implementing RESTful Web Service APIs.
In particular, Yii provides support for the following aspects regarding RESTful APIs:
* Quick prototyping with support for common APIs for ActiveRecord;
* Response format (supporting JSON and XML by default) and API version negotiation;
* Customizable object serialization with support for selectable output fields;
* Proper formatting of collection data and validation errors;
* Efficient routing with proper HTTP verb check;
* Support `OPTIONS` and `HEAD` verbs;
* Authentication;
* Authorization;
* Support for HATEOAS;
* Caching via `yii\web\HttpCache`;
* Rate limiting;
* Searching and filtering: TBD
* Testing: TBD
* Automatic generation of API documentation: TBD
A Quick Example
---------------
Let's use a quick example to show how to build a set of RESTful APIs using Yii.
Assume you want to expose the user data via RESTful APIs. The user data are stored in the user DB table,
and you have already created the ActiveRecord class `app\models\User` to access the user data.
First, create a controller class `app\controllers\UserController` as follows,
```php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
}
```
Then, modify the configuration about the `urlManager` component in your application configuration:
```php
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
],
]
```
With the above minimal amount of effort, you have already finished your task of creating the RESTful APIs
for accessing the user data. The APIs you have created include:
* `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 access your APIs with the `curl` command like the following,
```
curl -i -H "Accept:application/json" "http://localhost/users"
```
which may give the following output:
```
HTTP/1.1 200 OK
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
X-Powered-By: PHP/5.4.20
X-Pagination-Total-Count: 1000
X-Pagination-Page-Count: 50
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://localhost/users?page=1>; rel=self,
<http://localhost/users?page=2>; rel=next,
<http://localhost/users?page=50>; rel=last
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8
[
{
"id": 1,
...
},
{
"id": 2,
...
},
...
]
```
Try changing the acceptable content type to be `application/xml`, and you will see the result
is returned in XML format:
```
curl -i -H "Accept:application/xml" "http://localhost/users"
```
```
HTTP/1.1 200 OK
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
X-Powered-By: PHP/5.4.20
X-Pagination-Total-Count: 1000
X-Pagination-Page-Count: 50
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://localhost/users?page=1>; rel=self,
<http://localhost/users?page=2>; rel=next,
<http://localhost/users?page=50>; rel=last
Transfer-Encoding: chunked
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<response>
<item>
<id>1</id>
...
</item>
<item>
<id>2</id>
...
</item>
...
</response>
```
> Tip: You may also access your APIs via Web browser by entering the URL `http://localhost/users`.
As you can see, in the response headers, there are information about the total count, page count, etc.
There are also links that allow you to navigate to other pages of data. For example, `http://localhost/users?page=2`
would give you the next page of the user data.
Using the `fields` and `expand` parameters, you may also request to return a subset of the fields in the result.
For example, the URL `http://localhost/users?fields=id,email` will only return the `id` and `email` fields in the result:
> Info: You may have noticed that the result of `http://localhost/users` includes some sensitive fields,
> such as `password_hash`, `auth_key`. You certainly do not want these to appear in your API result.
> You can/should filter out these fields as described in the following sections.
In the following sections, we will explain in more details about implementing RESTful APIs.
General Architecture
--------------------
Using the Yii RESTful API framework, you implement an API endpoint in terms of a controller action, and you use
a controller to organize the actions that implement the endpoints for a single type of resource.
Resources are represented as data models which extend from the [[yii\base\Model]] class.
If you are working with databases (relational or NoSQL), it is recommended you use ActiveRecord to represent resources.
You may use [[yii\rest\UrlRule]] to simplify the routing to your API endpoints.
While not required, it is recommended that you develop your RESTful APIs as an application, separated from
your Web front end and back end.
Creating Resource Classes
-------------------------
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:
* 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
------------------------
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]].
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]].
Step 2 is usually a very mechanical data conversion process and can be well handled by the built-in response formatters.
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,
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 being returned.
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
// 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()
{
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;
},
];
}
// 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();
// remove fields that contain sensitive information
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
```
The return value of `fields()` should be an array. The array keys are the field names, and the array values
are the corresponding field definitions which can be either property/attribute names or anonymous functions
returning the corresponding field values.
> Warning: Because by default all attributes of a model will be included in the API result, you should
> examine your data to make sure they do not contain sensitive information. If there is such information,
> you should override `fields()` or `toArray()` to filter them out. In the above example, we choose
> to filter out `auth_key`, `password_hash` and `password_reset_token`.
You may use the `fields` query parameter to specify which fields in `fields()` should be included in the result.
If this parameter is not specified, all fields returned by `fields()` will be returned.
The method [[yii\base\Model::extraFields()]] is very similar to [[yii\base\Model::fields()]].
The difference between these methods is that the latter declares the fields that should be returned by default,
while the former declares the fields that should only be returned when the user specifies them in the `expand` query parameter.
For example, `http://localhost/users?fields=id,email&expand=profile` may return the following JSON data:
```php
[
{
"id": 100,
"email": "100@example.com",
"profile": {
"id": 100,
"age": 30,
}
},
...
]
```
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.
### Pagination
For API endpoints about resource collections, pagination is supported out-of-box if you use
[[yii\data\DataProviderInterface|data provider]] to serve the response data. In particular,
through query parameters `page` and `per-page`, an API consumer may specify which page of data
to return and how many data items should be included in each page. The corresponding response
will include the pagination information by the following HTTP headers (please also refer to the first example
in this chapter):
* `X-Pagination-Total-Count`: The total number of data items;
* `X-Pagination-Page-Count`: The number of pages;
* `X-Pagination-Current-Page`: The current page (1-based);
* `X-Pagination-Per-Page`: The number of data items in each page;
* `Link`: A set of navigational links allowing client to traverse the data page by page.
The response body will contain a list of data items in the requested page.
Sometimes, you may want to help simplify the client development work by including pagination information
directly in the response body. To do so, configure the [[yii\rest\Serializer::collectionEnvelope]] property
as follows:
```php
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}
```
You may then get the following response for request `http://localhost/users`:
```
HTTP/1.1 200 OK
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
X-Powered-By: PHP/5.4.20
X-Pagination-Total-Count: 1000
X-Pagination-Page-Count: 50
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://localhost/users?page=1>; rel=self,
<http://localhost/users?page=2>; rel=next,
<http://localhost/users?page=50>; rel=last
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8
{
"items": [
{
"id": 1,
...
},
{
"id": 2,
...
},
...
],
"_links": {
"self": "http://localhost/users?page=1",
"next": "http://localhost/users?page=2",
"last": "http://localhost/users?page=50"
},
"_meta": {
"totalCount": 1000,
"pageCount": 50,
"currentPage": 1,
"perPage": 20
}
}
```
### HATEOAS Support
[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS), an abbreviation for Hypermedia as the Engine of Application State,
promotes that RESTful APIs should return information that allow clients to discover actions supported for the returned
resources. The key of HATEOAS is to return a set of hyperlinks with relation information when resource data are served
by APIs.
You may let your model classes to implement the [[yii\web\Linkable]] interface to support HATEOAS. By implementing
this interface, a class is required to return a list of [[yii\web\Link|links]]. Typically, you should return at least
the `self` link, for example:
```php
use yii\db\ActiveRecord;
use yii\web\Linkable;
use yii\helpers\Url;
class User extends ActiveRecord implements Linkable
{
public function getLinks()
{
return [
Link::REL_SELF => Url::action(['user', 'id' => $this->id], true),
];
}
}
```
When a `User` object is returned in a response, it will contain a `_links` element representing the links related
to the user, for example,
```
{
"id": 100,
"email": "user@example.com",
...,
"_links" => [
"self": "https://example.com/users/100"
]
}
```
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).
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.
```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.
Error Handling
--------------
When handling a RESTful API request, if there is an error in the user request or if something unexpected
happens on the server, you may simply throw an exception to notify the user something wrong happened.
If you can identify the cause of the error (e.g. the requested resource does not exist), you should
consider throwing an exception with a proper HTTP status code (e.g. [[yii\web\NotFoundHttpException]]
representing a 404 HTTP status code). Yii will send the response with the corresponding HTTP status
code and text. It will also include in the response body the serialized representation of the
exception. For example,
```
HTTP/1.1 404 Not Found
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8
{
"type": "yii\\web\\NotFoundHttpException",
"name": "Not Found Exception",
"message": "The requested resource was not found.",
"code": 0,
"status": 404
}
```
The following list summarizes the HTTP status code that are used by the Yii REST framework:
* `200`: OK. Everything worked as expected.
* `201`: A resource was successfully created in response to a `POST` request. The `Location` header
contains the URL pointing to the newly created resource.
* `204`: The request is handled successfully and the response contains no body content (like a `DELETE` request).
* `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`: 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.
* `415`: Unsupported media type. The requested content type or version number is invalid.
* `422`: Data validation failed (in response to a `POST` request, for example). Please check the response body for detailed error messages.
* `429`: Too many requests. The request is rejected due to rate limiting.
* `500`: Internal server error. This could be caused by internal program errors.
Versioning
----------
Your APIs should be versioned. Unlike Web applications which you have full control on both client side and server side
code, for APIs you usually do not have control of the client code that consumes the APIs. Therefore, backward
compatibility (BC) of the APIs should be maintained whenever possible, and if some BC-breaking changes must be
introduced to the APIs, you should bump up the version number. You may refer to [Symantic Versioning](http://semver.org/)
for more information about designing the version numbers of your APIs.
Regarding how to implement API versioning, a common practice is to embed the version number in the API URLs.
For example, `http://example.com/v1/users` stands for `/users` API of version 1. Another method of API versioning
which gains momentum recently is to put version numbers in the HTTP request headers, typically through the `Accept` header,
like the following:
```
// via a parameter
Accept: application/json; version=v1
// via a vendor content type
Accept: application/vnd.company.myapp-v1+json
```
Both methods have pros and cons, and there are a lot of debates about them. Below we describe a practical strategy
of API versioning that is a kind of mix of these two methods:
* Put each major version of API implementation in a separate module whose ID is the major version number (e.g. `v1`, `v2`).
Naturally, the API URLs will contain major version numbers.
* Within each major version (and thus within the corresponding module), use the `Accept` HTTP request header
to determine the minor version number and write conditional code to respond to the minor versions accordingly.
For each module serving a major version, it should include the resource classes and the controller classes
serving for that specific version. To better separate code responsibility, you may keep a common set of
base resource and controller classes, and subclass them in each individual version module. Within the subclasses,
implement the concrete code such as `Model::fields()`. As a result, your code may be organized like the following:
```
api/
common/
controllers/
UserController.php
PostController.php
models/
User.php
Post.php
modules/
v1/
controllers/
UserController.php
PostController.php
models/
User.php
Post.php
v2/
controllers/
UserController.php
PostController.php
models/
User.php
Post.php
```
Your application configuration would look like:
```php
return [
'modules' => [
'v1' => [
'basePath' => '@app/modules/v1',
],
'v2' => [
'basePath' => '@app/modules/v2',
],
],
'components' => [
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => ['v1/user', 'v1/post']],
['class' => 'yii\rest\UrlRule', 'controller' => ['v2/user', 'v2/post']],
],
],
],
];
```
As a result, `http://example.com/v1/users` will return the list of users in version 1, while
`http://example.com/v2/users` will return version 2 users.
Using modules, code for different major versions can be well isolated. And it is still possible
to reuse code across modules via common base classes and other shared classes.
To deal with minor version numbers, you may take advantage of the content type negotiation
feature provided by [[yii\rest\Controller]]:
* Specify a list of supported minor versions (within the major version of the containing module)
via [[yii\rest\Controller::supportedVersions]].
* Get the version number by reading [[yii\rest\Controller::version]].
* In relevant code, such as actions, resource classes, serializers, etc., write conditional
code according to the requested minor version number.
Since minor versions require maintaining backward compatibility, hopefully there are not much
version checks in your code. Otherwise, chances are that you may need to create a new major version.
Caching
-------
Documentation
-------------
Testing
-------
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
use Yii;
use yii\helpers\ArrayHelper;
use yii\web\Link;
use yii\web\Linkable;
/**
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
trait ArrayableTrait
{
/**
* Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified.
*
* A field is a named element in the returned array by [[toArray()]].
*
* This method should return an array of field names or field definitions.
* If the former, the field name will be treated as an object property name whose value will be used
* as the field value. If the latter, the array key should be the field name while the array value should be
* the corresponding field definition which can be either an object property name or a PHP callable
* returning the corresponding field value. The signature of the callable should be:
*
* ```php
* function ($field, $model) {
* // return field value
* }
* ```
*
* For example, the following code declares four fields:
*
* - `email`: the field name is the same as the property name `email`;
* - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their
* values are obtained from the `first_name` and `last_name` properties;
* - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name`
* and `last_name`.
*
* ```php
* return [
* 'email',
* 'firstName' => 'first_name',
* 'lastName' => 'last_name',
* 'fullName' => function () {
* return $this->first_name . ' ' . $this->last_name;
* },
* ];
* ```
*
* In this method, you may also want to return different lists of fields based on some context
* information. For example, depending on the privilege of the current application user,
* you may return different sets of visible fields or filter out some fields.
*
* The default implementation of this method returns the public object member variables.
*
* @return array the list of field names or field definitions.
* @see toArray()
*/
public function fields()
{
$fields = array_keys(Yii::getObjectVars($this));
return array_combine($fields, $fields);
}
/**
* Returns the list of fields that can be expanded further and returned by [[toArray()]].
*
* This method is similar to [[fields()]] except that the list of fields returned
* by this method are not returned by default by [[toArray()]]. Only when field names
* to be expanded are explicitly specified when calling [[toArray()]], will their values
* be exported.
*
* The default implementation returns an empty array.
*
* You may override this method to return a list of expandable fields based on some context information
* (e.g. the current application user).
*
* @return array the list of expandable field names or field definitions. Please refer
* to [[fields()]] on the format of the return value.
* @see toArray()
* @see fields()
*/
public function extraFields()
{
return [];
}
/**
* Converts the model into an array.
*
* This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]].
* It will then turn the model into an array with these fields. If `$recursive` is true,
* any embedded objects will also be converted into arrays.
*
* If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element
* which refers to a list of links as specified by the interface.
*
* @param array $fields the fields being requested. If empty, all fields as specified by [[fields()]] will be returned.
* @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]]
* will be considered.
* @param boolean $recursive whether to recursively return array representation of embedded objects.
* @return array the array representation of the object
*/
public function toArray(array $fields = [], array $expand = [], $recursive = true)
{
$data = [];
foreach ($this->resolveFields($fields, $expand) as $field => $definition) {
$data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $field, $this);
}
if ($this instanceof Linkable) {
$data['_links'] = Link::serialize($this->getLinks());
}
return $recursive ? ArrayHelper::toArray($data) : $data;
}
/**
* Determines which fields can be returned by [[toArray()]].
* This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]]
* to determine which fields can be returned.
* @param array $fields the fields being requested for exporting
* @param array $expand the additional fields being requested for exporting
* @return array the list of fields to be exported. The array keys are the field names, and the array values
* are the corresponding object property names or PHP callables returning the field values.
*/
protected function resolveFields(array $fields, array $expand)
{
$result = [];
foreach ($this->fields() as $field => $definition) {
if (is_integer($field)) {
$field = $definition;
}
if (empty($fields) || in_array($field, $fields, true)) {
$result[$field] = $definition;
}
}
if (empty($expand)) {
return $result;
}
foreach ($this->extraFields() as $field => $definition) {
if (is_integer($field)) {
$field = $definition;
}
if (in_array($field, $expand, true)) {
$result[$field] = $definition;
}
}
return $result;
}
}
......@@ -93,10 +93,9 @@ class ErrorHandler extends Component
return;
}
$useErrorView = !YII_DEBUG || $exception instanceof UserException;
$response = Yii::$app->getResponse();
$response->getHeaders()->removeAll();
$useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
if ($useErrorView && $this->errorAction !== null) {
$result = Yii::$app->runAction($this->errorAction);
......@@ -121,7 +120,7 @@ class ErrorHandler extends Component
]);
}
} elseif ($exception instanceof Arrayable) {
$response->data = $exception;
$response->data = $exception->toArray();
} else {
$response->data = [
'type' => get_class($exception),
......
......@@ -13,9 +13,12 @@ use ArrayObject;
use ArrayIterator;
use ReflectionClass;
use IteratorAggregate;
use yii\helpers\ArrayHelper;
use yii\helpers\Inflector;
use yii\validators\RequiredValidator;
use yii\validators\Validator;
use yii\web\Link;
use yii\web\Linkable;
/**
* Model is the base class for data models.
......@@ -54,11 +57,12 @@ use yii\validators\Validator;
*/
class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable
{
use ArrayableTrait;
/**
* The name of the default scenario.
*/
const SCENARIO_DEFAULT = 'default';
/**
* @event ModelEvent an event raised at the beginning of [[validate()]]. You may set
* [[ModelEvent::isValid]] to be false to stop the validation.
......@@ -516,7 +520,8 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
/**
* Returns the first error of every attribute in the model.
* @return array the first errors. An empty array will be returned if there is no error.
* @return array the first errors. The array keys are the attribute names, and the array
* values are the corresponding error messages. An empty array will be returned if there is no error.
* @see getErrors()
* @see getFirstError()
*/
......@@ -526,13 +531,13 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
return [];
} else {
$errors = [];
foreach ($this->_errors as $attributeErrors) {
if (isset($attributeErrors[0])) {
$errors[] = $attributeErrors[0];
foreach ($this->_errors as $name => $es) {
if (!empty($es)) {
$errors[$name] = reset($es);
}
}
return $errors;
}
return $errors;
}
/**
......@@ -789,13 +794,92 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
}
/**
* Converts the object into an array.
* The default implementation will return [[attributes]].
* @return array the array representation of the object
* Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified.
*
* A field is a named element in the returned array by [[toArray()]].
*
* This method should return an array of field names or field definitions.
* If the former, the field name will be treated as an object property name whose value will be used
* as the field value. If the latter, the array key should be the field name while the array value should be
* the corresponding field definition which can be either an object property name or a PHP callable
* returning the corresponding field value. The signature of the callable should be:
*
* ```php
* function ($field, $model) {
* // return field value
* }
* ```
*
* For example, the following code declares four fields:
*
* - `email`: the field name is the same as the property name `email`;
* - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their
* values are obtained from the `first_name` and `last_name` properties;
* - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name`
* and `last_name`.
*
* ```php
* return [
* 'email',
* 'firstName' => 'first_name',
* 'lastName' => 'last_name',
* 'fullName' => function () {
* return $this->first_name . ' ' . $this->last_name;
* },
* ];
* ```
*
* In this method, you may also want to return different lists of fields based on some context
* information. For example, depending on [[scenario]] or the privilege of the current application user,
* you may return different sets of visible fields or filter out some fields.
*
* The default implementation of this method returns [[attributes()]] indexed by the same attribute names.
*
* @return array the list of field names or field definitions.
* @see toArray()
*/
public function fields()
{
$fields = $this->attributes();
return array_combine($fields, $fields);
}
/**
* Determines which fields can be returned by [[toArray()]].
* This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]]
* to determine which fields can be returned.
* @param array $fields the fields being requested for exporting
* @param array $expand the additional fields being requested for exporting
* @return array the list of fields to be exported. The array keys are the field names, and the array values
* are the corresponding object property names or PHP callables returning the field values.
*/
public function toArray()
protected function resolveFields(array $fields, array $expand)
{
return $this->getAttributes();
$result = [];
foreach ($this->fields() as $field => $definition) {
if (is_integer($field)) {
$field = $definition;
}
if (empty($fields) || in_array($field, $fields, true)) {
$result[$field] = $definition;
}
}
if (empty($expand)) {
return $result;
}
foreach ($this->extraFields() as $field => $definition) {
if (is_integer($field)) {
$field = $definition;
}
if (in_array($field, $expand, true)) {
$result[$field] = $definition;
}
}
return $result;
}
/**
......
......@@ -359,6 +359,9 @@ class Module extends Component
return $this->_modules[$id];
} elseif ($load) {
Yii::trace("Loading module: $id", __METHOD__);
if (is_array($this->_modules[$id]) && !isset($this->_modules[$id]['class'])) {
$this->_modules[$id]['class'] = 'yii\base\Module';
}
return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this);
}
}
......
......@@ -8,7 +8,10 @@
namespace yii\data;
use Yii;
use yii\base\Arrayable;
use yii\base\Object;
use yii\web\Link;
use yii\web\Linkable;
use yii\web\Request;
/**
......@@ -65,9 +68,8 @@ use yii\web\Request;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Pagination extends Object
class Pagination extends Object implements Linkable, Arrayable
{
const LINK_SELF = 'self';
const LINK_NEXT = 'next';
const LINK_PREV = 'prev';
const LINK_FIRST = 'first';
......@@ -301,7 +303,7 @@ class Pagination extends Object
$currentPage = $this->getPage();
$pageCount = $this->getPageCount();
$links = [
self::LINK_SELF => $this->createUrl($currentPage, $absolute),
Link::REL_SELF => $this->createUrl($currentPage, $absolute),
];
if ($currentPage > 0) {
$links[self::LINK_FIRST] = $this->createUrl(0, $absolute);
......@@ -315,6 +317,19 @@ class Pagination extends Object
}
/**
* @inheritdoc
*/
public function toArray()
{
return [
'totalCount' => $this->totalCount,
'pageCount' => $this->getPageCount(),
'currentPage' => $this->getPage(),
'perPage' => $this->getPageSize(),
];
}
/**
* Returns the value of the specified query parameter.
* This method returns the named parameter value from [[params]]. Null is returned if the value does not exist.
* @param string $name the parameter name
......
......@@ -1347,4 +1347,26 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
return $this->generateAttributeLabel($attribute);
}
/**
* @inheritdoc
*
* The default implementation returns the names of the columns whose values have been populated into this record.
*/
public function fields()
{
$fields = array_keys($this->_attributes);
return array_combine($fields, $fields);
}
/**
* @inheritdoc
*
* The default implementation returns the names of the relations that have been populated into this record.
*/
public function extraFields()
{
$fields = array_keys($this->getRelatedRecords());
return array_combine($fields, $fields);
}
}
......@@ -58,35 +58,42 @@ class BaseArrayHelper
*/
public static function toArray($object, $properties = [], $recursive = true)
{
if (!empty($properties) && is_object($object)) {
$className = get_class($object);
if (!empty($properties[$className])) {
$result = [];
foreach ($properties[$className] as $key => $name) {
if (is_int($key)) {
$result[$name] = $object->$name;
} else {
$result[$key] = static::getValue($object, $name);
if (is_array($object)) {
if ($recursive) {
foreach ($object as $key => $value) {
if (is_array($value) || is_object($value)) {
$object[$key] = static::toArray($value, true);
}
}
return $result;
}
}
if ($object instanceof Arrayable) {
$object = $object->toArray();
if (!$recursive) {
return $object;
return $object;
} elseif (is_object($object)) {
if (!empty($properties)) {
$className = get_class($object);
if (!empty($properties[$className])) {
$result = [];
foreach ($properties[$className] as $key => $name) {
if (is_int($key)) {
$result[$name] = $object->$name;
} else {
$result[$key] = static::getValue($object, $name);
}
}
return $recursive ? static::toArray($result) : $result;
}
}
}
$result = [];
foreach ($object as $key => $value) {
if ($recursive && (is_array($value) || is_object($value))) {
$result[$key] = static::toArray($value, $properties, true);
if ($object instanceof Arrayable) {
$result = $object->toArray();
} else {
$result[$key] = $value;
$result = [];
foreach ($object as $key => $value) {
$result[$key] = $value;
}
}
return $recursive ? static::toArray($result) : $result;
} else {
return [$object];
}
return $result;
}
/**
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecordInterface;
use yii\web\NotFoundHttpException;
/**
* Action is the base class for action classes that implement RESTful API.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Action extends \yii\base\Action
{
/**
* @var string class name of the model which will be handled by this action.
* The model class must implement [[ActiveRecordInterface]].
* This property must be set.
*/
public $modelClass;
/**
* @var callable a PHP callable that will be called to return the model corresponding
* to the specified primary key value. If not set, [[findModel()]] will be used instead.
* The signature of the callable should be:
*
* ```php
* function ($id, $action) {
* // $id is the primary key value. If composite primary key, the key values
* // will be separated by comma.
* // $action is the action object currently running
* }
* ```
*
* The callable should return the model found, or throw an exception if not found.
*/
public $findModel;
/**
* @var callable a PHP callable that will be called when running an action to determine
* if the current user has the permission to execute the action. If not set, the access
* check will not be performed. The signature of the callable should be as follows,
*
* ```php
* function ($action, $model = null) {
* // $model is the requested model instance.
* // If null, it means no specific model (e.g. IndexAction)
* }
* ```
*/
public $checkAccess;
/**
* @inheritdoc
*/
public function init()
{
if ($this->modelClass === null) {
throw new InvalidConfigException(get_class($this) . '::$modelClass must be set.');
}
}
/**
* Returns the data model based on the primary key given.
* If the data model is not found, a 404 HTTP exception will be raised.
* @param string $id the ID of the model to be loaded. If the model has a composite primary key,
* the ID must be a string of the primary key values separated by commas.
* The order of the primary key values should follow that returned by the `primaryKey()` method
* of the model.
* @return ActiveRecordInterface the model found
* @throws NotFoundHttpException if the model cannot be found
*/
public function findModel($id)
{
if ($this->findModel !== null) {
return call_user_func($this->findModel, $id, $this);
}
/**
* @var ActiveRecordInterface $modelClass
*/
$modelClass = $this->modelClass;
$keys = $modelClass::primaryKey();
if (count($keys) > 1) {
$values = explode(',', $id);
if (count($keys) === count($values)) {
$model = $modelClass::find(array_combine($keys, $values));
}
} elseif ($id !== null) {
$model = $modelClass::find($id);
}
if (isset($model)) {
return $model;
} else {
throw new NotFoundHttpException("Object not found: $id");
}
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use yii\base\InvalidConfigException;
use yii\base\Model;
/**
* ActiveController implements a common set of actions for supporting RESTful access to ActiveRecord.
*
* The class of the ActiveRecord should be specified via [[modelClass]], which must implement [[\yii\db\ActiveRecordInterface]].
* By default, the following actions are supported:
*
* - `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
*
* You may disable some of these actions by overriding [[actions()]] and unsetting the corresponding actions.
*
* To add a new action, either override [[actions()]] by appending a new action class or write a new action method.
* Make sure you also override [[verbs()]] to properly declare what HTTP methods are allowed by the new action.
*
* You should usually override [[checkAccess()]] to check whether the current user has the privilege to perform
* the specified action against the specified model.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ActiveController extends Controller
{
/**
* @var string the model class name. This property must be set.
*/
public $modelClass;
/**
* @var string the scenario used for updating a model.
* @see \yii\base\Model::scenarios()
*/
public $updateScenario = Model::SCENARIO_DEFAULT;
/**
* @var string the scenario used for creating a model.
* @see \yii\base\Model::scenarios()
*/
public $createScenario = Model::SCENARIO_DEFAULT;
/**
* @var boolean whether to use a DB transaction when creating, updating or deleting a model.
* This property is only useful for relational database.
*/
public $transactional = true;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if ($this->modelClass === null) {
throw new InvalidConfigException('The "modelClass" property must be set.');
}
}
/**
* @inheritdoc
*/
public function actions()
{
return [
'index' => [
'class' => 'yii\rest\IndexAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'view' => [
'class' => 'yii\rest\ViewAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'create' => [
'class' => 'yii\rest\CreateAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->createScenario,
'transactional' => $this->transactional,
],
'update' => [
'class' => 'yii\rest\UpdateAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->updateScenario,
'transactional' => $this->transactional,
],
'delete' => [
'class' => 'yii\rest\DeleteAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'transactional' => $this->transactional,
],
'options' => [
'class' => 'yii\rest\OptionsAction',
],
];
}
/**
* @inheritdoc
*/
protected function verbs()
{
return [
'index' => ['GET', 'HEAD'],
'view' => ['GET', 'HEAD'],
'create' => ['POST'],
'update' => ['PUT', 'PATCH'],
'delete' => ['DELETE'],
];
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use yii\web\User;
use yii\web\Request;
use yii\web\Response;
use yii\web\IdentityInterface;
use yii\web\UnauthorizedHttpException;
/**
* AuthInterface is the interface required by classes that support user authentication.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
interface AuthInterface
{
/**
* Authenticates the current user.
*
* @param User $user
* @param Request $request
* @param Response $response
* @return IdentityInterface the authenticated user identity. If authentication information is not provided, null will be returned.
* @throws UnauthorizedHttpException if authentication information is provided but is invalid.
*/
public function authenticate($user, $request, $response);
/**
* Handles authentication failure.
* The implementation should normally throw UnauthorizedHttpException to indicate authentication failure.
* @param Response $response
* @throws UnauthorizedHttpException
*/
public function handleFailure($response);
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\InvalidConfigException;
use yii\web\Response;
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.
*
* Controller implements the following steps in a RESTful API request handling cycle:
*
* 1. Resolving response format and API version number (see [[supportedFormats]], [[supportedVersions]] and [[version]]);
* 2. Validating request method (see [[verbs()]]).
* 3. Authenticating user (see [[authenticate()]]);
* 4. Formatting response data (see [[serializeData()]]).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Controller extends \yii\web\Controller
{
/**
* @var string the name of the header parameter representing the API version number.
*/
public $versionHeaderParam = 'version';
/**
* @var string|array the configuration for creating the serializer that formats the response data.
*/
public $serializer = 'yii\rest\Serializer';
/**
* @inheritdoc
*/
public $enableCsrfValidation = false;
/**
* @var array the supported authentication methods. This property should take a list of supported
* authentication methods, each represented by an authentication class or configuration.
* If this is not set or empty, it means authentication is disabled.
*/
public $authMethods;
/**
* @var string|array the rate limiter class or configuration. If this is not set or empty,
* the rate limiting will be disabled. Note that if the user is not authenticated, the rate limiting
* will also NOT be performed.
* @see checkRateLimit()
* @see authMethods
*/
public $rateLimiter = 'yii\rest\RateLimiter';
/**
* @var string the chosen API version number, or null if [[supportedVersions]] is empty.
* @see supportedVersions
*/
public $version;
/**
* @var array list of supported API version numbers. If the current request does not specify a version
* number, the first element will be used as the [[version|chosen version number]]. For this reason, you should
* put the latest version number at the first. If this property is empty, [[version]] will not be set.
*/
public $supportedVersions = [];
/**
* @var array list of supported response formats. The array keys are the requested content MIME types,
* and the array values are the corresponding response formats. The first element will be used
* as the response format if the current request does not specify a content type.
*/
public $supportedFormats = [
'application/json' => Response::FORMAT_JSON,
'application/xml' => Response::FORMAT_XML,
];
/**
* @inheritdoc
*/
public function behaviors()
{
return [
'verbFilter' => [
'class' => VerbFilter::className(),
'actions' => $this->verbs(),
],
];
}
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->resolveFormatAndVersion();
}
/**
* @inheritdoc
*/
public function beforeAction($action)
{
if (parent::beforeAction($action)) {
$this->authenticate();
$this->checkRateLimit($action);
return true;
} else {
return false;
}
}
/**
* @inheritdoc
*/
public function afterAction($action, $result)
{
$result = parent::afterAction($action, $result);
return $this->serializeData($result);
}
/**
* Resolves the response format and the API version number.
* @throws UnsupportedMediaTypeHttpException
*/
protected function resolveFormatAndVersion()
{
$this->version = empty($this->supportedVersions) ? null : reset($this->supportedVersions);
Yii::$app->getResponse()->format = reset($this->supportedFormats);
$types = Yii::$app->getRequest()->getAcceptableContentTypes();
if (empty($types)) {
$types['*/*'] = [];
}
foreach ($types as $type => $params) {
if (isset($this->supportedFormats[$type])) {
Yii::$app->getResponse()->format = $this->supportedFormats[$type];
if (isset($params[$this->versionHeaderParam])) {
if (in_array($params[$this->versionHeaderParam], $this->supportedVersions, true)) {
$this->version = $params[$this->versionHeaderParam];
} else {
throw new UnsupportedMediaTypeHttpException('You are requesting an invalid version number.');
}
}
return;
}
}
if (!isset($types['*/*'])) {
throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.');
}
}
/**
* Declares the allowed HTTP verbs.
* Please refer to [[VerbFilter::actions]] on how to declare the allowed verbs.
* @return array the allowed HTTP verbs.
*/
protected function verbs()
{
return [];
}
/**
* Authenticates the user.
* This method implements the user authentication based on an access token sent through the `Authorization` HTTP header.
* @throws UnauthorizedHttpException if the user is not authenticated successfully
*/
protected function authenticate()
{
if (empty($this->authMethods)) {
return;
}
$user = Yii::$app->getUser();
$request = Yii::$app->getRequest();
$response = Yii::$app->getResponse();
foreach ($this->authMethods as $i => $auth) {
$this->authMethods[$i] = $auth = Yii::createObject($auth);
if (!$auth instanceof AuthInterface) {
throw new InvalidConfigException(get_class($auth) . ' must implement yii\rest\AuthInterface');
} elseif ($auth->authenticate($user, $request, $response) !== null) {
return;
}
}
/** @var AuthInterface $auth */
$auth = reset($this->authMethods);
$auth->handleFailure($response);
}
/**
* Ensures the rate limit is not exceeded.
*
* This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check,
* the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must
* implement [[RateLimitInterface]].
*
* @param \yii\base\Action $action the action to be executed
* @throws TooManyRequestsHttpException if the rate limit is exceeded.
*/
protected function checkRateLimit($action)
{
if (empty($this->rateLimiter)) {
return;
}
$identity = Yii::$app->getUser()->getIdentity(false);
if ($identity instanceof RateLimitInterface) {
/** @var RateLimiter $rateLimiter */
$rateLimiter = Yii::createObject($this->rateLimiter);
$rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action);
}
}
/**
* Serializes the specified data.
* The default implementation will create a serializer based on the configuration given by [[serializer]].
* It then uses the serializer to serialize the given data.
* @param mixed $data the data to be serialized
* @return mixed the serialized data.
*/
protected function serializeData($data)
{
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 = [])
{
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
/**
* CreateAction implements the API endpoint for creating a new model from the given data.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class CreateAction extends Action
{
/**
* @var string the scenario to be assigned to the new model before it is validated and saved.
*/
public $scenario = Model::SCENARIO_DEFAULT;
/**
* @var boolean whether to start a DB transaction when saving the model.
*/
public $transactional = true;
/**
* @var string the name of the view action. This property is need to create the URL when the mode is successfully created.
*/
public $viewAction = 'view';
/**
* Creates a new model.
* @return \yii\db\ActiveRecordInterface the model newly created
* @throws \Exception if there is any error when creating the model
*/
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
/**
* @var \yii\db\ActiveRecord $model
*/
$model = new $this->modelClass([
'scenario' => $this->scenario,
]);
$model->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($this->transactional && $model instanceof ActiveRecord) {
if ($model->validate()) {
$transaction = $model->getDb()->beginTransaction();
try {
$model->insert(false);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollback();
throw $e;
}
}
} else {
$model->save();
}
if (!$model->hasErrors()) {
$response = Yii::$app->getResponse();
$response->setStatusCode(201);
$id = implode(',', array_values($model->getPrimaryKey(true)));
$response->getHeaders()->set('Location', $this->controller->createAbsoluteUrl([$this->viewAction, 'id' => $id]));
}
return $model;
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\db\ActiveRecord;
/**
* DeleteAction implements the API endpoint for deleting a model.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class DeleteAction extends Action
{
/**
* @var boolean whether to start a DB transaction when deleting the model.
*/
public $transactional = true;
/**
* Deletes a model.
*/
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
if ($this->transactional && $model instanceof ActiveRecord) {
$transaction = $model->getDb()->beginTransaction();
try {
$model->delete();
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollback();
throw $e;
}
} else {
$model->delete();
}
Yii::$app->getResponse()->setStatusCode(204);
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\Component;
use yii\web\UnauthorizedHttpException;
/**
* HttpBasicAuth implements the HTTP Basic authentication method.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HttpBasicAuth extends Component implements AuthInterface
{
/**
* @var string the HTTP authentication realm
*/
public $realm = 'api';
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
if (($accessToken = $request->getAuthUser()) !== null) {
$identity = $user->loginByAccessToken($accessToken);
if ($identity !== null) {
return $identity;
}
$this->handleFailure($response);
}
return null;
}
/**
* @inheritdoc
*/
public function handleFailure($response)
{
$response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\"");
throw new UnauthorizedHttpException('You are requesting with an invalid access token.');
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\Component;
use yii\web\UnauthorizedHttpException;
/**
* HttpBearerAuth implements the authentication method based on HTTP Bearer token.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HttpBearerAuth extends Component implements AuthInterface
{
/**
* @var string the HTTP authentication realm
*/
public $realm = 'api';
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
$authHeader = $request->getHeaders()->get('Authorization');
if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) {
$identity = $user->loginByAccessToken($matches[1]);
if ($identity !== null) {
return $identity;
}
$this->handleFailure($response);
}
return null;
}
/**
* @inheritdoc
*/
public function handleFailure($response)
{
$response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\"");
throw new UnauthorizedHttpException('You are requesting with an invalid access token.');
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\data\ActiveDataProvider;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class IndexAction extends Action
{
/**
* @var callable a PHP callable that will be called to prepare a data provider that
* should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead.
* The signature of the callable should be:
*
* ```php
* function ($action) {
* // $action is the action object currently running
* }
* ```
*
* The callable should return an instance of [[ActiveDataProvider]].
*/
public $prepareDataProvider;
/**
* @return ActiveDataProvider
*/
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
/**
* Prepares the data provider that should return the requested collection of the models.
* @return ActiveDataProvider
*/
protected function prepareDataProvider()
{
if ($this->prepareDataProvider !== null) {
return call_user_func($this->prepareDataProvider, $this);
}
/**
* @var \yii\db\BaseActiveRecord $modelClass
*/
$modelClass = $this->modelClass;
return new ActiveDataProvider([
'query' => $modelClass::find(),
]);
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
/**
* OptionsAction responds to the OPTIONS request by sending back an `Allow` header.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class OptionsAction extends \yii\base\Action
{
/**
* @var array the HTTP verbs that are supported by the collection URL
*/
public $collectionOptions = ['GET', 'POST', 'HEAD', 'OPTIONS'];
/**
* @var array the HTTP verbs that are supported by the resource URL
*/
public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
/**
* Responds to the OPTIONS request.
* @param string $id
*/
public function run($id = null)
{
if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') {
Yii::$app->getResponse()->setStatusCode(405);
}
$options = $id === null ? $this->collectionOptions : $this->resourceOptions;
Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $options));
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\Component;
use yii\web\UnauthorizedHttpException;
/**
* QueryParamAuth implements the authentication method based on the access token passed through a query parameter.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class QueryParamAuth extends Component implements AuthInterface
{
/**
* @var string the parameter name for passing the access token
*/
public $tokenParam = 'access-token';
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
$accessToken = $request->get($this->tokenParam);
if (is_string($accessToken)) {
$identity = $user->loginByAccessToken($accessToken);
if ($identity !== null) {
return $identity;
}
}
if ($accessToken !== null) {
$this->handleFailure($response);
}
return null;
}
/**
* @inheritdoc
*/
public function handleFailure($response)
{
throw new UnauthorizedHttpException('You are requesting with an invalid access token.');
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
/**
* RateLimitInterface is the interface that may be implemented by an identity object to enforce rate limiting.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
interface RateLimitInterface
{
/**
* Returns the maximum number of allowed requests and the window size.
* @param array $params the additional parameters associated with the rate limit.
* @return array an array of two elements. The first element is the maximum number of allowed requests,
* and the second element is the size of the window in seconds.
*/
public function getRateLimit($params = []);
/**
* Loads the number of allowed requests and the corresponding timestamp from a persistent storage.
* @param array $params the additional parameters associated with the rate limit.
* @return array an array of two elements. The first element is the number of allowed requests,
* and the second element is the corresponding UNIX timestamp.
*/
public function loadAllowance($params = []);
/**
* Saves the number of allowed requests and the corresponding timestamp to a persistent storage.
* @param integer $allowance the number of allowed requests remaining.
* @param integer $timestamp the current timestamp.
* @param array $params the additional parameters associated with the rate limit.
*/
public function saveAllowance($allowance, $timestamp, $params = []);
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use yii\base\Component;
use yii\base\Action;
use yii\web\Request;
use yii\web\Response;
use yii\web\TooManyRequestsHttpException;
/**
* RateLimiter implements a rate limiting algorithm based on the [leaky bucket algorithm](http://en.wikipedia.org/wiki/Leaky_bucket).
*
* You may call [[check()]] to enforce rate limiting.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class RateLimiter extends Component
{
/**
* @var boolean whether to include rate limit headers in the response
*/
public $enableRateLimitHeaders = true;
/**
* @var string the message to be displayed when rate limit exceeds
*/
public $errorMessage = 'Rate limit exceeded.';
/**
* Checks whether the rate limit exceeds.
* @param RateLimitInterface $user the current user
* @param Request $request
* @param Response $response
* @param Action $action the action to be executed
* @throws TooManyRequestsHttpException if rate limit exceeds
*/
public function check($user, $request, $response, $action)
{
$current = time();
$params = [
'request' => $request,
'action' => $action,
];
list ($limit, $window) = $user->getRateLimit($params);
list ($allowance, $timestamp) = $user->loadAllowance($params);
$allowance += (int)(($current - $timestamp) * $limit / $window);
if ($allowance > $limit) {
$allowance = $limit;
}
if ($allowance < 1) {
$user->saveAllowance(0, $current, $params);
$this->addRateLimitHeaders($response, $limit, 0, $window);
throw new TooManyRequestsHttpException($this->errorMessage);
} else {
$user->saveAllowance($allowance - 1, $current, $params);
$this->addRateLimitHeaders($response, $limit, 0, (int)(($limit - $allowance) * $window / $limit));
}
}
/**
* Adds the rate limit headers to the response
* @param Response $response
* @param integer $limit the maximum number of allowed requests during a period
* @param integer $remaining the remaining number of allowed requests within the current period
* @param integer $reset the number of seconds to wait before having maximum number of allowed requests again
*/
protected function addRateLimitHeaders($response, $limit, $remaining, $reset)
{
if ($this->enableRateLimitHeaders) {
$response->getHeaders()
->set('X-Rate-Limit-Limit', $limit)
->set('X-Rate-Limit-Remaining', $remaining)
->set('X-Rate-Limit-Reset', $reset);
}
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\Component;
use yii\base\Model;
use yii\data\DataProviderInterface;
use yii\data\Pagination;
use yii\helpers\ArrayHelper;
use yii\web\Link;
use yii\web\Request;
use yii\web\Response;
/**
* Serializer converts resource objects and collections into array representation.
*
* Serializer is mainly used by REST controllers to convert different objects into array representation
* so that they can be further turned into different formats, such as JSON, XML, by response formatters.
*
* The default implementation handles resources as [[Model]] objects and collections as objects
* implementing [[DataProviderInterface]]. You may override [[serialize()]] to handle more types.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Serializer extends Component
{
/**
* @var string the name of the query parameter containing the information about which fields should be returned
* for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
* by [[Model::fields()]] will be returned.
*/
public $fieldsParam = 'fields';
/**
* @var string the name of the query parameter containing the information about which fields should be returned
* in addition to those listed in [[fieldsParam]] for a resource object.
*/
public $expandParam = 'expand';
/**
* @var string the name of the HTTP header containing the information about total number of data items.
* This is used when serving a resource collection with pagination.
*/
public $totalCountHeader = 'X-Pagination-Total-Count';
/**
* @var string the name of the HTTP header containing the information about total number of pages of data.
* This is used when serving a resource collection with pagination.
*/
public $pageCountHeader = 'X-Pagination-Page-Count';
/**
* @var string the name of the HTTP header containing the information about the current page number (1-based).
* This is used when serving a resource collection with pagination.
*/
public $currentPageHeader = 'X-Pagination-Current-Page';
/**
* @var string the name of the HTTP header containing the information about the number of data items in each page.
* This is used when serving a resource collection with pagination.
*/
public $perPageHeader = 'X-Pagination-Per-Page';
/**
* @var string the name of the envelope (e.g. `items`) for returning the resource objects in a collection.
* This is used when serving a resource collection. When this is set and pagination is enabled, the serializer
* will return a collection in the following format:
*
* ```php
* [
* 'items' => [...], // assuming collectionEnvelope is "items"
* '_links' => { // pagination links as returned by Pagination::getLinks()
* 'self' => '...',
* 'next' => '...',
* 'last' => '...',
* },
* '_meta' => { // meta information as returned by Pagination::toArray()
* 'totalCount' => 100,
* 'pageCount' => 5,
* 'currentPage' => 1,
* 'perPage' => 20,
* },
* ]
* ```
*
* If this property is not set, the resource arrays will be directly returned without using envelope.
* The pagination information as shown in `_links` and `_meta` can be accessed from the response HTTP headers.
*/
public $collectionEnvelope;
/**
* @var Request the current request. If not set, the `request` application component will be used.
*/
public $request;
/**
* @var Response the response to be sent. If not set, the `response` application component will be used.
*/
public $response;
/**
* @inheritdoc
*/
public function init()
{
if ($this->request === null) {
$this->request = Yii::$app->getRequest();
}
if ($this->response === null) {
$this->response = Yii::$app->getResponse();
}
}
/**
* Serializes the given data into a format that can be easily turned into other formats.
* This method mainly converts the objects of recognized types into array representation.
* It will not do conversion for unknown object types or non-object data.
* The default implementation will handle [[Model]] and [[DataProviderInterface]].
* You may override this method to support more object types.
* @param mixed $data the data to be serialized.
* @return mixed the converted data.
*/
public function serialize($data)
{
if ($data instanceof Model) {
return $data->hasErrors() ? $this->serializeModelErrors($data) : $this->serializeModel($data);
} elseif ($data instanceof DataProviderInterface) {
return $this->serializeDataProvider($data);
} else {
return $data;
}
}
/**
* @return array the names of the requested fields. The first element is an array
* representing the list of default fields requested, while the second element is
* an array of the extra fields requested in addition to the default fields.
* @see Model::fields()
* @see Model::extraFields()
*/
protected function getRequestedFields()
{
$fields = $this->request->get($this->fieldsParam);
$expand = $this->request->get($this->expandParam);
return [
preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY),
preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY),
];
}
/**
* Serializes a data provider.
* @param DataProviderInterface $dataProvider
* @return array the array representation of the data provider.
*/
protected function serializeDataProvider($dataProvider)
{
$models = $this->serializeModels($dataProvider->getModels());
if (($pagination = $dataProvider->getPagination()) !== false) {
$this->addPaginationHeaders($pagination);
}
if ($this->request->getIsHead()) {
return null;
} elseif ($this->collectionEnvelope === null) {
return $models;
} else {
$result = [
$this->collectionEnvelope => $models,
];
if ($pagination !== false) {
$result['_links'] = Link::serialize($pagination->getLinks());
$result['_meta'] = $pagination->toArray();
}
return $result;
}
}
/**
* Adds HTTP headers about the pagination to the response.
* @param Pagination $pagination
*/
protected function addPaginationHeaders($pagination)
{
$links = [];
foreach ($pagination->getLinks(true) as $rel => $url) {
$links[] = "<$url>; rel=$rel";
}
$this->response->getHeaders()
->set($this->totalCountHeader, $pagination->totalCount)
->set($this->pageCountHeader, $pagination->getPageCount())
->set($this->currentPageHeader, $pagination->getPage() + 1)
->set($this->perPageHeader, $pagination->pageSize)
->set('Link', implode(', ', $links));
}
/**
* Serializes a model object.
* @param Model $model
* @return array the array representation of the model
*/
protected function serializeModel($model)
{
if ($this->request->getIsHead()) {
return null;
} else {
list ($fields, $expand) = $this->getRequestedFields();
return $model->toArray($fields, $expand);
}
}
/**
* Serializes the validation errors in a model.
* @param Model $model
* @return array the array representation of the errors
*/
protected function serializeModelErrors($model)
{
$this->response->setStatusCode(422, 'Data Validation Failed.');
$result = [];
foreach ($model->getFirstErrors() as $name => $message) {
$result[] = [
'field' => $name,
'message' => $message,
];
}
return $result;
}
/**
* Serializes a set of models.
* @param array $models
* @return array the array representation of the models
*/
protected function serializeModels(array $models)
{
list ($fields, $expand) = $this->getRequestedFields();
foreach ($models as $i => $model) {
if ($model instanceof Model) {
$models[$i] = $model->toArray($fields, $expand);
} elseif (is_array($model)) {
$models[$i] = ArrayHelper::toArray($model);
}
}
return $models;
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
/**
* UpdateAction implements the API endpoint for updating a model.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class UpdateAction extends Action
{
/**
* @var string the scenario to be assigned to the model before it is validated and updated.
*/
public $scenario = Model::SCENARIO_DEFAULT;
/**
* @var boolean whether to start a DB transaction when saving the model.
*/
public $transactional = true;
/**
* Updates an existing model.
* @param string $id the primary key of the model.
* @return \yii\db\ActiveRecordInterface the model being updated
* @throws \Exception if there is any error when updating the model
*/
public function run($id)
{
/** @var ActiveRecord $model */
$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(), '');
if ($this->transactional && $model instanceof ActiveRecord) {
if ($model->validate()) {
$transaction = $model->getDb()->beginTransaction();
try {
$model->update(false);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollback();
throw $e;
}
}
} else {
$model->save();
}
return $model;
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
use yii\base\InvalidConfigException;
use yii\helpers\Inflector;
use yii\web\CompositeUrlRule;
/**
* UrlRule is provided to simplify the creation of URL rules for RESTful API support.
*
* The simplest usage of UrlRule is to declare a rule like the following in the application configuration,
*
* ```php
* [
* 'class' => 'yii\rest\UrlRule',
* 'controller' => 'user',
* ]
* ```
*
* The above code will create a whole set of URL rules supporting the following RESTful API endpoints:
*
* - `'PUT,PATCH users/<id>' => 'user/update'`: update a user
* - `'DELETE users/<id>' => 'user/delete'`: delete a user
* - `'GET,HEAD users/<id>' => 'user/view'`: return the details/overview/options of a user
* - `'POST users' => 'user/create'`: create a new user
* - `'GET,HEAD users' => 'user/index'`: return a list/overview/options of users
* - `'users/<id>' => 'user/options'`: process all unhandled verbs of a user
* - `'users' => 'user/options'`: process all unhandled verbs of user collection
*
* You may configure [[only]] and/or [[except]] to disable some of the above rules.
* You may configure [[patterns]] to completely redefine your own list of rules.
* You may configure [[controller]] with multiple controller IDs to generate rules for all these controllers.
* For example, the following code will disable the `delete` rule and generate rules for both `user` and `post` controllers:
*
* ```php
* [
* 'class' => 'yii\rest\UrlRule',
* 'controller' => ['user', 'post'],
* 'except' => ['delete'],
* ]
* ```
*
* The property [[controller]] is required and should be the controller ID. It should be prefixed with
* the module ID if the controller is within a module.
*
* The controller ID used in the pattern will be automatically pluralized (e.g. `user` becomes `users`
* as shown in the above examples). You may configure [[urlName]] to explicitly specify the controller ID
* in the pattern.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class UrlRule extends CompositeUrlRule
{
/**
* @var string the common prefix string shared by all patterns.
*/
public $prefix;
/**
* @var string the suffix that will be assigned to [[\yii\web\UrlRule::suffix]] for every generated rule.
*/
public $suffix;
/**
* @var string|array the controller ID (e.g. `user`, `post-comment`) that the rules in this composite rule
* are dealing with. It should be prefixed with the module ID if the controller is within a module (e.g. `admin/user`).
*
* By default, the controller ID will be pluralized automatically when it is put in the patterns of the
* generated rules. If you want to explicitly specify how the controller ID should appear in the patterns,
* you may use an array with the array key being as the controller ID in the pattern, and the array value
* the actual controller ID. For example, `['u' => 'user']`.
*
* You may also pass multiple controller IDs as an array. If this is the case, this composite rule will
* generate applicable URL rules for EVERY specified controller. For example, `['user', 'post']`.
*/
public $controller;
/**
* @var array list of acceptable actions. If not empty, only the actions within this array
* will have the corresponding URL rules created.
* @see patterns
*/
public $only = [];
/**
* @var array list of actions that should be excluded. Any action found in this array
* will NOT have its URL rules created.
* @see patterns
*/
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 $extraPatterns = [];
/**
* @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
*/
public $tokens = [
'{id}' => '<id:\\d[\\d,]*>',
];
/**
* @var array list of possible patterns and the corresponding actions for creating the URL rules.
* The keys are the patterns and the values are the corresponding actions.
* The format of patterns is `Verbs Pattern`, where `Verbs` stands for a list of HTTP verbs separated
* by comma (without space). If `Verbs` is not specified, it means all verbs are allowed.
* `Pattern` is optional. It will be prefixed with [[prefix]]/[[controller]]/,
* and tokens in it will be replaced by [[tokens]].
*/
public $patterns = [
'PUT,PATCH {id}' => 'update',
'DELETE {id}' => 'delete',
'GET,HEAD {id}' => 'view',
'POST' => 'create',
'GET,HEAD' => 'index',
'{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;
/**
* @inheritdoc
*/
public function init()
{
if (empty($this->controller)) {
throw new InvalidConfigException('"controller" must be set.');
}
$controllers = [];
foreach ((array)$this->controller as $urlName => $controller) {
if (is_integer($urlName)) {
$urlName = $this->pluralize ? Inflector::pluralize($controller) : $controller;
}
$controllers[$urlName] = $controller;
}
$this->controller = $controllers;
$this->prefix = trim($this->prefix, '/');
parent::init();
}
/**
* @inheritdoc
*/
protected function createRules()
{
$only = array_flip($this->only);
$except = array_flip($this->except);
$patterns = array_merge($this->patterns, $this->extraPatterns);
$rules = [];
foreach ($this->controller as $urlName => $controller) {
$prefix = trim($this->prefix . '/' . $urlName, '/');
foreach ($patterns as $pattern => $action) {
if (!isset($except[$action]) && (empty($only) || isset($only[$action]))) {
$rules[$urlName][] = $this->createRule($pattern, $prefix, $controller . '/' . $action);
}
}
}
return $rules;
}
/**
* Creates a URL rule using the given pattern and action.
* @param string $pattern
* @param string $prefix
* @param string $action
* @return \yii\web\UrlRuleInterface
*/
protected function createRule($pattern, $prefix, $action)
{
$verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
if (preg_match("/^((?:($verbs),)*($verbs))(?:\\s+(.*))?$/", $pattern, $matches)) {
$verbs = explode(',', $matches[1]);
$pattern = isset($matches[4]) ? $matches[4] : '';
} else {
$verbs = [];
}
$config = $this->ruleConfig;
$config['verb'] = $verbs;
$config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/');
$config['route'] = $action;
if (!in_array('GET', $verbs)) {
$config['mode'] = \yii\web\UrlRule::PARSING_ONLY;
}
$config['suffix'] = $this->suffix;
return Yii::createObject($config);
}
/**
* @inheritdoc
*/
public function parseRequest($manager, $request)
{
$pathInfo = $request->getPathInfo();
foreach ($this->rules as $urlName => $rules) {
if (strpos($pathInfo, $urlName) !== false) {
foreach ($rules as $rule) {
/** @var \yii\web\UrlRule $rule */
if (($result = $rule->parseRequest($manager, $request)) !== false) {
Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__);
return $result;
}
}
}
}
return false;
}
/**
* @inheritdoc
*/
public function createUrl($manager, $route, $params)
{
foreach ($this->controller as $urlName => $controller) {
if (strpos($route, $controller) !== false) {
foreach ($this->rules[$urlName] as $rule) {
/** @var \yii\web\UrlRule $rule */
if (($url = $rule->createUrl($manager, $route, $params)) !== false) {
return $url;
}
}
}
}
return false;
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use Yii;
/**
* ViewAction implements the API endpoint for returning the detailed information about a model.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ViewAction extends Action
{
/**
* Displays a model.
* @param string $id the primary key of the model.
* @return \yii\db\ActiveRecordInterface the model being displayed
*/
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
return $model;
}
}
......@@ -52,6 +52,14 @@ interface IdentityInterface
*/
public static function findIdentity($id);
/**
* Finds an identity by the given secrete token.
* @param string $token the secrete token
* @return IdentityInterface the identity object that matches the given token.
* Null should be returned if such an identity cannot be found
* or the identity is not in an active state (disabled, deleted, etc.)
*/
public static function findIdentityByAccessToken($token);
/**
* Returns an ID that can uniquely identify a user identity.
* @return string|integer an ID that uniquely identifies a user identity.
*/
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
use yii\base\Arrayable;
use yii\base\Object;
/**
* Link represents a link object as defined in [JSON Hypermedia API Language](https://tools.ietf.org/html/draft-kelly-json-hal-03).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Link extends Object implements Arrayable
{
/**
* The self link.
*/
const REL_SELF = 'self';
/**
* @var string a URI [RFC3986](https://tools.ietf.org/html/rfc3986) or
* URI template [RFC6570](https://tools.ietf.org/html/rfc6570). This property is required.
*/
public $href;
/**
* @var string a secondary key for selecting Link Objects which share the same relation type
*/
public $name;
/**
* @var string a hint to indicate the media type expected when dereferencing the target resource
*/
public $type;
/**
* @var boolean a value indicating whether [[href]] refers to a URI or URI template.
*/
public $templated = false;
/**
* @var string a URI that hints about the profile of the target resource.
*/
public $profile;
/**
* @var string a label describing the link
*/
public $title;
/**
* @var string the language of the target resource
*/
public $hreflang;
/**
* @inheritdoc
*/
public function toArray()
{
return array_filter((array)$this);
}
/**
* Serializes a list of links into proper array format.
* @param array $links the links to be serialized
* @return array the proper array representation of the links.
*/
public static function serialize(array $links)
{
foreach ($links as $rel => $link) {
if (is_array($link)) {
foreach ($link as $i => $l) {
$link[$i] = $l instanceof self ? $l->toArray() : ['href' => $l];
}
$links[$rel] = $link;
} elseif (!$link instanceof self) {
$links[$rel] = ['href' => $link];
}
}
return $links;
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
/**
* Linkable is the interface that should be implemented by classes that typically represent locatable resources.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
interface Linkable
{
/**
* Returns a list of links.
*
* Each link is either a URI or a [[Link]] object. The return value of this method should
* be an array whose keys are the relation names and values the corresponding links.
*
* If a relation name corresponds to multiple links, use an array to represent them.
*
* For example,
*
* ```php
* [
* 'self' => 'http://example.com/users/1',
* 'friends' => [
* 'http://example.com/users/2',
* 'http://example.com/users/3',
* ],
* 'manager' => $managerLink, // $managerLink is a Link object
* ]
* ```
*
* @return array the links
*/
public function getLinks();
}
......@@ -199,7 +199,7 @@ class UrlRule extends Object implements UrlRuleInterface
return false;
}
if ($this->verb !== null && !in_array($request->getMethod(), $this->verb, true)) {
if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
return false;
}
......
......@@ -211,6 +211,23 @@ class User extends Component
}
/**
* Logs in a user by the given access token.
* Note that unlike [[login()]], this method will NOT start a session to remember the user authentication status.
* Also if the access token is invalid, the user will remain as a guest.
* @param string $token the access token
* @return IdentityInterface the identity associated with the given access token. Null is returned if
* the access token is invalid.
*/
public function loginByAccessToken($token)
{
/** @var IdentityInterface $class */
$class = $this->identityClass;
$identity = $class::findIdentityByAccessToken($token);
$this->setIdentity($identity);
return $identity;
}
/**
* Logs in a user by cookie.
*
* This method attempts to log in a user using the ID and authKey information
......
......@@ -147,7 +147,7 @@ class ModelTest extends TestCase
$this->assertTrue($speaker->hasErrors('firstName'));
$this->assertFalse($speaker->hasErrors('lastName'));
$this->assertEquals(['Something is wrong!'], $speaker->getFirstErrors());
$this->assertEquals(['firstName' => 'Something is wrong!'], $speaker->getFirstErrors());
$this->assertEquals('Something is wrong!', $speaker->getFirstError('firstName'));
$this->assertNull($speaker->getFirstError('lastName'));
......
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