mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-26 06:15:19 +08:00
Fixes #2034: Added ContentNegotiator
to support response format and language negotiation
This commit is contained in:
@ -5,7 +5,7 @@ Yii provides a whole set of tools to greatly simplify the task of implementing R
|
||||
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;
|
||||
* Response format (supporting JSON and XML by default) 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;
|
||||
@ -187,7 +187,23 @@ 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]].
|
||||
other formats, you should configure the `contentNegotiator` behavior in your REST controller classes as follows,
|
||||
|
||||
|
||||
```php
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
public function behaviors()
|
||||
{
|
||||
return ArrayHelper::merge(parent::behaviors(), [
|
||||
'contentNegotiator' => [
|
||||
'formats' => [
|
||||
// ... other supported formats ...
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Formatting response data in general involves two steps:
|
||||
|
||||
@ -808,8 +824,8 @@ The following list summarizes the HTTP status code that are used by the Yii REST
|
||||
* `500`: Internal server error. This could be caused by internal program errors.
|
||||
|
||||
|
||||
Versioning
|
||||
----------
|
||||
API 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
|
||||
@ -902,14 +918,16 @@ As a result, `http://example.com/v1/users` will return the list of users in vers
|
||||
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]]:
|
||||
To deal with minor version numbers, you may take advantage of the content negotiation
|
||||
feature provided by the [[yii\filters\ContentNegotiator|contentNegotiator]] behavior. The `contentNegotiator`
|
||||
behavior will set the [[yii\web\Response::acceptParams]] property when it determines which
|
||||
content type to support.
|
||||
|
||||
* 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.
|
||||
For example, if a request is sent with the HTTP header `Accept: application/json; version=v1`,
|
||||
after content negotiation, [[yii\web\Response::acceptParams]] will contain the value `['version' => 'v1']`.
|
||||
|
||||
Based on the version information in `acceptParams`, you may write conditional code in places
|
||||
such as actions, resource classes, serializers, etc.
|
||||
|
||||
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.
|
||||
|
@ -287,6 +287,7 @@ Yii Framework 2 Change Log
|
||||
- New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo)
|
||||
- New #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul)
|
||||
- New #1956: Implemented test fixture framework (qiangxue)
|
||||
- New #2034: Added `ContentNegotiator` to support response format and language negotiation (qiangxue)
|
||||
- New #2149: Added `yii\base\DynamicModel` to support ad-hoc data validation (qiangxue)
|
||||
- New #2360: Added `AttributeBehavior` and `BlameableBehavior`, and renamed `AutoTimestamp` to `TimestampBehavior` (lucianobaraglia, qiangxue)
|
||||
- New #2932: Added `yii\web\ViewAction` that allow you to render views based on GET parameter (samdark)
|
||||
|
254
framework/filters/ContentNegotiator.php
Normal file
254
framework/filters/ContentNegotiator.php
Normal file
@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* @link http://www.yiiframework.com/
|
||||
* @copyright Copyright (c) 2008 Yii Software LLC
|
||||
* @license http://www.yiiframework.com/license/
|
||||
*/
|
||||
|
||||
namespace yii\filters;
|
||||
|
||||
use Yii;
|
||||
use yii\base\ActionFilter;
|
||||
use yii\base\BootstrapInterface;
|
||||
use yii\base\InvalidConfigException;
|
||||
use yii\web\Response;
|
||||
use yii\web\Request;
|
||||
use yii\web\UnsupportedMediaTypeHttpException;
|
||||
|
||||
/**
|
||||
* ContentNegotiator supports response format negotiation and application language negotiation.
|
||||
*
|
||||
* When the [[formats|supported formats]] property is specified, ContentNegotiator will support response format
|
||||
* negotiation based on the value of the GET parameter [[formatParam]] and the `Accept` HTTP header.
|
||||
* If a match is found, the [[Response::format]] property will be set as the chosen format.
|
||||
* The [[Response::acceptMimeType]] as well as [[Response::acceptParams]] will also be updated accordingly.
|
||||
*
|
||||
* When the [[languages|supported languages]] is specified, ContentNegotiator will support application
|
||||
* language negotiation based on the value of the GET parameter [[languageParam]] and the `Accept-Language` HTTP header.
|
||||
* If a match is found, the [[\yii\base\Application::language]] property will be set as the chosen language.
|
||||
*
|
||||
* You may use ContentNegotiator as a bootstrap component as well as an action filter.
|
||||
*
|
||||
* The following code shows how you can use ContentNegotiator as a bootstrap component. Note that in this case,
|
||||
* the content negotiation applies to the whole application.
|
||||
*
|
||||
* ```php
|
||||
* // in application configuration
|
||||
* use yii\web\Response;
|
||||
*
|
||||
* return [
|
||||
* 'bootstrap' => [
|
||||
* [
|
||||
* 'class' => 'yii\filters\ContentNegotiator',
|
||||
* 'formats' => [
|
||||
* 'application/json' => Response::FORMAT_JSON,
|
||||
* 'application/xml' => Response::FORMAT_XML,
|
||||
* ],
|
||||
* 'languages' => [
|
||||
* 'en',
|
||||
* 'de',
|
||||
* ],
|
||||
* ],
|
||||
* ],
|
||||
* ];
|
||||
* ```
|
||||
*
|
||||
* The following code shows how you can use ContentNegotiator as an action filter in either a controller or a module.
|
||||
* In this case, the content negotiation result only applies to the corresponding controller or module, or even
|
||||
* specific actions if you configure the `only` or `except` property of the filter.
|
||||
*
|
||||
* ```php
|
||||
* use yii\web\Response;
|
||||
*
|
||||
* public function behaviors()
|
||||
* {
|
||||
* return [
|
||||
* [
|
||||
* 'class' => 'yii\filters\ContentNegotiator',
|
||||
* 'formats' => [
|
||||
* 'application/json' => Response::FORMAT_JSON,
|
||||
* 'application/xml' => Response::FORMAT_XML,
|
||||
* ],
|
||||
* 'languages' => [
|
||||
* 'en',
|
||||
* 'de',
|
||||
* ],
|
||||
* ],
|
||||
* ];
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @author Qiang Xue <qiang.xue@gmail.com>
|
||||
* @since 2.0
|
||||
*/
|
||||
class ContentNegotiator extends ActionFilter implements BootstrapInterface
|
||||
{
|
||||
/**
|
||||
* @var string the name of the GET parameter that specifies the response format.
|
||||
* Note that if the specified format does not exist in [[formats]], a [[UnsupportedMediaTypeHttpException]]
|
||||
* exception will be thrown. If the parameter value is empty or if this property is null,
|
||||
* the response format will be determined based on the `Accept` HTTP header.
|
||||
* @see formats
|
||||
*/
|
||||
public $formatParam = '_format';
|
||||
/**
|
||||
* @var string the name of the GET parameter that specifies the [[\yii\base\Application::language|application language]].
|
||||
* Note that if the specified language does not match any of [[languages]], the first language in [[languages]]
|
||||
* will be used. If the parameter value is empty or if this property is null,
|
||||
* the application language will be determined based on the `Accept-Language` HTTP header.
|
||||
* @see languages
|
||||
*/
|
||||
public $languageParam = '_lang';
|
||||
/**
|
||||
* @var array list of supported response formats. The keys are MIME types (e.g. `application/json`)
|
||||
* while the values are the corresponding formats (e.g. `html`, `json`) which must be supported
|
||||
* as declared in [[\yii\web\Response::formatters]].
|
||||
*
|
||||
* If this property is empty or not set, response format negotiation will be skipped.
|
||||
*/
|
||||
public $formats;
|
||||
/**
|
||||
* @var array a list of supported languages. The array keys are the supported language variants (e.g. `en-GB`, `en-US`),
|
||||
* while the array values are the corresponding language codes (e.g. `en`, `de`) recognized by the application.
|
||||
*
|
||||
* Array keys are not always required. When an array value does not have a key, the matching of the requested language
|
||||
* will be based on a language fallback mechanism. For example, a value of `en` will match `en`, `en_US`, `en-US`, `en-GB`, etc.
|
||||
*
|
||||
* If this property is empty or not set, response format negotiation will be skipped.
|
||||
*/
|
||||
public $languages;
|
||||
/**
|
||||
* @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 bootstrap($app)
|
||||
{
|
||||
$this->negotiate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function beforeAction($action)
|
||||
{
|
||||
$this->negotiate();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiates the response format and application language.
|
||||
*/
|
||||
public function negotiate()
|
||||
{
|
||||
$request = $this->request ? : Yii::$app->getRequest();
|
||||
$response = $this->response ? : Yii::$app->getResponse();
|
||||
if (!empty($this->formats)) {
|
||||
$this->negotiateContentType($request, $response);
|
||||
}
|
||||
if (!empty($languages)) {
|
||||
Yii::$app->language = $this->negotiateLanguage($request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiates the response format.
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @throws InvalidConfigException if [[formats]] is empty
|
||||
* @throws UnsupportedMediaTypeHttpException if none of the requested content types is accepted.
|
||||
*/
|
||||
protected function negotiateContentType($request, $response)
|
||||
{
|
||||
if (!empty($this->formatParam) && ($format = $request->get($this->formatParam)) !== null) {
|
||||
if (in_array($format, $this->formats)) {
|
||||
$response->format = $format;
|
||||
$response->acceptMimeType = null;
|
||||
$response->acceptParams = [];
|
||||
return;
|
||||
} else {
|
||||
throw new UnsupportedMediaTypeHttpException('The requested response format is not supported: ' . $format);
|
||||
}
|
||||
}
|
||||
|
||||
$types = $request->getAcceptableContentTypes();
|
||||
if (empty($types)) {
|
||||
$types['*/*'] = [];
|
||||
}
|
||||
|
||||
foreach ($types as $type => $params) {
|
||||
if (isset($this->formats[$type])) {
|
||||
$response->format = $this->formats[$type];
|
||||
$response->acceptMimeType = $type;
|
||||
$response->acceptParams = $params;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($types['*/*'])) {
|
||||
// return the first format
|
||||
foreach ($this->formats as $type => $format) {
|
||||
$response->format = $this->formats[$type];
|
||||
$response->acceptMimeType = $type;
|
||||
$response->acceptParams = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiates the application language.
|
||||
* @param Request $request
|
||||
* @return string the chosen language
|
||||
*/
|
||||
protected function negotiateLanguage($request)
|
||||
{
|
||||
if (!empty($this->languageParam) && ($language = $request->get($this->languageParam)) !== null) {
|
||||
if (isset($this->languages[$language])) {
|
||||
return $this->languages[$language];
|
||||
}
|
||||
foreach ($this->languages as $key => $supported) {
|
||||
if (is_integer($key) && $this->isLanguageSupported($language, $supported)) {
|
||||
return $supported;
|
||||
}
|
||||
}
|
||||
return reset($this->languages);
|
||||
}
|
||||
|
||||
foreach ($request->getAcceptableLanguages() as $language) {
|
||||
if (isset($this->languages[$language])) {
|
||||
return $this->languages[$language];
|
||||
}
|
||||
foreach ($this->languages as $key => $supported) {
|
||||
if (is_integer($key) && $this->isLanguageSupported($language, $supported)) {
|
||||
return $supported;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reset($this->languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a value indicating whether the requested language matches the supported language.
|
||||
* @param string $requested the requested language code
|
||||
* @param string $supported the supported language code
|
||||
* @return boolean whether the requested language is supported
|
||||
*/
|
||||
protected function isLanguageSupported($requested, $supported)
|
||||
{
|
||||
$supported = str_replace('_', '-', strtolower($supported));
|
||||
$requested = str_replace('_', '-', strtolower($requested));
|
||||
return strpos($requested . '-', $supported . '-') === 0;
|
||||
}
|
||||
}
|
@ -41,13 +41,22 @@ class CompositeAuth extends AuthMethod
|
||||
/**
|
||||
* @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, no authentication will be performed.
|
||||
*
|
||||
* If this property is empty, no authentication will be performed.
|
||||
*
|
||||
* Note that an auth method class must implement the [[\yii\filters\auth\AuthInterface]] interface.
|
||||
*/
|
||||
public $authMethods = [];
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function beforeAction($action)
|
||||
{
|
||||
return empty($this->authMethods) ? true : parent::beforeAction($action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -9,6 +9,7 @@ namespace yii\rest;
|
||||
|
||||
use Yii;
|
||||
use yii\filters\auth\CompositeAuth;
|
||||
use yii\filters\ContentNegotiator;
|
||||
use yii\filters\RateLimiter;
|
||||
use yii\web\Response;
|
||||
use yii\web\UnsupportedMediaTypeHttpException;
|
||||
@ -20,10 +21,10 @@ use yii\web\ForbiddenHttpException;
|
||||
*
|
||||
* 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]]);
|
||||
* 1. Resolving response format (see [[ContentNegotiator]]);
|
||||
* 2. Validating request method (see [[verbs()]]).
|
||||
* 3. Authenticating user (see [[\yii\filters\auth\AuthInterface]]);
|
||||
* 4. Rate limiting (see [[\yii\filters\RateLimiter]]);
|
||||
* 4. Rate limiting (see [[RateLimiter]]);
|
||||
* 5. Formatting response data (see [[serializeData()]]).
|
||||
*
|
||||
* @author Qiang Xue <qiang.xue@gmail.com>
|
||||
@ -31,10 +32,6 @@ use yii\web\ForbiddenHttpException;
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -43,26 +40,7 @@ class Controller extends \yii\web\Controller
|
||||
* @inheritdoc
|
||||
*/
|
||||
public $enableCsrfValidation = false;
|
||||
/**
|
||||
* @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
|
||||
@ -70,6 +48,13 @@ class Controller extends \yii\web\Controller
|
||||
public function behaviors()
|
||||
{
|
||||
return [
|
||||
'contentNegotiator' => [
|
||||
'class' => ContentNegotiator::className(),
|
||||
'formats' => [
|
||||
'application/json' => Response::FORMAT_JSON,
|
||||
'application/xml' => Response::FORMAT_XML,
|
||||
],
|
||||
],
|
||||
'verbFilter' => [
|
||||
'class' => VerbFilter::className(),
|
||||
'actions' => $this->verbs(),
|
||||
@ -83,15 +68,6 @@ class Controller extends \yii\web\Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
$this->resolveFormatAndVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@ -101,39 +77,6 @@ class Controller extends \yii\web\Controller
|
||||
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.
|
||||
|
@ -102,23 +102,16 @@ class Response extends \yii\base\Response
|
||||
*/
|
||||
public $format = self::FORMAT_HTML;
|
||||
/**
|
||||
* @var array a list of supported response formats. The keys are MIME types (e.g. `application/json`)
|
||||
* while the values are the corresponding formats (e.g. `html`, `json`) which must be supported by [[formatters]].
|
||||
* When this property is set, a content type negotiation process will be conducted to determine
|
||||
* the value of [[format]] and the corresponding [[mimeType]] and [[acceptParams]] values.
|
||||
* @var string the MIME type (e.g. `application/json`) from the request ACCEPT header chosen for this response.
|
||||
* This property is mainly set by [\yii\filters\ContentNegotiator]].
|
||||
*/
|
||||
public $supportedFormats;
|
||||
public $acceptMimeType;
|
||||
/**
|
||||
* @var string the MIME type (e.g. `application/json`) chosen for this response after content type negotiation.
|
||||
* This property will be set by the content type negotiation process.
|
||||
* @var array the parameters (e.g. `['q' => 1, 'version' => '1.0']`) associated with the [[acceptMimeType|chosen MIME type]].
|
||||
* This is a list of name-value pairs associated with [[mimeType]] from the ACCEPT HTTP header.
|
||||
* This property is mainly set by [\yii\filters\ContentNegotiator]].
|
||||
*/
|
||||
public $mimeType;
|
||||
/**
|
||||
* @var array the parameters (e.g. `['q' => 1, 'version' => '1.0']`) for the MIME type chosen
|
||||
* by the content type negotiation. This is a list of name-value pairs associated with [[mimeType]]
|
||||
* from the ACCEPT HTTP header. This property will be set by the content type negotiation process.
|
||||
*/
|
||||
public $acceptParams;
|
||||
public $acceptParams = [];
|
||||
/**
|
||||
* @var array the formatters for converting data into the response content of the specified [[format]].
|
||||
* The array keys are the format names, and the array values are the corresponding configurations
|
||||
|
Reference in New Issue
Block a user