Commit 3f42d582 by Qiang Xue

REST wip.

parent c41e389f
......@@ -216,7 +216,7 @@ of the object if it implements [[yii\base\ArrayableInterface]]. If an object doe
an array consisting of all its public properties will be returned.
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 `expandableFields()` method to customize the data to be returned.
you may also override the `fields()` method and/or the `extraFields()` method to customize the data to be returned.
The method [[yii\base\Model::fields()]] declares a set of fields of an object that should be included in the result.
The default implementation returns all attributes of a model as the output fields. You can customize it to add,
......@@ -254,7 +254,7 @@ returning the corresponding field values.
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::expandableFields()]] is very similar to [[yii\base\Model::fields()]].
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.
......
<?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;
}
}
......@@ -57,11 +57,12 @@ use yii\web\Linkable;
*/
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.
......@@ -829,8 +830,8 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
* ```
*
* In this method, you may also want to return different lists of fields based on some context
* information. For example, depending on privilege of the current application user, you may return different
* sets of visible fields.
* 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.
*
......@@ -844,51 +845,8 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
}
/**
* 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.
*
* @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 expandableFields()
{
return [];
}
/**
* Converts the object into an array.
* The default implementation will return [[attributes]].
* @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 [[expandableFields()]]
* 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 [[expandableFields()]]
* 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
......@@ -912,7 +870,7 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
return $result;
}
foreach ($this->expandableFields() as $field => $definition) {
foreach ($this->extraFields() as $field => $definition) {
if (is_integer($field)) {
$field = $definition;
}
......
......@@ -8,6 +8,7 @@
namespace yii\data;
use Yii;
use yii\base\Arrayable;
use yii\base\Object;
use yii\web\Link;
use yii\web\Linkable;
......@@ -67,7 +68,7 @@ use yii\web\Request;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Pagination extends Object implements Linkable
class Pagination extends Object implements Linkable, Arrayable
{
const LINK_NEXT = 'next';
const LINK_PREV = 'prev';
......@@ -316,6 +317,19 @@ class Pagination extends Object implements Linkable
}
/**
* @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
......
......@@ -1361,7 +1361,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*
* The default implementation returns the names of the relations that have been populated into this record.
*/
public function expandableFields()
public function extraFields()
{
$fields = array_keys($this->getRelatedRecords());
return array_combine($fields, $fields);
......
......@@ -93,7 +93,7 @@ class Action extends \yii\base\Action
if (count($keys) === count($values)) {
$model = $modelClass::find(array_combine($keys, $values));
}
} else {
} elseif ($id !== null) {
$model = $modelClass::find($id);
}
......
......@@ -8,6 +8,7 @@
namespace yii\rest;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\web\ForbiddenHttpException;
/**
......@@ -44,12 +45,12 @@ class ActiveController extends Controller
* @var string the scenario used for updating a model.
* @see \yii\base\Model::scenarios()
*/
public $updateScenario = 'api-update';
public $updateScenario = Model::SCENARIO_DEFAULT;
/**
* @var string the scenario used for creating a model.
* @see \yii\base\Model::scenarios()
*/
public $createScenario = 'api-create';
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.
......
......@@ -14,7 +14,7 @@ use yii\web\IdentityInterface;
use yii\web\UnauthorizedHttpException;
/**
* AuthInterface is the interface required by classes than support user authentication.
* AuthInterface is the interface required by classes that support user authentication.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
......
......@@ -31,10 +31,9 @@ use yii\web\VerbFilter;
class Controller extends \yii\web\Controller
{
/**
* The name of the header parameter representing the API version number.
* @var string the name of the header parameter representing the API version number.
*/
const HEADER_VERSION = 'version';
public $versionHeaderParam = 'version';
/**
* @var string|array the configuration for creating the serializer that formats the response data.
*/
......@@ -58,16 +57,16 @@ class Controller extends \yii\web\Controller
*/
public $rateLimiter = 'yii\rest\RateLimiter';
/**
* @var string the chosen API version number
* @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 chosen version number. For this reason, you should
* put the latest version number at the first.
* 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 = ['1.0'];
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
......@@ -129,7 +128,7 @@ class Controller extends \yii\web\Controller
*/
protected function resolveFormatAndVersion()
{
$this->version = reset($this->supportedVersions);
$this->version = empty($this->supportedVersions) ? null : reset($this->supportedVersions);
Yii::$app->getResponse()->format = reset($this->supportedFormats);
$types = Yii::$app->getRequest()->getAcceptableContentTypes();
if (empty($types)) {
......@@ -139,9 +138,9 @@ class Controller extends \yii\web\Controller
foreach ($types as $type => $params) {
if (isset($this->supportedFormats[$type])) {
Yii::$app->getResponse()->format = $this->supportedFormats[$type];
if (isset($params[self::HEADER_VERSION])) {
if (in_array($params[self::HEADER_VERSION], $this->supportedVersions, true)) {
$this->version = $params[self::HEADER_VERSION];
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.');
}
......
......@@ -8,6 +8,7 @@
namespace yii\rest;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
/**
......@@ -21,7 +22,7 @@ class CreateAction extends Action
/**
* @var string the scenario to be assigned to the new model before it is validated and saved.
*/
public $scenario = 'api-create';
public $scenario = Model::SCENARIO_DEFAULT;
/**
* @var boolean whether to start a DB transaction when saving the model.
*/
......
......@@ -13,31 +13,93 @@ 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 Request
* @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
* @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) {
......@@ -48,6 +110,15 @@ class Serializer extends Component
}
}
/**
* 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) {
......@@ -59,6 +130,13 @@ class Serializer extends Component
}
}
/**
* @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);
......@@ -70,12 +148,13 @@ class Serializer extends Component
}
/**
* Serializes a data provider.
* @param DataProviderInterface $dataProvider
* @return array
* @return array the array representation of the data provider.
*/
protected function serializeDataProvider($dataProvider)
{
$models = $dataProvider->getModels();
$models = $this->serializeModels($dataProvider->getModels());
if (($pagination = $dataProvider->getPagination()) !== false) {
$this->addPaginationHeaders($pagination);
......@@ -83,12 +162,22 @@ class Serializer extends Component
if ($this->request->getIsHead()) {
return null;
} elseif ($this->collectionEnvelope === null) {
return $models;
} else {
return $this->serializeModels($models);
$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)
......@@ -107,8 +196,9 @@ class Serializer extends Component
}
/**
* Serializes a model object.
* @param Model $model
* @return array
* @return array the array representation of the model
*/
protected function serializeModel($model)
{
......@@ -121,8 +211,9 @@ class Serializer extends Component
}
/**
* Serializes the validation errors in a model.
* @param Model $model
* @return array
* @return array the array representation of the errors
*/
protected function serializeModelErrors($model)
{
......@@ -137,6 +228,11 @@ class Serializer extends Component
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();
......
......@@ -8,6 +8,7 @@
namespace yii\rest;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
/**
......@@ -21,7 +22,7 @@ class UpdateAction extends Action
/**
* @var string the scenario to be assigned to the model before it is validated and updated.
*/
public $scenario = 'api-update';
public $scenario = Model::SCENARIO_DEFAULT;
/**
* @var boolean whether to start a DB transaction when saving the model.
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment