Commit 8d3665c8 by Nobuo Kihara

Merge branch 'master' into docs-guide-ja-2014-11-01

parents 447808d5 3d49a95b
......@@ -11,18 +11,13 @@ Yii 2 requires PHP 5.4 and embraces the best practices and protocols found in mo
[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2/v/stable.png)](https://packagist.org/packages/yiisoft/yii2)
[![Total Downloads](https://poser.pugx.org/yiisoft/yii2/downloads.png)](https://packagist.org/packages/yiisoft/yii2)
[![Reference Status](https://www.versioneye.com/php/yiisoft:yii2/reference_badge.svg)](https://www.versioneye.com/php/yiisoft:yii2/references)
[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2)
[![Dependency Status](https://www.versioneye.com/php/yiisoft:yii2/dev-master/badge.png)](https://www.versioneye.com/php/yiisoft:yii2/dev-master)
[![HHVM Status](http://hhvm.h4cc.de/badge/yiisoft/yii2-dev.png)](http://hhvm.h4cc.de/package/yiisoft/yii2-dev)
[![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/coverage.png?s=31d80f1036099e9d6a3e4d7738f6b000b3c3d10e)](https://scrutinizer-ci.com/g/yiisoft/yii2/)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/quality-score.png?s=b1074a1ff6d0b214d54fa5ab7abbb90fc092471d)](https://scrutinizer-ci.com/g/yiisoft/yii2/)
[![Code Climate](https://codeclimate.com/github/yiisoft/yii2.png)](https://codeclimate.com/github/yiisoft/yii2)
[![Issue Stats Issues](http://issuestats.com/github/yiisoft/yii2/badge/issue)](http://issuestats.com/github/yiisoft/yii2)
[![Issue Stats Pull request](http://issuestats.com/github/yiisoft/yii2/badge/pr)](http://issuestats.com/github/yiisoft/yii2)
DIRECTORY STRUCTURE
-------------------
......
......@@ -89,8 +89,8 @@
"require-dev": {
"phpunit/phpunit": "3.7.*",
"twig/twig": "*",
"smarty/smarty": "*",
"imagine/imagine": "v0.5.0",
"smarty/smarty": "~3.1",
"imagine/imagine": "0.5.*",
"swiftmailer/swiftmailer": "*",
"fzaninotto/faker": "*",
"cebe/indent": "*"
......
Autenticación
==============
A diferencia de las aplicaciones Web, las API RESTful son usualmente sin estado (stateless), lo que permite que las sesiones o las cookies no sean usadas. Por lo tanto, cada petición debe llevar alguna suerte de credenciales de autenticación, porque la autenticación del usuario no puede ser mantenida por las sesiones o las cookies. Una práctica común es enviar una pieza (token) secreta de acceso con cada petición para autenticar al usuario. Dado que una pieza de autenticación puede ser usada para identificar y autenticar solamente a un usuario, **el API de peticiones tiene que ser siempre enviado vía HTTPS para prevenir ataques que intervengan en la transmisión "man-in-the-middle" (MitM) **.
> Tip: Sin estado (stateless) se refiere a un protocolo en el que cada petición es una transacción independiente del resto de peticiones y la comunicación consiste en pares de peticion y respuesta, por lo que no es necesario retener información en la sesión.
Hay muchas maneras de enviar una pieza (token) de acceso:
* [Autorización Básica HTTP (HTTP Basic Auth)](http://en.wikipedia.org/wiki/Basic_access_authentication): la pieza de acceso es enviada como nombre de usuario. Esto sólo debe de ser usado cuando la pieza de acceso puede ser guardada de forma segura en la parte del API del consumidor. Por ejemplo, el API del consumidor es un programa ejecutándose en un servidor.
* Parámetro de la consulta: la pieza de acceso es enviada como un parámetro de la consulta en la URL de la API, p.e.,
`https://example.com/users?access-token=xxxxxxxx`. Debido que muchos servidores dejan los parámetros de consulta en los logs del servidor esta aproximación suele ser usada principalmente para servir peticiones `JSONP` que no usen las cabeceras HTTP para enviar piezas de acceso.
* [OAuth 2](http://oauth.net/2/): la pieza de acceso es obtenida por el consumidor por medio de una autorización del servidor y enviada al API del servidor según el protocolo OAuth 2 [Piezas HTTP de la portadora (HTTP Bearer Tokens)](http://tools.ietf.org/html/rfc6750).
Yii soporta todos los métodos anteriores de autenticación. Puedes crear nuevos métodos de autenticación de una forma fácil.
Para activar la autenticación para tus APIs, sigue los pasos siguientes:
1. Configura la propiedad [[yii\web\User::enableSession|enableSession]] de el componente `user` de la aplicación a false.
2. Especifica cuál método de autenticación planeas usar configurando la funcionalidad `authenticator` en las clases de la controladora REST.
3. Implementa [[yii\web\IdentityInterface::findIdentityByAccessToken()]] en tu [[yii\web\User::identityClass|clase de identidad de usuario]].
El paso 1 no es necesario pero sí recomendable para las APIs RESTful, pues son sin estado (stateless). Cuando [[yii\web\User::enableSession|enableSession]]
es false, el estado de autenticación del usuario puede NO ser mantenido (persisted) durante varias peticiones usando sesiones. Si embargo, la autenticación puede ser realizada para cada petición, la cual es realizada por los pasos 2 y 3.
> Tip: Puede configurar [[yii\web\User::enableSession|enableSession]] del componente de la aplicación `user` en la configuración de las aplicaciones si estás desarrollando APIs RESTful en términos de un aplicación. Si desarrollas un módulo de las APIs RESTful, puedes poner la siguiente línea en el método del módulo `init()`, tal y como sigue:
> ```php
public function init()
{
parent::init();
\Yii::$app->user->enableSession = false;
}
```
Por ejemplo, para usar HTTP Basic Auth, puedes configurar `authenticator` como sigue,
```php
use yii\filters\auth\HttpBasicAuth;
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
];
return $behaviors;
}
```
Si quires implementar las tres autenticaciones explicadas antes, puedes usar `CompositeAuth` de la siguiente manera,
```php
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => CompositeAuth::className(),
'authMethods' => [
HttpBasicAuth::className(),
HttpBearerAuth::className(),
QueryParamAuth::className(),
],
];
return $behaviors;
}
```
Cada elemento en `authMethods` debe de ser el nombre de un método de autenticación de una clase o un array de configuración.
La implementación de `findIdentityByAccessToken()` es específico de la aplicación. Por ejemplo, en escenarios simples cuando cada usuario sólo puede terner una pieza (token) de acceso, puedes almacenar la pieza de acceso en la columna `access_token` en la tabla de usuario. El método debe de ser inmediatamente implementado en la clase `User` como sigue,
```php
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
class User extends ActiveRecord implements IdentityInterface
{
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
}
```
Después que la autenticación es activada, tal y como se describe arriba, para cada petición de la API, la controladora solicitada puede intentar autenticar al usuario en su paso `beforeAction()`.
Si la autenticación ocurre, la controladora puede realizar otras comprobaciones (como son límite del ratio, autorización) y entonces ejecutar la acción. La identidad del usuario autenticado puede ser recogida via `Yii::$app->user->identity`.
Si la autenticación falla, una respuesta con estado HTTP 401 puede ser devuelta junto con otras cabeceras apropiadas (como son la cabecera para autenticación básica HTTP `WWW-Authenticate` ).
## Autorización <a name="authorization"></a>
Después de que un usuario se ha autenticado, probablementer querrás comprobar si él o ella tiene los permisos para realizar la acción pedida para el recurso pedido. Este proceso es llamado *autorización (authorization)* y está cubierto en detalle en la [Sección de Autorización](security-authorization.md).
Si tus controladoras extienden de [[yii\rest\ActiveController]], puedes sobreescribir (override) el método [[yii\rest\Controller::checkAccess()|checkAccess()]] para realizar la comprobación de la autorización. El método será llamado por las acciones contenidas en [[yii\rest\ActiveController]].
Manejo de errores
==============
Cuando se maneja una petición del API RESTful, si ocurre un error en la petición del usuario o si algo inesperado ocurre en el servidor, simplemente puedes lanzar una excepción para notificar al usuario que algo erróneo ocurrió.
Si puedes identificar la causa del error (p.e., el recurso pedido no existe), debes considerar lanzar una excepción con el apropiado códig HTTP de estado (p.e., [[yii\web\NotFoundHttpException]] representa un código de estado 404). Yii enviará la respuesta a continuación con el correspondiente código de estado HTTP y el texto. Yii puede incluir también la representación serializada de la excepción en el cuerpo de la respuesta. Por ejemplo:
```
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
{
"name": "Not Found Exception",
"message": "El recurso solicitado no ha sido encontrado.",
"code": 0,
"status": 404
}
```
La siguiente lista sumariza los códigos de estado HTTP que son usados por el framework REST:
* `200`: OK. Todo ha funcionado como se esperaba.
* `201`: El recurso ha creado con éxito en respuesta a la petición `POST`. La cabecera de situación `Location` contiene la URL apuntando al nuevo recurso creado.
* `204`: La petición ha sido manejada con éxito y el cuerpo de la respuesta no tiene contenido (como una petición `DELETE`).
* `304`: El recurso no ha sido modificado. Puede usar la versión en caché.
* `400`: Petición errónea. Esto puede estar causado por varias acciones de el usuario, como proveer un JSON no válido en el cuerpo de la petición, proveyendo parámetros de acción no válidos, etc.
* `401`: Autenticación fallida.
* `403`: El usuario autenticado no tiene permitido acceder a la API final.
* `404`: El recurso pedido no existe.
* `405`: Método no permitido. Por favor comprueba la cabecera `Allow` por los métodos HTTP permitidos.
* `415`: Tipo de medio no soportado. El tipo de contenido pedido o el número de versión no es válido.
* `422`: La validación de datos ha fallado (en respuesta a una petición `POST` , por ejemplo). Por favor, comprobad en el cuerpo de la respuesta el mensaje detallado.
* `429`: Demasiadas peticiones. La petición ha sido rechazada debido a un limitación de rango.
* `500`: Error interno del servidor. Esto puede estar causado por errores internos del programa.
## Personalizando la Respuesta al Error <a name="customizing-error-response"></a>
A veces puedes querer personalizar el formato de la respuesta del error por defecto . Por ejemplo, en lugar de depender del uso de diferentes estados HTTP para indicar los diferentes errores, puedes querer usar siempre el estado HTTP 200 y encajonar el código de estado HTTP en la respuesta, tal y como se ve en lo que sigue,
```
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
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8
{
"success": false,
"data": {
"name": "Not Found Exception",
"message": "The requested resource was not found.",
"code": 0,
"status": 404
}
}
```
Para lograr este objetivo, puedes responder al evento `beforeSend` de el componente `response` en la configuración de la aplicación:
```php
return [
// ...
'components' => [
'response' => [
'class' => 'yii\web\Response',
'on beforeSend' => function ($event) {
$response = $event->sender;
if ($response->data !== null && !empty(Yii::$app->request->get['suppress_response_code'])) {
$response->data = [
'success' => $response->isSuccessful,
'data' => $response->data,
];
$response->statusCode = 200;
}
},
],
],
];
```
El anterior código puede reformatear la respuesta (para ambas respuestas, exitosa o fallida) de forma aclaratoria cuando
`suppress_response_code` es pasado como un parámetro `GET`.
Limitando el ratio (rate)
=============
Para prevenir el abuso, puedes considerar añadir un *límitación del ratio* (*rate limiting*) para tus APIs. Por ejemplo, puedes querer limitar el uso del API de cada usuario que no sea como mucho 100 llamadas al API dentro de un periodo de 10 minutos. Si demasiadas peticiones son recibidas de un usuario dentro del periodo de tiempo declarado , una respuesta con código de estado 429 (significa "Demasiadas peticiones") puede ser devuelto.
Para activar la limitación de ratio, la clase [[yii\web\User::identityClass|user identity class]] debe implementar [[yii\filters\RateLimitInterface]].
Este interface requiere la implementación de tres métodos:
* `getRateLimit()`: devuelve el número máximo de peticiones permitidas y el periodo de tiempo (p.e., `[100, 600]` significa que como mucho puede haber 100 llamadas al API dentro de 600 segundos).
* `loadAllowance()`: devuelve el número de peticiones que quedan permitidas y el tiempo (fecha/hora) UNIX con el último límite del ratio que ha sido comprobado.
* `saveAllowance()`: guarda ambos, el número que quedan de peticiones permitidas y el actual tiempo (fecha/hora) UNIX .
Tu puedes usar dos columnas en la tabla de usuario para guardar la información de lo permitido y la fecha/hora (timestamp). Con ambas definidas, entonces `loadAllowance()` y `saveAllowance()` pueden ser implementadas para leer y guardar los valores de las dos columnas correspondientes al actual usuario autenticado. Para mejorar el desempeño, también puedes considerar almacenar esas piezas de información en caché o almacenamiento NoSQL.
Una vez que la clase de identidad implementa el requerido interface, Yii puede usar automáticamente [[yii\filters\RateLimiter]] configurado como una acción de filtrado para [[yii\rest\Controller]] y mejorar la comprobación del limitador de ratio. El limitador de ratio puede lanzar una [[yii\web\TooManyRequestsHttpException]] cuando el límite del ratio es excedido.
Puedes configurar el limitador de ratio en tu clase REST de la controladora como sigue:
```php
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['rateLimiter']['enableRateLimitHeaders'] = false;
return $behaviors;
}
```
Cuando la limitación de ratio está activada, por defecto cada respuesta puede ser enviada con las siguientes cabeceras de HTTP conteniendo la información actual del límite de ratio:
* `X-Rate-Limit-Limit`, el máximo número de peticiones permitidas en un periodo de tiempo
* `X-Rate-Limit-Remaining`, el número de peticiones restantes en el periodo de tiempo actual
* `X-Rate-Limit-Reset`, el número de segundos a esperar para pedir el máximo número de peticiones permitidas
Puedes desactivar estas cabeceras configurando [[yii\filters\RateLimiter::enableRateLimitHeaders]] a false, tal y como en el anterior ejemplo.
Recursos
=========
Las APIs RESTful lo son todos para acceder y manipular *recursos (resources)*. Puedes observar los recursos en el paradigma MVC en [Modelos (models)](structure-models.md) .
Mientras que no hay restricción a cómo representar un recurso, en YII usualmente, puedes representar recursos como objetos de la clase [[yii\base\Model]] o sus clases hijas (p.e. [[yii\db\ActiveRecord]]), por las siguientes razones:
* [[yii\base\Model]] implementa el interface [[yii\base\Arrayable]] , el cual te permite personalizar como exponer los datos de los recursos a travès de las APIs RESTful.
* [[yii\base\Model]] soporta [Validación de entrada (input validation)](input-validation.md), lo cual es muy usado en las APIs RESTful para soportar la entrada de datos.
* [[yii\db\ActiveRecord]] provee un poderoso soporte para el acceso a datos en bases de datos y su manipulación, lo que lo le hace servir perfectamente si sus recursos de datos están en bases de datos.
En esta sección, vamos principalmente a describir como la clase con recursos que extiende de [[yii\base\Model]] (o sus clases hijas) puede especificar qué datos puede ser devueltos vía las APIs RESTful. Si la clase de los recursos no extiende de [[yii\base\Model]], entonces todas las variables públicas miembro serán devueltas.
## Campos (fields) <a name="fields"></a>
Cuando incluimos un recurso en una respuesta de la API RESTful, el recurso necesita ser serializado en una cadena.
Yii divide este proceso en dos pasos. Primero, el recurso es convertido en un array por [[yii\rest\Serializer]].
Segundo, el array es serializado en una cadena en el formato requerido (p.e. JSON, XML) por [[yii\web\ResponseFormatterInterface|response formatters]]. El primer paso es en el que debes de concentrarte principalmente cuando desarrolles una clase de un recurso.
Sobreescribiendo [[yii\base\Model::fields()|fields()]] y/o [[yii\base\Model::extraFields()|extraFields()]],
puedes especificar qué datos, llamados *fields*, en el recursos, pueden ser colocados en el array que le representa.
La diferencia entre estos dos métodos es que el primero especifica el conjunto de campos por defecto que deben ser incluidos en el array que los representa, mientras que el último especifica campos adicionales que deben de ser incluidos en el array si una petición del usuario final para ellos vía el parámetro de consulta `expand`. Por ejemplo,
```
// devuelve todos los campos declarados en fields()
http://localhost/users
// sólo devuelve los campos id y email, provistos por su declaración en fields()
http://localhost/users?fields=id,email
// devuelve todos los campos en fields() y el campo profile siempre y cuando esté declarado en extraFields()
http://localhost/users?expand=profile
// sólo devuelve los campos id, email y profile, siempre y cuando ellos estén declarados en fields() y extraFields()
http://localhost/users?fields=id,email&expand=profile
```
### Sobreescribiendo `fields()` <a name="overriding-fields"></a>
Por defecto, [[yii\base\Model::fields()]] devuelve todos los atributos de los modelos como si fueran campos, mientras [[yii\db\ActiveRecord::fields()]] sólo devuelve los atributos que tengan datos en la base de datos.
Puedes sobreescribir `fields()` para añadir, quitar, renombrar o redefinir campos. El valor de retorno de `fields()` ha de estar en un array. Las claves del array son los nombres de los campos y los valores del array son las correspondientes definiciones de los campos que pueden ser tanto nombres de propiedades/atributos o funciones anónimas que devuelven los correspondientes valores del los campos. En el caso especial de que el nombre de un campo sea el mismo que su definición puedes omitir la clave en el array. Por ejemplo,
```php
// explícitamente lista cada campo, siendo mejor usarlo cuando quieras asegurarte que los cambios
// en una tabla de la base de datos o en un atributo del modelo no provoque el cambio de tu campo (para mantener la compatibilidad anterior).
public function fields()
{
return [
// el nombre de campo es el mismo nombre del atributo
'id',
// el nombre del campo es "email", su atributo se denomina "email_address"
'email' => 'email_address',
// el nombre del campo es "name", su valor es definido está definido por una función anónima de retrollamada (callback)
'name' => function () {
return $this->first_name . ' ' . $this->last_name;
},
];
}
// el ignorar algunos campos, es mejor usarlo cuando heredas de una implementación padre
// y pones en la lista negra (blacklist) algunos campos sensibles
public function fields()
{
$fields = parent::fields();
// quita los campos con información sensible
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
```
> Atención: Dado que, por defecto, todos los atributos de un modelo pueden ser incluidos en la devolución del API, debes
> examinar tus datos para estar seguro de que no contiene información sensible. Si se da este tipo de información,
> debes sobreescribir `fields()` para filtrarlos. En el ejemplo anterior, escogemos
> quitar `auth_key`, `password_hash` y `password_reset_token`.
### Sobreescribiendo `extraFields()` <a name="overriding-extra-fields"></a>
Por defecto, [[yii\base\Model::extraFields()]] no devuelve nada, mientras que [[yii\db\ActiveRecord::extraFields()]] devuelve los nombres de las relaciones que tienen datos (populated) obtenidos de la base de datos.
El formato de devolución de los datos de `extraFields()` es el mismo que el de `fields()`. Usualmente, `extraFields()` es principalmente usado para especificar campos cuyos valores sean objetos. Por ejemplo, dado la siguiente declaración de campo,
```php
public function fields()
{
return ['id', 'email'];
}
public function extraFields()
{
return ['profile'];
}
```
la petición `http://localhost/users?fields=id,email&expand=profile` puede devolver los siguientes datos en formato JSON :
```php
[
{
"id": 100,
"email": "100@example.com",
"profile": {
"id": 100,
"age": 30,
}
},
...
]
```
## Enlaces (Links) <a name="links"></a>
[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS), es una abreviación de Hipermedia es el Motor del Estado de la Aplicación (Hypermedia as the Engine of Application State), promueve que las APIs RESTfull devuelvan información que permita a los clientes descubrir las acciones que soportan los recursos devueltos. El sentido de HATEOAS es devolver un conjunto de hiperenlaces con relación a la información, cuando los datos de los recursos son servidos por las APIs.
Las clases con recursos pueden soportar HATEOAS implementando el interfaz [[yii\web\Linkable]] . El interfaz contiene sólo un método [[yii\web\Linkable::getLinks()|getLinks()]] el cual debe de de devolver una lista de [[yii\web\Link|links]].
Típicamente, debes devolver al menos un enlace `self` representando la URL al mismo recurso objeto. Por ejemplo,
```php
use yii\db\ActiveRecord;
use yii\web\Link;
use yii\web\Linkable;
use yii\helpers\Url;
class User extends ActiveRecord implements Linkable
{
public function getLinks()
{
return [
Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true),
];
}
}
```
Cuando un objeto `User` es devuelto en una respuesta, puede contener un elemento `_links` representando los enlaces relacionados con el usuario, por ejemplo,
```
{
"id": 100,
"email": "user@example.com",
// ...
"_links" => [
"self": "https://example.com/users/100"
]
}
```
## Colecciones <a name="collections"></a>
Los objetos de los recursos pueden ser agrupados en *collections*. Cada colección contiene una lista de recursos objeto del mismo tipo.
Las colecciones pueden ser representadas como arrays pero, es usualmente más deseable representarlas como [proveedores de datos (data providers)](output-data-providers.md). Esto es así porque los proveedores de datos soportan paginación y ordenación de los recursos, lo cual es comunmente necesario en las colecciones devueltas con las APIs RESTful. Por ejemplo, la siguiente acción devuelve un proveedor de datos sobre los recursos post:
```php
namespace app\controllers;
use yii\rest\Controller;
use yii\data\ActiveDataProvider;
use app\models\Post;
class PostController extends Controller
{
public function actionIndex()
{
return new ActiveDataProvider([
'query' => Post::find(),
]);
}
}
```
Cuando un proveedor de datos está enviando una respuesta con el API RESTful, [[yii\rest\Serializer]] llevará la actual página de los recursos y los serializa como un array de recursos objeto. Adicionalmente, [[yii\rest\Serializer]]
puede incluir también la información de paginación a través de las cabeceras HTTP siguientes:
* `X-Pagination-Total-Count`: Número total de recursos;
* `X-Pagination-Page-Count`: Número de páginas;
* `X-Pagination-Current-Page`: Página actual (iniciando en 1);
* `X-Pagination-Per-Page`: Número de recursos por página;
* `Link`: Un conjunto de enlaces de navegación permitiendo al cliente recorrer los recursos página a página.
Un ejemplo se puede ver en la sección [Inicio rápido (Quick Start)](rest-quick-start.md#trying-it-out).
Dando Formato a la Respuesta
===================
Cuando se maneja una petición al API RESTful, una aplicación realiza usualmente los siguientes pasos que están relacionados con el formato de la respuesta:
1. Determinar varios factores que pueden afectar al formato de la respuesta, como son el tipo de medio, lenguaje, versión, etc.
Este proceso es también conocido como [Negociación de contenido (content negotiation)](http://en.wikipedia.org/wiki/Content_negotiation).
2. La conversión de objetos recursos en arrays, está descrito en la sección [Recursos (Resources)](rest-resources.md).
Esto es realizado por el serializador [[yii\rest\Serializer]].
> Tip: Serializar es convertir un elemento a un formato que nos permita guardardarlo de forma permanente, de modo que posteriormente, al recuperarlo, nos de una copia igual a la del elemento antes de ser inicialmente serializado.
3. La conversión de arrays en cadenas con el formato determinado por el paso de negociación de contenido. Esto es realizado por [[yii\web\ResponseFormatterInterface|response formatters]] registrado con el componente de la aplicación [[yii\web\Response::formatters|response]].
## Negociación de contenido (Content Negotiation) <a name="content-negotiation"></a>
Yii soporta la negoiciación de contenido a través del filtro [[yii\filters\ContentNegotiator]]. La clase controladora base del API RESTful [[yii\rest\Controller]] está equipada con este filtro bajo el nombre `contentNegotiator`.
El filtro provee tanto un formato de respuesta de negociación como una negociación de lenguaje. Por ejemplo, si la petición API RESTful contiene la siguiente cabecera,
```
Accept: application/json; q=1.0, */*; q=0.1
```
puede obtener una respuesta en formato JSON, como lo que sigue:
```
$ curl -i -H "Accept: application/json; q=1.0, */*; q=0.1" "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
[
{
"id": 1,
...
},
{
"id": 2,
...
},
...
]
```
En la parte de atrás, antes de que sea ejecutada una acción de la controladora del API RESTful, el filtro [[yii\filters\ContentNegotiator]] comprobará en la cabecera HTTP el `Accept` de la petición y pondrá como `'json'` [[yii\web\Response::format|response format]]. Después de que la acción sea ejecutada y devuelva el recurso objeto resultante o una colección resultante,
[[yii\rest\Serializer]] convertirá el resultado en un array. Y finalmente, [[yii\web\JsonResponseFormatter]] serializará el array en una cadena JSON incluyéndola en el cuerpo de la respuesta.
Por defecto, el API RESTful soporta tanto el formato JSON como el XML. Para soportar un nuevo formato, debes configurar la propiedad [[yii\filters\ContentNegotiator::formats|formats]] del filtro `contentNegotiator` tal y como sigue, en las clases de la controladora del API:
```php
use yii\web\Response;
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['contentNegotiator']['formats']['text/html'] = Response::FORMAT_HTML;
return $behaviors;
}
```
Las claves de la propiedad `formats` son los tipos MIME soportados, mientras que los valores son los nombre de formato de respuesta correspondientes, los cuales han de estar soportados en [[yii\web\Response::formatters]].
## Serialización de Datos <a name="data-serializing"></a>
Como hemos descrito antes, [[yii\rest\Serializer]] es la pieza central para convertir recursos objeto o colecciones en arrays. Reconoce objetos tanto implementando [[yii\base\ArrayableInterface]] como [[yii\data\DataProviderInterface]]. El primer formateador es implementado principalmente para recursos objeto, mientras que el segundo para recursos collección.
Puedes configurar el serializador poniendo la propiedad [[yii\rest\Controller::serializer]] con un array de configuración.
Por ejemplo, a veces puedes querer simplificar la ayuda al trabajo de desarrollo del cliente incluyendo información de la paginación directamente en el cuerpo de la respuesta. Para hacer esto, configura la propiedad [[yii\rest\Serializer::collectionEnvelope]] como sigue:
```php
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}
```
Puedes obtener la respuesta que sigue para la petición `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
}
}
```
Enrutado
=======
Con los recursos y las clases controladoras preparadas, puedes acceder a los recursos usando una URL como `http://localhost/index.php?r=user/create`, parecida a la que usas con aplicaciones Web normales.
En la práctica, querrás usualmente usar URLs más bonitas y obtener ventajas de los comandos de acciones (verbos) HTTP.
Por ejemplo, una petición `POST /users` puede permitir el acceso a la acción `user/create`.
Esto puede realizarse fácilmente configurando el componente de la aplicación `urlManager` en la configuración tal y como sigue:
```php
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
],
]
```
En comparación con la gestión de URL en las aplicaciones Web, lo nuevo de lo anterior es el uso de [[yii\rest\UrlRule]] para el enrutado de las peticiones con el API RESTful. Esta clase especial que contiene la norma para gestionar las URLs puede crear todo un conjunto de URLs hijas para mantener el enrutado y la creación de URLs para la/s especificada/s controlador/as.
Por ejemplo, el código anterior es aproximadamente equivalente a las siguientes reglas:
```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',
]
```
Y los siguientes puntos finales del API son mantenidos por la siguiente regla:
* `GET /users`: listado de todos los usuarios página a página;
* `HEAD /users`: enseña ĺa información resumén del usuario listado;
* `POST /users`: crea un nuevo usuario;
* `GET /users/123`: devuelve los detalles del usuario 123;
* `HEAD /users/123`: enseña la información resúmen del usuario 123;
* `PATCH /users/123` y `PUT /users/123`: actualizan al usuario 123;
* `DELETE /users/123`: borra el usuario 123;
* `OPTIONS /users`: presenta las acciones finales soportadas por `/users`;
* `OPTIONS /users/123`: presenta las acciones finales que soporta `/users/123`.
Puedes configurar las opciones `only` y `except` para explícitamente listar las acciones a soportar y cuales desabilitar, respectivamente. Por ejemplo,
```php
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'except' => ['delete', 'create', 'update'],
],
```
También puedes configurar `patterns` o `extraPatterns` para redifinir patrones existentes o añadir nuevos patrones que soportan esta regla.
Por ejemplo, para soportar la nueva acción `search` por `GET /users/search`, configura la opción `extraPatterns` como sigue,
```php
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'extraPatterns' => [
'GET search' => 'search',
],
```
Queda advertido que la ID de la controladora `user` aparece finalmente en plural tal que`users`.
Esto es debido a que [[yii\rest\UrlRule]] pluraliza de forma automáticalos IDs de las controladoras para ser usadas en los puntos finales.
Puedes desactivar este comportamiento poniendo a false [[yii\rest\UrlRule::pluralize]] , o si quieres usar algunos nombres especiales, debes configurar la propiedad [[yii\rest\UrlRule::controller]]. Dése cuenta que la pluralización de puntos finales del RESTful no siempre añade simplemente una "s" l final de la id de la controladora. Una controladora cuyo ID termina en "x", por ejemplo "BoxController" (con ID `box`), tiene el punto final del RESTful pluralizada a `boxes` por [[yii\rest\UrlRule]].
Versionado
==========
Una buena API ha de ser *versionada*: los cambios y las nuevas características son implementadas en las nuevas versiones del API, en vez de estar continuamente modificando sólo una versión. Al contrario que en las aplicaciones Web, en las cuales tienes total control del código de ambas partes lado del cliente y lado del servidor, las APIs están destinadas a ser usadas por los clientes fuera de tu control. Por esta razón, compatibilidades hacia atrás (BC Backward compatibility) de las APIs ha de ser mantenida siempre que sea posible. Si es necesario un cambio que puede romper la BC, debes de introducirla en la nueva versión del API, e incrementar el número de versión. Los clientes que la usan pueden continuar usando la antigua versión de trabajo del API; los nuevos y actualizados clientes pueden obtener la nueva funcionalidad de la nueva versión del API.
> Tip: referirse a [Semántica del versionado](http://semver.org/) para más información en el diseño del número de versión del API.
Una manera común de implementar el versionado de la API es embeber el número de versión en las URLs del AP.
Por ejemplo, `http://example.com/v1/users` se inicia por la versión 1 de la API del la parte final `/users`.
Otro método de versionado de la API , la cual está ganando predominancia recientemente, es poner el número de versión en las cabeceras de la petición HTTP. Esto se suele hacer típicamente a través la cabecera `Accept` :
```
// vía parámetros
Accept: application/json; version=v1
// vía de el tipo de contenido del vendedor
Accept: application/vnd.company.myapp-v1+json
```
Ambos métodos tienen sus pros y sus contras, y hay gran cantidad de debates sobre cada uno. Debajo puedes ver una estrategia práctica para el versionado de la API que es una mezcla de estos dos métodos:
* Pon cada versión superior del API en un módulo separado cuya ID es el número de la versión principal. (p.e. `v1`, `v2`).
Naturalmente, las URLs del API pueden contener números de versión superiores.
* Dentro de cada versión superior (y por tanto, dentro del correspondiente módulo), usa la cabecera de HTTP `Accept` para determinar el número de la menor versión y escribe código condicional para responder a la menor versión en consecuencia.
Para cada módulo sirviendo una versión superior, el módulo debe incluir los recursos y la clase controladora que especifican la versión. Para mejor separar la responsabilidad del código, puedes conservar un conjunto de recursos base y clases de controladores comunes, y hacer subclases de ellas en cada uno de los módulos de versión individual. Dentro de las subclases, impementa el código concreto como es `Model::fields()`.
Tu código puede estar organizado como lo que sigue:
```
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
```
La configuración de su aplicación puede tener este aspecto:
```php
return [
'modules' => [
'v1' => [
'basePath' => '@app/modules/v1',
'controllerNamespace' => 'app\modules\v1\controllers',
],
'v2' => [
'basePath' => '@app/modules/v2',
'controllerNamespace' => 'app\modules\v2\controllers',
],
],
'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']],
],
],
],
];
```
Como consecuencia de el anterior código, `http://example.com/v1/users` puede devolver la lista de usuarios de la versión 1, mientras
`http://example.com/v2/users` puede devolver la versión 2 de los usuarios.
Gracias a los módulos, el código de las diferentes principales versiónes puede ser aislado. Pero, los módulos, hacen posible reusar el código a través de los módulos vía clases base comunes y otros recursos compartidos.
Para traficar con los números de versión menores, puede obtener las ventajas de el contenido de las capacidades de las conductas de negociación provistas por el [[yii\filters\ContentNegotiator|contentNegotiator]]. La conducta `contentNegotiator` puede poner la propiead [[yii\web\Response::acceptParams]] cuando determina cuál tipo de contenido a soportar.
Por ejemplo, si una peticiónes enviada con la cabecera HTTP `Accept: application/json; version=v1`, entonces la conducta de negociación, [[yii\web\Response::acceptParams]] puede contener el valor `['version' => 'v1']`.
Basado en la información de versión contenida en `acceptParams`, puedes escribir código condicional en lugares como acciones, clases de recursos, serializadores, etc. para proveer la funcionalidad apropiada.
Desde la menor versión, por definición, es necesario mantener la compatibilidad hacia atrás, con suerte no tendrás demasiadas versiones a comporbar en tu código. De otra manera, probablemente puede ocurrir que necesites crear una versión principal.
Trabajando con código de terceros
=================================
De tiempo en tiempo, puede necesitar usar algún código de terceros en sus aplicaciones Yii. O puedes querer usar Yii como una librería en otros sistemas de terceros. En esta sección, te enseñaremos cómo conseguir estos objetivos.
## Usando librerías de terceros en Yii <a name="using-libs-in-yii"></a>
Para usar una librería en una aplicación Yii, primeramente debes de asegurarte que las clases een la librería son incluidas adecuadamente o pueden ser cargadas de forma automática.
### Usando Paquetes de Composer <a name="using-composer-packages"></a>
Muchas librerías de terceros son liberadas en términos de paquetes [Composer](https://getcomposer.org/).
Puedes instalar este tipo de librerias siguiendo dos sencillos pasos:
1. modificar el fichero `composer.json` de tu aplicación y especificar que paquetes Composer quieres instalar.
2. ejecuta `composer install` para instalar los paquetes específicados.
Las clases en los paquetes Composer instalados pueden ser autocargados usando el cargador automatizado de Composer autoloader. Asegúrate que el fichero [script de entrada](structure-entry-scripts.md) de tu aplicación contiene las siguientes líneas para instalar el cargador automático de Composer:
```php
// instalar el cargador automático de Composer
require(__DIR__ . '/../vendor/autoload.php');
// incluir rl fichero de la clase Yii
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
```
### Usando librerías Descargadas <a name="using-downloaded-libs"></a>
Si la librería no es liberada como un paquete de Composer, debes de seguir sus instrucciones de instalación para instalarla.
En muchos casos, puedes necesitar descargar manualmente el fichero de la versión y desempaquetarlo en el directorio `BasePath/vendor` , donde `BasePath` representa el [camino base (base path)](structure-applications.md#basePath) de tu aplicación.
Si la librería lleva su propio cargador automático (autoloader), puedes instalarlo en [script de entrada](structure-entry-scripts.md) de tu aplicación. Es recomendable que la instalación se termine antes de incluir el fichero `Yii.php` de forma que el cargador automático tenga precedencia al cargar de forma automática las clases.
Si la librería no provee un cargador automático de clases, pero la denominación de sus clases sigue el [PSR-4](http://www.php-fig.org/psr/psr-4/), puedes usar el cargador automático de Yii para cargar de forma automática las clases. Todo lo que necesitas es declarar un [alias raiz](concept-aliases.md#defining-aliases) para cada espacio de nombres (namespace) raiz usado en sus clases. Por ejemplo, asume que has instalado una librería en el directorio `vendor/foo/bar`, y que las clases de la librería están bajo el espacio de nombres raiz `xyz`. Puedes incluir el siguiente código en la configuración de tu aplicación:
```php
[
'aliases' => [
'@xyz' => '@vendor/foo/bar',
],
]
```
Si ninguno de lo anterior es el caso, estaría bien que la librería dependa del camino de inclusión (include path) de configuración de PHP para localizar correctamente e incluir los ficheros de las clases. Simplemente siguiendo estas instrucciones de cómo configurar el camino de inclusión de PHP.
En el caso más grave en el que la librería necesite incluir cada uno de sus ficheros de clases, puedes usar el siguiente método para incluir las clases según se pidan:
* Identificar que clases contiene la librería.
* Listar las clases y el camino a los ficheros correspondientes en `Yii::$classMap` en el script de entrada [script de entrada](structure-entry-scripts.md) de la aplicación. Por ejemplo,
```php
Yii::$classMap['Class1'] = 'path/to/Class1.php';
Yii::$classMap['Class2'] = 'path/to/Class2.php';
```
## Usando Yii en Sistemas de Terceros <a name="using-yii-in-others"></a>
Debido a que Yii provee muchas posibilidades excelentes, a veces puedes querer usar alguna de sus características para permitir el desarrollo o mejora de sistemas de terceros, como es WordPress, Joomla, o aplicaciones desarrolladas usando otros frameworks de PHP. Por ejemplo, puedes queres usar la clase [[yii\helpers\ArrayHelper]] o usar la característica [Active Record](db-active-record.md) en un sistema de terceros. Para lograr este objetivo, principalmente necesitas realizar dos pasos: instalar Yii , e iniciar Yii.
Si el sistema de terceros usa Composer para manejar sus dependencias, simplemente ejecuta estos comandos para instalar Yii:
```
composer require "yiisoft/yii2:*"
composer install
```
En otro caso, puedes [descargar](http://www.yiiframework.com/download/) el fichero de la edición de Yii y desempaquetarla en el directorio `BasePath/vendor`.
Después, debes de modificar el script de entrada de sistema de terceros para incluir el siguiente código al principio:
```php
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
$yiiConfig = require(__DIR__ . '/../config/yii/web.php');
new yii\web\Application($yiiConfig); // No ejecutes run() aquí
```
Como puedes ver, el código anterior es muy similar al que puedes ver en [script de entrada](structure-entry-scripts.md) de una aplicación típica. La única diferencia es que después de que se crea la instancia de la aplicación, el método `run()` no es llamado. Esto es así porque llamando a `run()`, Yii se haría cargo del control del flujo de trabajo del manejo de las peticiones, lo cual no es necesario en este caso por estar ya es manejado por la aplicación existente.
Como en una aplicación Yii, debes configurar la instancia de la aplicación basándose en el entorno que se está ejecutando del sistema de terceros. Por ejemplo, para usar la característica [Active Record](db-active-record.md) , necesitas configurar `db` [componente de la aplicación](structure-application-components.md) con los parámetros de la conexión de base de datos usados por el sistema de terceros.
Ahora puedes usar muchas características provistas por Yii. Por ejemplo, puedes crear clases Active Record y usarlas para trabajar con bases de datos.
## Usando Yii 2 con Yii 1 <a name="using-both-yii2-yii1"></a>
Si estaba usando Yii 1 previamente, es como si tuvieras una aplicación Yii 1 funcionando. En vez de reescribir toda la aplicación en Yii 2, puedes solamente mejorarla usando alguna de las características sólo disponibles en Yii 2.
Esto se puede lograr tal y como se describe abajo.
> Nota: Yii 2 requiere PHP 5.4 o superior. Debes de estar seguro que tanto tu servidor como la aplicación existente lo soportan.
Primero, instala Yii 2 en tu aplicación siguiendo las instrucciones descritas en la [última subsección](#using-yii-in-others).
Segundo,modifica el script de entrada de la aplicación como sigue,
```php
// incluir la clase Yii personalizada descrita debajo
require(__DIR__ . '/../components/Yii.php');
// configuración para la aplicación Yii 2
$yii2Config = require(__DIR__ . '/../config/yii2/web.php');
new yii\web\Application($yii2Config); // No llamar a run()
// configuración para la aplicación Yii 1
$yii1Config = require(__DIR__ . '/../config/yii1/main.php');
Yii::createWebApplication($yii1Config)->run();
```
Debido a que ambos Yii 1 y Yii 2 tiene la clase `Yii` , debes crear una versión personalizada para combinarlas.
El código anterior incluye el fichero con la clase `Yii` personalizada, que tiene que ser creada como sigue.
```php
$yii2path = '/path/to/yii2';
require($yii2path . '/BaseYii.php'); // Yii 2.x
$yii1path = '/path/to/yii1';
require($yii1path . '/YiiBase.php'); // Yii 1.x
class Yii extends \yii\BaseYii
{
// copy-paste the code from YiiBase (1.x) here
}
Yii::$classMap = include($yii2path . '/classes.php');
// registrar el autoloader de Yii2 autoloader via Yii1
Yii::registerAutoloader(['Yii', 'autoload']);
// crear el contenedor de inyección de dependencia
Yii::$container = new yii\di\Container;
```
¡Esto es todo!. Ahora, en cualquier parte de tu código, puedes usar `Yii::$app` para acceder a la instancia de la aplicación de Yii 2, mientras `Yii::app()` proporciona la instancia de la aplicación de Yii 1 :
```php
echo get_class(Yii::app()); // genera 'CWebApplication'
echo get_class(Yii::$app); // genera 'yii\web\Application'
```
Widgets de Bootstrap
====================
> Nota: Esta sección está bajo desarrollo.
Yii incluye soporta las marcas y componentes del framework [Bootstrap 3](http://getbootstrap.com/) (también conocido como "Twitter Bootstrap"). Bootstrap es un excelente, adaptable framework que puede aumentar la velocidad de desarrollo de los procesos del lado del cliente.
El núcleo de Bootstrap está represntado en dos partes:
- Elementos básicos de CSS, como son un sistema de diseño en formato cuadrícula , tipografía, clases de ayuda (helpers), y utilidades adapatables(responsive).
- Componentes preparados para su uso, tales como formularios, menús, paginación, cajas modales, pestañas, etc
Elementos básicos
------
Yii no hace uso de elementos básicos de boostrap en el código PHP ya que HTML es muy simple por sí mismo, en este caso. Puedes encontrar detalle del uso de estos elementos básicos en [sitio web de la documentación de bootstrap](http://getbootstrap.com/css/). Aún así Yii provee una manera conveniente de incluir los elementos básicos de los recursos de bootstrap en tus páginas con una simple línea añadida a `AppAsset.php` localizada en tu directorio `@app/assets` :
```php
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset', // Esta línea
];
```
Usar bootstrap a través de el gestor de recursos Yii te permite minimizar estos recursos y combinar con tus propios recursos cuando sea necesario..
Widgets de Yii
-----------
Componentes más complejos de bootstrap components están envueltos dentro de widgets de Yii para permitir una sintaxis más robusta e integrar con las posibilidades y características del framework. Todos los widgets pertenecen al espacio de nombres `\yii\bootstrap` :
- [[yii\bootstrap\ActiveForm|ActiveForm]]
- [[yii\bootstrap\Alert|Alert]]
- [[yii\bootstrap\Button|Button]]
- [[yii\bootstrap\ButtonDropdown|ButtonDropdown]]
- [[yii\bootstrap\ButtonGroup|ButtonGroup]]
- [[yii\bootstrap\Carousel|Carousel]]
- [[yii\bootstrap\Collapse|Collapse]]
- [[yii\bootstrap\Dropdown|Dropdown]]
- [[yii\bootstrap\Modal|Modal]]
- [[yii\bootstrap\Nav|Nav]]
- [[yii\bootstrap\NavBar|NavBar]]
- [[yii\bootstrap\Progress|Progress]]
- [[yii\bootstrap\Tabs|Tabs]]
Usando los ficheros .less de Bootstrap directamente
-------------------------------------------
Si quieres incluir el [Css Bootstrap directamente en tus ficheros less](http://getbootstrap.com/getting-started/#customizing) puedes necesitar desactivar la carga los ficheros css originales de bootstrap.
Esto lo puedes hacer poniendo la propiedad css de [[yii\bootstrap\BootstrapAsset|BootstrapAsset]] vacía.
Para esto necesitas configurar el `assetManager` [componente de la aplicación](structure-application-components.md) como sigue:
```php
'assetManager' => [
'bundles' => [
'yii\bootstrap\BootstrapAsset' => [
'css' => [],
]
]
]
```
Widgets de Jquery UI
====================
> Nota: Esta sección está en desarrollo.
Además de lo anterior, Yii incluye soporte para la librería jquery [jQuery UI](http://api.jqueryui.com/). jQuery UI es un probado conjunto de interacciones con el interface de usuario, efectos, widgets, y temas sobre la librería JavaScript de jquery.
widgets de Yii
--------------
Los componentes más complejos de jQuery UI están envueltos dentro de los widgets de Yii para permitir una sintaxis más robusta e integralas con las características del framework. Todos los widgets pertenecen al espacio de nombre `\yii\jui` :
- [[yii\jui\Accordion|Accordion]]
- [[yii\jui\AutoComplete|AutoComplete]]
- [[yii\jui\DatePicker|DatePicker]]
- [[yii\jui\Dialog|Dialog]]
- [[yii\jui\Draggable|Draggable]]
- [[yii\jui\Droppable|Droppable]]
- [[yii\jui\Menu|Menu]]
- [[yii\jui\ProgressBar|ProgressBar]]
- [[yii\jui\Resizable|Resizable]]
- [[yii\jui\Selectable|Selectable]]
- [[yii\jui\Slider|Slider]]
- [[yii\jui\SliderInput|SliderInput]]
- [[yii\jui\Sortable|Sortable]]
- [[yii\jui\Spinner|Spinner]]
- [[yii\jui\Tabs|Tabs]]
\ No newline at end of file
......@@ -83,7 +83,7 @@ You can specify the rule type in one of the following forms:
the [Core Validators](tutorial-core-validators.md) for the complete list of core validators.
* the name of a validation method in the model class, or an anonymous function. Please refer to the
[Inline Validators](#inline-validators) subsection for more details.
* the name of a validator class. Please refer to the [Standalone Validators](#standalone-validators)
* a fully qualified validator class name. Please refer to the [Standalone Validators](#standalone-validators)
subsection for more details.
A rule can be used to validate one or multiple attributes, and an attribute may be validated by one or multiple rules.
......@@ -367,11 +367,12 @@ class MyForm extends Model
or if they have already failed some validation rules. If you want to make sure a rule is always applied,
you may configure the [[yii\validators\Validator::skipOnEmpty|skipOnEmpty]] and/or [[yii\validators\Validator::skipOnError|skipOnError]]
properties to be false in the rule declarations. For example:
>
> ```php
[
['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false],
]
```
> [
> ['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false],
> ]
> ```
### Standalone Validators <a name="standalone-validators"></a>
......@@ -529,13 +530,14 @@ JS;
```
> Tip: The above code is given mainly to demonstrate how to support client-side validation. In practice,
you may use the [in](tutorial-core-validators.md#in) core validator to achieve the same goal. You may
write the validation rule like the following:
> you may use the [in](tutorial-core-validators.md#in) core validator to achieve the same goal. You may
> write the validation rule like the following:
>
> ```php
[
['status', 'in', 'range' => Status::find()->select('id')->asArray()->column()],
]
```
> [
> ['status', 'in', 'range' => Status::find()->select('id')->asArray()->column()],
> ]
> ```
### Deferred Validation <a name="deferred-validation"></a>
......
......@@ -25,7 +25,9 @@ Yii supports all of the above authentication methods. You can also easily create
To enable authentication for your APIs, do the following steps:
1. Configure the [[yii\web\User::enableSession|enableSession]] property of the `user` application component to be false.
1. Configure the `user` application component:
- Set the [[yii\web\User::enableSession|enableSession]] property to be `false`.
- Set the [[yii\web\User::loginUrl|loginUrl]] property to be `null` to show a HTTP 403 error instead of redirecting to the login page.
2. Specify which authentication methods you plan to use by configuring the `authenticator` behavior
in your REST controller classes.
3. Implement [[yii\web\IdentityInterface::findIdentityByAccessToken()]] in your [[yii\web\User::identityClass|user identity class]].
......@@ -45,7 +47,7 @@ public function init()
}
```
For example, to use HTTP Basic Auth, you may configure `authenticator` as follows,
For example, to use HTTP Basic Auth, you may configure the `authenticator` behavior as follows,
```php
use yii\filters\auth\HttpBasicAuth;
......
......@@ -5,7 +5,7 @@ Yii provides a whole set of tools to simplify the task of implementing RESTful W
In particular, Yii supports the following features about RESTful APIs:
* Quick prototyping with support for common APIs for [Active Record](db-active-record.md);
* Response format (supporting JSON and XML by default) negotiation;
* Response format negotiation (supporting JSON and XML by default);
* Customizable object serialization with support for selectable output fields;
* Proper formatting of collection data and validation errors;
* Support for [HATEOAS](http://en.wikipedia.org/wiki/HATEOAS);
......@@ -18,7 +18,7 @@ In particular, Yii supports the following features about RESTful APIs:
In the following, we use an example to illustrate how you can build a set of RESTful APIs with some minimal coding effort.
Assume you want to expose the user data via RESTful APIs. The user data are stored in the user DB table,
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 [[yii\db\ActiveRecord|ActiveRecord]] class `app\models\User` to access the user data.
......@@ -60,6 +60,23 @@ The above configuration mainly adds a URL rule for the `user` controller so that
can be accessed and manipulated with pretty URLs and meaningful HTTP verbs.
## Enabling JSON Input <a name="enabling-json-input"></a>
To let the API accept input data in JSON format, configure the [[yii\web\Request::$parsers|parsers]] property of
the `request` application component to use the [[yii\web\JsonParser]] for JSON input:
```php
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
]
]
```
> Info: The above configuration is optional. Without the above configuration, the API would only recognize
`application/x-www-form-urlencoded` and `multipart/form-data` input formats.
## Trying it Out <a name="trying-it-out"></a>
With the above minimal amount of effort, you have already finished your task of creating the RESTful APIs
......@@ -76,6 +93,7 @@ for accessing the user data. The APIs you have created include:
* `OPTIONS /users/123`: show the supported verbs regarding endpoint `/users/123`.
> Info: Yii will automatically pluralize controller names for use in endpoints.
> You can configure this using the [[yii\rest\UrlRule::$pluralize]]-property.
You may access your APIs with the `curl` command like the following,
......@@ -83,9 +101,7 @@ You may access your APIs with the `curl` command like the following,
$ curl -i -H "Accept:application/json" "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
......@@ -116,9 +132,7 @@ 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
......@@ -143,6 +157,20 @@ Content-Type: application/xml
</response>
```
The following command will create a new user by sending a POST request with the user data in JSON format:
```
$ curl -i -H "Accept:application/json" -H "Content-Type:application/json" -XPOST "http://localhost/users" -d '{"username": "example", "email": "user@example.com"}'
HTTP/1.1 201 Created
...
Location: http://localhost/users/1
Content-Length: 99
Content-Type: application/json; charset=UTF-8
{"id":1,"username":"example","email":"user@example.com","created_at":1414674789,"updated_at":1414674789}
```
> Tip: You may also access your APIs via Web browser by entering the URL `http://localhost/users`.
However, you may need some browser plugins to send specific request headers.
......@@ -172,4 +200,3 @@ 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 a separate application, different from
your Web front end and back end for easier maintenance.
......@@ -202,6 +202,9 @@ use app\assets\AppAsset;
AppAsset::register($this); // $this represents the view object
```
> Info: The [[yii\web\AssetBundle::register()]] method returns an asset bundle object containing the information
about the published assets, such as [[yii\web\AssetBundle::basePath|basePath]] or [[yii\web\AssetBundle::baseUrl|baseUrl]].
If you are registering an asset bundle in other places, you should provide the needed view object. For example,
to register an asset bundle in a [widget](structure-widgets.md) class, you can get the view object by `$this->view`.
......
......@@ -15,6 +15,7 @@ Yii Framework 2 Change Log
- Bug #5702: Parenthesis should be automatically added to `Validator::whenClient` to avoid js error (mdmunir, qiangxue)
- Bug #5745: Gii and debug modules may cause 404 exception when the route contains dashes (qiangxue)
- Bug #5780: `QueryBuilder::batchInsert()` may cause "undefined index" error (qiangxue)
- Bug #5833: The `message` command fails with a FK constraint error when trying to update messages (qiangxue)
- Bug: Gii console command help information does not contain global options (qiangxue)
- Bug: `yii\web\UrlRule` was unable to create URLs for rules containing unicode characters (samdark)
- Enh #5223: Query builder now supports selecting sub-queries as columns (qiangxue)
......@@ -25,6 +26,7 @@ Yii Framework 2 Change Log
- Enh #5613: Added `--overwrite` option to Gii console command to support overwriting all files (motin, qiangxue)
- Enh #5646: Call `yii\base\ErrorHandler::unregister()` instead of `restore_*_handlers` directly (aivus)
- Enh #5735: Added `yii\bootstrap\Tabs::renderTabContent` to support manually rendering tab contents (RomeroMsk)
- Enh #5770: Added more PHP error names for `ErrorException` (mongosoft)
- Enh #5806: Allow `Html::encode()` to be used when the application is not started (qiangxue)
- Enh: `Console::confirm()` now uses `Console::stdout()` instead of `echo` to be consistent with all other functions (cebe)
- Chg #3630: `yii\db\Command::queryInternal()` is now protected (samdark)
......
......@@ -73,20 +73,22 @@ class ErrorException extends \ErrorException
*/
public function getName()
{
$names = [
E_ERROR => 'PHP Fatal Error',
E_PARSE => 'PHP Parse Error',
E_CORE_ERROR => 'PHP Core Error',
static $names = [
E_COMPILE_ERROR => 'PHP Compile Error',
E_USER_ERROR => 'PHP User Error',
E_WARNING => 'PHP Warning',
E_CORE_WARNING => 'PHP Core Warning',
E_COMPILE_WARNING => 'PHP Compile Warning',
E_USER_WARNING => 'PHP User Warning',
E_STRICT => 'PHP Strict Warning',
E_CORE_ERROR => 'PHP Core Error',
E_CORE_WARNING => 'PHP Core Warning',
E_DEPRECATED => 'PHP Deprecated Warning',
E_ERROR => 'PHP Fatal Error',
E_NOTICE => 'PHP Notice',
E_PARSE => 'PHP Parse Error',
E_RECOVERABLE_ERROR => 'PHP Recoverable Error',
E_DEPRECATED => 'PHP Deprecated Warning',
E_STRICT => 'PHP Strict Warning',
E_USER_DEPRECATED => 'PHP User Deprecated Warning',
E_USER_ERROR => 'PHP User Error',
E_USER_NOTICE => 'PHP User Notice',
E_USER_WARNING => 'PHP User Warning',
E_WARNING => 'PHP Warning',
];
return isset($names[$this->getCode()]) ? $names[$this->getCode()] : 'Error';
......
......@@ -203,10 +203,10 @@ class MessageController extends Controller
$db->createCommand()
->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute();
$lastId = $db->getLastInsertID();
$lastID = $db->getLastInsertID();
foreach ($languages as $language) {
$db->createCommand()
->insert($messageTable, ['id' => $lastId, 'language' => $language])->execute();
->insert($messageTable, ['id' => $lastID, 'language' => $language])->execute();
}
}
}
......@@ -222,17 +222,12 @@ class MessageController extends Controller
->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute();
echo "deleted.\n";
} else {
$last_id = $db->getLastInsertID();
$db->createCommand()
->update(
$sourceMessageTable,
['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")],
['in', 'id', $obsolete]
)->execute();
foreach ($languages as $language) {
$db->createCommand()
->insert($messageTable, ['id' => $last_id, 'language' => $language])->execute();
}
echo "updated.\n";
}
}
......
......@@ -1246,7 +1246,7 @@ class BaseHtml
* - uncheck: string, the value associated with the uncheck state of the radio button. If not set,
* it will take the default value '0'. This method will render a hidden input so that if the radio button
* is not checked and is submitted, the value of this attribute will still be submitted to the server
* via the hidden input.
* via the hidden input. If you do not want any hidden input, you should explicitly set this option as null.
* - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass
* in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks.
* The radio button will be enclosed by the label tag. Note that if you do not specify this option, a default label
......@@ -1296,7 +1296,7 @@ class BaseHtml
* - uncheck: string, the value associated with the uncheck state of the radio button. If not set,
* it will take the default value '0'. This method will render a hidden input so that if the radio button
* is not checked and is submitted, the value of this attribute will still be submitted to the server
* via the hidden input.
* via the hidden input. If you do not want any hidden input, you should explicitly set this option as null.
* - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass
* in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks.
* The checkbox will be enclosed by the label tag. Note that if you do not specify this option, a default label
......
......@@ -18,13 +18,13 @@
*/
return [
'(not set)' => '(məlumat yoxdur)',
'An internal server error occurred.' => 'Daxili server xətasi meydana gəldi.',
'An internal server error occurred.' => 'Daxili server xətası meydana gəldi.',
'Are you sure you want to delete this item?' => 'Bu elementi silmək istədiyinizə əminsinizmi?',
'Delete' => 'Sil',
'Error' => 'Xəta',
'File upload failed.' => 'Fayl yüklənmədi.',
'Home' => 'Ana səhifə',
'Invalid data received for parameter "{param}".' => 'Bu "{param}" parametri üçün yalnış məlumat alındı.',
'Invalid data received for parameter "{param}".' => '"{param}" parametri üçün yalnış məlumat alındı.',
'Login Required' => 'İstifadəçi girişi tələb olunur',
'Missing required arguments: {params}' => 'Tələb olunan arqumentlər tapılmadı: {params}',
'Missing required parameters: {params}' => 'Tələb olunan parametrlər tapılmadı: {params}',
......@@ -33,19 +33,19 @@ return [
'No help for unknown sub-command "{command}".' => 'Qeyri-müəyyən "{command}" sub-əmri üçün kömək yoxdur.',
'No results found.' => 'Heç bir nəticə tapılmadı',
'Only files with these MIME types are allowed: {mimeTypes}.' => 'Ancaq bu MIME tipli fayllara icazə verilib: {mimeTypes}.',
'Only files with these extensions are allowed: {extensions}.' => 'Genislənmələri ancaq bu tipdə olan fayllara icazə verilib: {extensions}.',
'Only files with these extensions are allowed: {extensions}.' => 'Genişlənmələri ancaq bu tipdə olan fayllara icazə verilib: {extensions}.',
'Page not found.' => 'Səhifə tapılmadı.',
'Please fix the following errors:' => 'Xahiş olunur xətaları düzəldin:',
'Please upload a file.' => 'Xahiş olunur fayl yükləyin.',
'Showing <b>{begin, number}-{end, number}</b> of <b>{totalCount, number}</b> {totalCount, plural, one{item} other{items}}.' => '<b>{totalCount, number}</b> {totalCount, plural, one{elementin} other{element}} <b>{begin, number}-{end, number} arasi göstərilir.</b>',
'Please fix the following errors:' => 'Xahiş olunur xətaları düzəldin: ',
'Please upload a file.' => 'Xahiş olunur bir fayl yükləyin.',
'Showing <b>{begin, number}-{end, number}</b> of <b>{totalCount, number}</b> {totalCount, plural, one{item} other{items}}.' => '<b>{totalCount, number}</b> {totalCount, plural, one{elementin} other{elementdən}} <b>{begin, number}-{end, number}</b> arası göstərilir.',
'The file "{file}" is not an image.' => '"{file}" təsvir faylı deyil.',
'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => '"{file}" faylı çox böyükdür. Həcmi {limit, number} {limit, plural, one{byte} other{bytes}} qiymətindən böyük ola bilməz.',
'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => '"{file}" faylı çox kiçikdir. Həcmi {limit, number} {limit, plural, one{byte} other{bytes}} qiymətindən kiçik ola bilməz.',
'The format of {attribute} is invalid.' => '{attribute} formati düzgün deyil.',
'The format of {attribute} is invalid.' => '{attribute} formatı düzgün deyil.',
'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox böyükdür. Uzunluq {limit, plural, one{pixel} other{pixels}} qiymətindən böyük ola bilməz.',
'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox böyükdür. En {limit, number} {limit, plural, one{pixel} other{pixel}} qiymətindən böyük ola bilməz.',
'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox kiçikdir. En {limit, number} {limit, plural, one{pixel} other{pixel}} qiymətindən kiçik ola bilməz.',
'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox kiçikdir. En {limit, number} {limit, plural, one{pixel} other{pixels}} qiymətindən kiçik ola bilməz.',
'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox böyükdür. Eni {limit, number} {limit, plural, one{pixel} other{pixel}} qiymətindən böyük ola bilməz.',
'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox kiçikdir. Eni {limit, number} {limit, plural, one{pixel} other{pixel}} qiymətindən kiçik ola bilməz.',
'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => '"{file}" çox kiçikdir. Eni {limit, number} {limit, plural, one{pixel} other{pixels}} qiymətindən kiçik ola bilməz.',
'The verification code is incorrect.' => 'Təsdiqləmə kodu səhvdir.',
'Total <b>{count, number}</b> {count, plural, one{item} other{items}}.' => 'Toplam <b>{count, number}</b> {count, plural, one{element} other{element}}.',
'Unable to verify your data submission.' => 'Təqdim etdiyiniz məlumat təsdiqlənmədi.',
......@@ -54,7 +54,7 @@ return [
'Update' => 'Yenilə',
'View' => 'Bax',
'Yes' => 'Bəli',
'You are not allowed to perform this action.' => 'Bu əməliyyati yerinə yetirmək üçün icazəniz yoxdur.',
'You are not allowed to perform this action.' => 'Bu əməliyyatı yerinə yetirmək üçün icazəniz yoxdur.',
'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Ancaq {limit, number} {limit, plural, one{fayl} other{fayl}} yükləyə bilərsiniz.',
'the input value' => 'daxil olunmuş qiymət',
'{attribute} "{value}" has already been taken.' => '{attribute} "{value}" artıq istifadə olunub.',
......@@ -68,12 +68,12 @@ return [
'{attribute} must be an integer.' => '{attribute} tam ədəd olmalıdır.',
'{attribute} must be either "{true}" or "{false}".' => '{attribute} {true} ya da {false} ola bilər.',
'{attribute} must be greater than "{compareValue}".' => '{attribute}, "{compareValue}" dən böyük olmalıdır.',
'{attribute} must be greater than or equal to "{compareValue}".' => '{attribute}, "{compareValue}" dən böyük və ya bərabər olmalıdır.',
'{attribute} must be greater than or equal to "{compareValue}".' => '{attribute}, "{compareValue}"dən böyük və ya bərabər olmalıdır.',
'{attribute} must be less than "{compareValue}".' => '{attribute}, "{compareValue}" dən az olmalıdır.',
'{attribute} must be less than or equal to "{compareValue}".' => '{attribute}, "{compareValue}" dən az və ya bərabər olmalıdır.',
'{attribute} must be less than or equal to "{compareValue}".' => '{attribute}, "{compareValue}"dən az və ya bərabər olmalıdır.',
'{attribute} must be no greater than {max}.' => '{attribute} {max} dən böyük olmamalıdır.',
'{attribute} must be no less than {min}.' => '{attribute} {min} dən kiçik olmamalıdır.',
'{attribute} must be repeated exactly.' => '{attribute} dəqiq təkrar olunmalidir.',
'{attribute} must be repeated exactly.' => '{attribute} dəqiq təkrar olunmalıdir.',
'{attribute} must not be equal to "{compareValue}".' => '{attribute}, "{compareValue}" ilə eyni olmamalıdır',
'{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} ən az {min, number} simvol olmalıdır.',
'{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} ən çox {max, number} simvol olmalıdır.',
......
......@@ -67,13 +67,13 @@ class BooleanValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$options = [
'trueValue' => $this->trueValue,
'falseValue' => $this->falseValue,
'message' => Yii::$app->getI18n()->format($this->message, [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
'true' => $this->trueValue,
'false' => $this->falseValue,
], Yii::$app->language),
......
......@@ -117,11 +117,11 @@ class CompareValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$value = $object->$attribute;
$value = $model->$attribute;
if (is_array($value)) {
$this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
$this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
return;
}
......@@ -129,12 +129,12 @@ class CompareValidator extends Validator
$compareLabel = $compareValue = $this->compareValue;
} else {
$compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute;
$compareValue = $object->$compareAttribute;
$compareLabel = $object->getAttributeLabel($compareAttribute);
$compareValue = $model->$compareAttribute;
$compareLabel = $model->getAttributeLabel($compareAttribute);
}
if (!$this->compareValues($this->operator, $this->type, $value, $compareValue)) {
$this->addError($object, $attribute, $this->message, [
$this->addError($model, $attribute, $this->message, [
'compareAttribute' => $compareLabel,
'compareValue' => $compareValue,
]);
......@@ -201,7 +201,7 @@ class CompareValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$options = [
'operator' => $this->operator,
......@@ -213,8 +213,8 @@ class CompareValidator extends Validator
$compareValue = $this->compareValue;
} else {
$compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute;
$compareValue = $object->getAttributeLabel($compareAttribute);
$options['compareAttribute'] = Html::getInputId($object, $compareAttribute);
$compareValue = $model->getAttributeLabel($compareAttribute);
$options['compareAttribute'] = Html::getInputId($model, $compareAttribute);
}
if ($this->skipOnEmpty) {
......@@ -222,7 +222,7 @@ class CompareValidator extends Validator
}
$options['message'] = Yii::$app->getI18n()->format($this->message, [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
'compareAttribute' => $compareValue,
'compareValue' => $compareValue,
], Yii::$app->language);
......
......@@ -94,14 +94,14 @@ class DateValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$value = $object->$attribute;
$value = $model->$attribute;
$timestamp = $this->parseDateValue($value);
if ($timestamp === false) {
$this->addError($object, $attribute, $this->message, []);
$this->addError($model, $attribute, $this->message, []);
} elseif ($this->timestampAttribute !== null) {
$object->{$this->timestampAttribute} = $timestamp;
$model->{$this->timestampAttribute} = $timestamp;
}
}
......
......@@ -41,13 +41,13 @@ class DefaultValueValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
if ($this->isEmpty($object->$attribute)) {
if ($this->isEmpty($model->$attribute)) {
if ($this->value instanceof \Closure) {
$object->$attribute = call_user_func($this->value, $object, $attribute);
$model->$attribute = call_user_func($this->value, $model, $attribute);
} else {
$object->$attribute = $this->value;
$model->$attribute = $this->value;
}
}
}
......
......@@ -92,14 +92,14 @@ class EmailValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$options = [
'pattern' => new JsExpression($this->pattern),
'fullPattern' => new JsExpression($this->fullPattern),
'allowName' => $this->allowName,
'message' => Yii::$app->getI18n()->format($this->message, [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
], Yii::$app->language),
'enableIDN' => (boolean) $this->enableIDN,
];
......
......@@ -81,7 +81,7 @@ class ExistValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
......@@ -91,31 +91,31 @@ class ExistValidator extends Validator
}
$params = [];
foreach ($targetAttribute as $k => $v) {
$params[$v] = is_integer($k) ? $object->$v : $object->$k;
$params[$v] = is_integer($k) ? $model->$v : $model->$k;
}
} else {
$params = [$targetAttribute => $object->$attribute];
$params = [$targetAttribute => $model->$attribute];
}
if (!$this->allowArray) {
foreach ($params as $value) {
if (is_array($value)) {
$this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
$this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
return;
}
}
}
$targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass;
$targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
$query = $this->createQuery($targetClass, $params);
if (is_array($object->$attribute)) {
if ($query->count("DISTINCT [[$targetAttribute]]") != count($object->$attribute)) {
$this->addError($object, $attribute, $this->message);
if (is_array($model->$attribute)) {
if ($query->count("DISTINCT [[$targetAttribute]]") != count($model->$attribute)) {
$this->addError($model, $attribute, $this->message);
}
} elseif (!$query->exists()) {
$this->addError($object, $attribute, $this->message);
$this->addError($model, $attribute, $this->message);
}
}
......
......@@ -166,12 +166,12 @@ class FileValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
if ($this->maxFiles > 1) {
$files = $object->$attribute;
$files = $model->$attribute;
if (!is_array($files)) {
$this->addError($object, $attribute, $this->uploadRequired);
$this->addError($model, $attribute, $this->uploadRequired);
return;
}
......@@ -180,24 +180,24 @@ class FileValidator extends Validator
unset($files[$i]);
}
}
$object->$attribute = array_values($files);
$model->$attribute = array_values($files);
if (empty($files)) {
$this->addError($object, $attribute, $this->uploadRequired);
$this->addError($model, $attribute, $this->uploadRequired);
}
if (count($files) > $this->maxFiles) {
$this->addError($object, $attribute, $this->tooMany, ['limit' => $this->maxFiles]);
$this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]);
} else {
foreach ($files as $file) {
$result = $this->validateValue($file);
if (!empty($result)) {
$this->addError($object, $attribute, $result[0], $result[1]);
$this->addError($model, $attribute, $result[0], $result[1]);
}
}
}
} else {
$result = $this->validateValue($object->$attribute);
$result = $this->validateValue($model->$attribute);
if (!empty($result)) {
$this->addError($object, $attribute, $result[0], $result[1]);
$this->addError($model, $attribute, $result[0], $result[1]);
}
}
}
......@@ -334,22 +334,22 @@ class FileValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
ValidationAsset::register($view);
$options = $this->getClientOptions($object, $attribute);
$options = $this->getClientOptions($model, $attribute);
return 'yii.validation.file(attribute, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');';
}
/**
* Returns the client side validation options.
* @param \yii\base\Model $object the model being validated
* @param \yii\base\Model $model the model being validated
* @param string $attribute the attribute name being validated
* @return array the client side validation options
*/
protected function getClientOptions($object, $attribute)
protected function getClientOptions($model, $attribute)
{
$label = $object->getAttributeLabel($attribute);
$label = $model->getAttributeLabel($attribute);
$options = [];
if ($this->message !== null) {
......
......@@ -65,11 +65,11 @@ class FilterValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$value = $object->$attribute;
$value = $model->$attribute;
if (!$this->skipOnArray || !is_array($value)) {
$object->$attribute = call_user_func($this->filter, $value);
$model->$attribute = call_user_func($this->filter, $value);
}
}
}
......@@ -162,21 +162,21 @@ class ImageValidator extends FileValidator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
ValidationAsset::register($view);
$options = $this->getClientOptions($object, $attribute);
$options = $this->getClientOptions($model, $attribute);
return 'yii.validation.image(attribute, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ', deferred);';
}
/**
* @inheritdoc
*/
protected function getClientOptions($object, $attribute)
protected function getClientOptions($model, $attribute)
{
$options = parent::getClientOptions($object, $attribute);
$options = parent::getClientOptions($model, $attribute);
$label = $object->getAttributeLabel($attribute);
$label = $model->getAttributeLabel($attribute);
if ($this->notImage !== null) {
$options['notImage'] = Yii::$app->getI18n()->format($this->notImage, [
......
......@@ -58,11 +58,11 @@ class InlineValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$method = $this->method;
if (is_string($method)) {
$method = [$object, $method];
$method = [$model, $method];
}
call_user_func($method, $attribute, $this->params);
}
......@@ -70,12 +70,12 @@ class InlineValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
if ($this->clientValidate !== null) {
$method = $this->clientValidate;
if (is_string($method)) {
$method = [$object, $method];
$method = [$model, $method];
}
return call_user_func($method, $attribute, $this->params);
......
......@@ -75,22 +75,22 @@ class NumberValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$value = $object->$attribute;
$value = $model->$attribute;
if (is_array($value)) {
$this->addError($object, $attribute, $this->message);
$this->addError($model, $attribute, $this->message);
return;
}
$pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern;
if (!preg_match($pattern, "$value")) {
$this->addError($object, $attribute, $this->message);
$this->addError($model, $attribute, $this->message);
}
if ($this->min !== null && $value < $this->min) {
$this->addError($object, $attribute, $this->tooSmall, ['min' => $this->min]);
$this->addError($model, $attribute, $this->tooSmall, ['min' => $this->min]);
}
if ($this->max !== null && $value > $this->max) {
$this->addError($object, $attribute, $this->tooBig, ['max' => $this->max]);
$this->addError($model, $attribute, $this->tooBig, ['max' => $this->max]);
}
}
......@@ -117,9 +117,9 @@ class NumberValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$label = $object->getAttributeLabel($attribute);
$label = $model->getAttributeLabel($attribute);
$options = [
'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern),
......
......@@ -79,7 +79,7 @@ class RangeValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$range = [];
foreach ($this->range as $value) {
......@@ -89,7 +89,7 @@ class RangeValidator extends Validator
'range' => $range,
'not' => $this->not,
'message' => Yii::$app->getI18n()->format($this->message, [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
], Yii::$app->language),
];
if ($this->skipOnEmpty) {
......
......@@ -62,7 +62,7 @@ class RegularExpressionValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$pattern = $this->pattern;
$pattern = preg_replace('/\\\\x\{?([0-9a-fA-F]+)\}?/', '\u$1', $pattern);
......@@ -82,7 +82,7 @@ class RegularExpressionValidator extends Validator
'pattern' => new JsExpression($pattern),
'not' => $this->not,
'message' => Yii::$app->getI18n()->format($this->message, [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
], Yii::$app->language),
];
if ($this->skipOnEmpty) {
......
......@@ -86,7 +86,7 @@ class RequiredValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$options = [];
if ($this->requiredValue !== null) {
......@@ -102,7 +102,7 @@ class RequiredValidator extends Validator
}
$options['message'] = Yii::$app->getI18n()->format($options['message'], [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
], Yii::$app->language);
ValidationAsset::register($view);
......
......@@ -18,7 +18,7 @@ class SafeValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
}
}
......@@ -96,12 +96,12 @@ class StringValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$value = $object->$attribute;
$value = $model->$attribute;
if (!is_string($value)) {
$this->addError($object, $attribute, $this->message);
$this->addError($model, $attribute, $this->message);
return;
}
......@@ -109,13 +109,13 @@ class StringValidator extends Validator
$length = mb_strlen($value, $this->encoding);
if ($this->min !== null && $length < $this->min) {
$this->addError($object, $attribute, $this->tooShort, ['min' => $this->min]);
$this->addError($model, $attribute, $this->tooShort, ['min' => $this->min]);
}
if ($this->max !== null && $length > $this->max) {
$this->addError($object, $attribute, $this->tooLong, ['max' => $this->max]);
$this->addError($model, $attribute, $this->tooLong, ['max' => $this->max]);
}
if ($this->length !== null && $length !== $this->length) {
$this->addError($object, $attribute, $this->notEqual, ['length' => $this->length]);
$this->addError($model, $attribute, $this->notEqual, ['length' => $this->length]);
}
}
......@@ -146,9 +146,9 @@ class StringValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
$label = $object->getAttributeLabel($attribute);
$label = $model->getAttributeLabel($attribute);
$options = [
'message' => Yii::$app->getI18n()->format($this->message, [
......
......@@ -74,24 +74,24 @@ class UniqueValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
/* @var $targetClass ActiveRecordInterface */
$targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass;
$targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
$targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
if (is_array($targetAttribute)) {
$params = [];
foreach ($targetAttribute as $k => $v) {
$params[$v] = is_integer($k) ? $object->$v : $object->$k;
$params[$v] = is_integer($k) ? $model->$v : $model->$k;
}
} else {
$params = [$targetAttribute => $object->$attribute];
$params = [$targetAttribute => $model->$attribute];
}
foreach ($params as $value) {
if (is_array($value)) {
$this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
$this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
return;
}
......@@ -106,14 +106,14 @@ class UniqueValidator extends Validator
$query->andWhere($this->filter);
}
if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) {
// if current $object isn't in the database yet then it's OK just to call exists()
if (!$model instanceof ActiveRecordInterface || $model->getIsNewRecord()) {
// if current $model isn't in the database yet then it's OK just to call exists()
$exists = $query->exists();
} else {
// if current $object is in the database already we can't use exists()
/* @var $objects ActiveRecordInterface[] */
$objects = $query->limit(2)->all();
$n = count($objects);
// if current $model is in the database already we can't use exists()
/* @var $models ActiveRecordInterface[] */
$models = $query->limit(2)->all();
$n = count($models);
if ($n === 1) {
$keys = array_keys($params);
$pks = $targetClass::primaryKey();
......@@ -121,10 +121,10 @@ class UniqueValidator extends Validator
sort($pks);
if ($keys === $pks) {
// primary key is modified and not unique
$exists = $object->getOldPrimaryKey() != $object->getPrimaryKey();
$exists = $model->getOldPrimaryKey() != $model->getPrimaryKey();
} else {
// non-primary key, need to exclude the current record based on PK
$exists = $objects[0]->getPrimaryKey() != $object->getOldPrimaryKey();
$exists = $models[0]->getPrimaryKey() != $model->getOldPrimaryKey();
}
} else {
$exists = $n > 1;
......@@ -132,7 +132,7 @@ class UniqueValidator extends Validator
}
if ($exists) {
$this->addError($object, $attribute, $this->message);
$this->addError($model, $attribute, $this->message);
}
}
}
......@@ -66,14 +66,14 @@ class UrlValidator extends Validator
/**
* @inheritdoc
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$value = $object->$attribute;
$value = $model->$attribute;
$result = $this->validateValue($value);
if (!empty($result)) {
$this->addError($object, $attribute, $result[0], $result[1]);
$this->addError($model, $attribute, $result[0], $result[1]);
} elseif ($this->defaultScheme !== null && strpos($value, '://') === false) {
$object->$attribute = $this->defaultScheme . '://' . $value;
$model->$attribute = $this->defaultScheme . '://' . $value;
}
}
......@@ -111,7 +111,7 @@ class UrlValidator extends Validator
/**
* @inheritdoc
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
if (strpos($this->pattern, '{schemes}') !== false) {
$pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern);
......@@ -122,7 +122,7 @@ class UrlValidator extends Validator
$options = [
'pattern' => new JsExpression($pattern),
'message' => Yii::$app->getI18n()->format($this->message, [
'attribute' => $object->getAttributeLabel($attribute),
'attribute' => $model->getAttributeLabel($attribute),
], Yii::$app->language),
'enableIDN' => (boolean) $this->enableIDN,
];
......
......@@ -176,17 +176,17 @@ class Validator extends Component
* Creates a validator object.
* @param mixed $type the validator type. This can be a built-in validator name,
* a method name of the model class, an anonymous function, or a validator class name.
* @param \yii\base\Model $object the data object to be validated.
* @param \yii\base\Model $model the data model to be validated.
* @param array|string $attributes list of attributes to be validated. This can be either an array of
* the attribute names or a string of comma-separated attribute names.
* @param array $params initial values to be applied to the validator properties
* @return Validator the validator
*/
public static function createValidator($type, $object, $attributes, $params = [])
public static function createValidator($type, $model, $attributes, $params = [])
{
$params['attributes'] = $attributes;
if ($type instanceof \Closure || $object->hasMethod($type)) {
if ($type instanceof \Closure || $model->hasMethod($type)) {
// method-based validator
$params['class'] = __NAMESPACE__ . '\InlineValidator';
$params['method'] = $type;
......@@ -217,13 +217,13 @@ class Validator extends Component
/**
* Validates the specified object.
* @param \yii\base\Model $object the data object being validated
* @param \yii\base\Model $model the data model being validated
* @param array|null $attributes the list of attributes to be validated.
* Note that if an attribute is not associated with the validator,
* it will be ignored.
* If this parameter is null, every attribute listed in [[attributes]] will be validated.
*/
public function validateAttributes($object, $attributes = null)
public function validateAttributes($model, $attributes = null)
{
if (is_array($attributes)) {
$attributes = array_intersect($this->attributes, $attributes);
......@@ -231,11 +231,11 @@ class Validator extends Component
$attributes = $this->attributes;
}
foreach ($attributes as $attribute) {
$skip = $this->skipOnError && $object->hasErrors($attribute)
|| $this->skipOnEmpty && $this->isEmpty($object->$attribute);
$skip = $this->skipOnError && $model->hasErrors($attribute)
|| $this->skipOnEmpty && $this->isEmpty($model->$attribute);
if (!$skip) {
if ($this->when === null || call_user_func($this->when, $object, $attribute)) {
$this->validateAttribute($object, $attribute);
if ($this->when === null || call_user_func($this->when, $model, $attribute)) {
$this->validateAttribute($model, $attribute);
}
}
}
......@@ -244,14 +244,14 @@ class Validator extends Component
/**
* Validates a single attribute.
* Child classes must implement this method to provide the actual validation logic.
* @param \yii\base\Model $object the data object to be validated
* @param \yii\base\Model $model the data model to be validated
* @param string $attribute the name of the attribute to be validated.
*/
public function validateAttribute($object, $attribute)
public function validateAttribute($model, $attribute)
{
$result = $this->validateValue($object->$attribute);
$result = $this->validateValue($model->$attribute);
if (!empty($result)) {
$this->addError($object, $attribute, $result[0], $result[1]);
$this->addError($model, $attribute, $result[0], $result[1]);
}
}
......@@ -303,7 +303,7 @@ class Validator extends Component
* - `messages`: an array used to hold the validation error messages for the attribute.
* - `deferred`: an array used to hold deferred objects for asynchronous validation
*
* @param \yii\base\Model $object the data object being validated
* @param \yii\base\Model $model the data model being validated
* @param string $attribute the name of the attribute to be validated.
* @param \yii\web\View $view the view object that is going to be used to render views or view files
* containing a model form with this validator applied.
......@@ -311,7 +311,7 @@ class Validator extends Component
* client-side validation.
* @see \yii\widgets\ActiveForm::enableClientValidation
*/
public function clientValidateAttribute($object, $attribute, $view)
public function clientValidateAttribute($model, $attribute, $view)
{
return null;
}
......@@ -335,17 +335,17 @@ class Validator extends Component
/**
* Adds an error about the specified attribute to the model object.
* This is a helper method that performs message selection and internationalization.
* @param \yii\base\Model $object the data object being validated
* @param \yii\base\Model $model the data model being validated
* @param string $attribute the attribute being validated
* @param string $message the error message
* @param array $params values for the placeholders in the error message
*/
public function addError($object, $attribute, $message, $params = [])
public function addError($model, $attribute, $message, $params = [])
{
$value = $object->$attribute;
$params['attribute'] = $object->getAttributeLabel($attribute);
$value = $model->$attribute;
$params['attribute'] = $model->getAttributeLabel($attribute);
$params['value'] = is_array($value) ? 'array()' : $value;
$object->addError($attribute, Yii::$app->getI18n()->format($message, $params, Yii::$app->language));
$model->addError($attribute, Yii::$app->getI18n()->format($message, $params, Yii::$app->language));
}
/**
......
......@@ -10,6 +10,29 @@ namespace yii\widgets;
use yii\base\Widget;
/**
* Block records all output between [[begin()]] and [[end()]] calls and stores it in [[\yii\base\View::$blocks]].
* for later use.
*
* [[\yii\base\View]] component contains two methods [[\yii\base\View::beginBlock()]] and [[\yii\base\View::endBlock()]].
* The general idea is that you're defining block default in a view or layout:
*
* ```php
* <?php $this->beginBlock('messages', true) ?>
* Nothing.
* <?php $this->endBlock() ?>
* ```
*
* And then overriding default in sub-views:
*
* ```php
* <?php $this->beginBlock('username') ?>
* Umm... hello?
* <?php $this->endBlock() ?>
* ```
*
* Second parameter defines if block content should be outputted which is desired when rendering its content but isn't
* desired when redefining it in subviews.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
......
......@@ -11,6 +11,32 @@ use yii\base\InvalidConfigException;
use yii\base\Widget;
/**
* ContentDecorator records all output between [[begin()]] and [[end()]] calls, passes it to the given view file
* as `$content` and then echoes rendering result.
*
* ```php
* <?php ContentDecorator::begin([
* 'viewFile' => '@app/views/layouts/base.php',
* 'params' => [],
* 'view' => $this,
* ]) ?>
*
* some content here
*
* <?php ContentDecorator::end() ?>
* ```
*
* There are [[\yii\base\View::beginContent()]] and [[\yii\base\View::endContent()]] wrapper methods in the
* [[\yii\base\View]] component to make syntax more friendly. In the view these could be used as follows:
*
* ```php
* <?php $this->beginContent('@app/views/layouts/base.php') ?>
*
* some content here
*
* <?php $this->endContent() ?>
* ```
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
......
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