Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PSDI Army
yii2
Commits
134d3d46
Commit
134d3d46
authored
Apr 09, 2014
by
Qiang Xue
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fixes #2034: Added `ContentNegotiator` to support response format and language negotiation
parent
fa767cea
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
312 additions
and
94 deletions
+312
-94
rest.md
docs/guide/rest.md
+29
-11
CHANGELOG.md
framework/CHANGELOG.md
+1
-0
ContentNegotiator.php
framework/filters/ContentNegotiator.php
+254
-0
CompositeAuth.php
framework/filters/auth/CompositeAuth.php
+10
-1
Controller.php
framework/rest/Controller.php
+11
-68
Response.php
framework/web/Response.php
+7
-14
No files found.
docs/guide/rest.md
View file @
134d3d46
...
...
@@ -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.
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']`
.
*
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.
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.
...
...
framework/CHANGELOG.md
View file @
134d3d46
...
...
@@ -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)
...
...
framework/filters/ContentNegotiator.php
0 → 100644
View file @
134d3d46
<?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
;
}
}
framework/filters/auth/CompositeAuth.php
View file @
134d3d46
...
...
@@ -41,7 +41,8 @@ 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.
*/
...
...
@@ -51,6 +52,14 @@ class CompositeAuth extends AuthMethod
/**
* @inheritdoc
*/
public
function
beforeAction
(
$action
)
{
return
empty
(
$this
->
authMethods
)
?
true
:
parent
::
beforeAction
(
$action
);
}
/**
* @inheritdoc
*/
public
function
authenticate
(
$user
,
$request
,
$response
)
{
foreach
(
$this
->
authMethods
as
$i
=>
$auth
)
{
...
...
framework/rest/Controller.php
View file @
134d3d46
...
...
@@ -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>
...
...
@@ -32,10 +33,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.
*/
public
$serializer
=
'yii\rest\Serializer'
;
...
...
@@ -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
(),
...
...
@@ -86,15 +71,6 @@ class Controller extends \yii\web\Controller
/**
* @inheritdoc
*/
public
function
init
()
{
parent
::
init
();
$this
->
resolveFormatAndVersion
();
}
/**
* @inheritdoc
*/
public
function
afterAction
(
$action
,
$result
)
{
$result
=
parent
::
afterAction
(
$action
,
$result
);
...
...
@@ -102,39 +78,6 @@ class Controller extends \yii\web\Controller
}
/**
* 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.
...
...
framework/web/Response.php
View file @
134d3d46
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment