===== Servidor REST API con Yii2 ===== Podemos crear fácilmente un servidor REST API con Yii2, que puede hacer las mismas funciones que node.js, aprovechando parte de una aplicación que ya tengamos con Yii2 o partiendo de cero. Los pasos son: ==== Habilitar URL's limpias y configurar reglas ==== En [[..:cleanurl|este documento]] se explica como habilitar las URL's limpias. Para el caso del servidor REST API debemos asignar la propiedad **enableStrictParsing** de urlManager a true, y definir la propiedad **rules**,que especificarán la relación entre rutas y controladore/acciones. Ver [[http://www.yiiframework.com/doc-2.0/yii-web-urlmanager.html#$rules-detail|este documento]]. Si vamos a definir muchas reglas, es cómodo separar esta información en un fichero aparte y cargarlo en config/web.php: 'urlManager' => [ 'enablePrettyUrl' => true, 'enableStrictParsing' => true, 'showScriptName' => false, 'rules' => require(__DIR__ . '/rules.php') ] Y así, en rules.php ponemos las reglas: 'yii\rest\UrlRule', 'pluralize'=>false, 'controller' => ['CONT1','CONT2','...'], ], ['class' => 'yii\rest\UrlRule', 'controller' => ['user'], 'pluralize'=>false, 'extraPatterns'=>['POST authenticate'=>'authenticate'] ] ]; // CONT1,CONT2...son los controladores de nuestra aplicación: usuarios, articulos, tienda, eventos, etc.. Es habitual que en los verbos POST, PUT y PATCH, los datos del objeto a actualizar se envíen en formato JSON, como es el caso de ANGULAR JS. Para que Yii pueda procesarlos correctamente, tenemos que activar el parser de JSON en el componente Request->parsers: 'request' => [ 'cookieValidationKey' => '....', 'parsers' => [ 'application/json' => 'yii\web\JsonParser', ] ], ==== Controladores ==== Los controladores para servicios REST API derivan de la clase yii\rest\ActiveController. Para crear un controlador que defina los servicios básicos (GET, GET id,POST, PUT, PATCH, DELETE), basta con especificar el modelo con el que va a trabajar el controlador namespace app\controllers; use yii\rest\ActiveController; class UsuariosController extends ActiveController { public $modelClass = 'app\models\Usuarios'; } Con esto, ya podemos hacer pruebas de solicitar datos a nuestro servidor. Es conveniente utilizar algún programa para poder hacer tests de nuestro servidor. Uno muy conveniente es POSTMAN, que tiene versión de escritorio y también puede instalarse como plugin de Chrome. Si accedemos a la ruta: **http://SERVIDOR/APLICACION/web/usuarios**, debería salir una lista de usuarios, en formato JSON. (Si lo hacemos en el navegador, se mostrará en XML, porque Yii detecta que se está accediendo de esa forma). ==== Selección de datos a Recibir ==== En el servidor es posible definir qué atributos queremos enviar en las peticiones GET, y que atributos puede solicitar el usuario bajo demanda. Para ello, utilizaremos los métodos fields() y extrafields() en los modelos implicados. El método fields() devuelve un array con las propiedades que se devolverán por defecto, si no se especifican en la petición public function fields(){ return ['id','nombre','estado','email','rol']; } Por defecto, fields() devuelve todos los atributos del modelo. Podemos eliminar o añadir atributos a partir de los predefinidos en la clase base: public function fields(){ $fields=array_diff(parent::fields(),['password','authkey']); //Nunca devuelve password ni authkey return array_merge($fields,['estadoText','comentarios']); //Añade estadoText y el array de comentarios definidos mediante una relación con el modelo Comentarios } El cliente puede especificar los atributos que desea que se devuelvan, que han de estar en la lista generada en el modelo mediante fields(). Para ello, se añade el parámetro ?fields=propiedades en la URL: http://SERVIDOR/usuarios/7?fields=id,nombre,email // Únicamente se reciben estas 3 propiedades El método extrafields es similar a fields(), y devuelve las propiedades opcionales que se pueden obtener, mediante el parámetro ?expand=propiedades en la URL public function extrafields(){ return ['grupos','apuntes','eventos','saldo']); } Así, para obtener los apuntes del usuario: http://SERVIDOR/usuarios/7?extrafields=apuntes // Devuelve el usuario junto con sus apuntes ==== Autenticación ==== Podemos obligar a que en determinados controladores o peticiones sea necesario que el usuario se identifique. A diferencia de las aplicaciones estándar, donde se suelente utilizar las sesiones de usuario ($_SESSION de php), en las aplicaciones Rest API se suele enviar en cada petición lo necesario para que el servidor identifique al usuario. Hay diversas formas de hacerlo.[[http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html|Ver documentación]]. Una solución típica es implementar una acción de autenticación con usuario y contraseña, que devuelve un token (cadena aleatoria asociada a cada usuario), y en las sucesivas peticiones se envía este token en las HEADERS de la petición http para identificar al usuario. (Bearer token). Para ello,los pasos a seguir son: En la configuraciób de rules de urlManager (rules.php, o web.php si no utilizamos un fichero aparte), definimos un verbo particular para esta acción 'rules'=>.... ['class' => 'yii\rest\UrlRule', 'controller' => ['user'], 'pluralize'=>false, 'extraPatterns'=>['POST authenticate'=>'authenticate', 'OPTIONS authenticate'=>'authenticate', ] ], ... Creamos un controlador UserController que contiene la acción de autenticación, utilizando el modelo Usuario: namespace app\controllers; use yii\rest\Controller; class UserController extends Controller { public $modelClass = 'app\models\Usuarios'; public function actionAuthenticate(){ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Si se envían los datos en formato raw dentro de la petición http, se recogen así: $params=json_decode(file_get_contents("php://input"), false); @$username=$params->username; @$password=$params->password; // Si se envían los datos de la forma habitual (form-data), se reciben en $_POST: //$username=$_POST['username']; //$password=$_POST['password' ]; if($u=\app\models\Usuarios::findOne(['usuario'=>$username])) if($u->password==md5($password)) {//o crypt, según esté en la BD return ['token'=>$u->token,'id'=>$u->id,'nombre'=>$u->nombre]; } return ['error'=>'Usuario incorrecto. '.$username]; } } En la tabla de usuarios necesitaremos un campo 'token', asociado a cada usuario, que podemos generar de forma aleatoria en el momento del alta del mismo. Este dato es el que se devuelve en la autenticación y se empleará en las sucesivas peticiones para identificar al usuario. El modelo Usuarios ha de implementar UserIdentityInterface, tal como se detalla [[http://www.yiiframework.com/doc-2.0/guide-security-authentication.html|aquí]] Por defecto, Yii utiliza el modelo User para la autenticación. Si lo cambiamos por Usuarios, debemos modificar la configuración de components en web.php: 'components' => [ ... 'user' => [ 'identityClass' => 'app\models\Usuarios', 'enableAutoLogin' => true, ], ... En el modelo Usuarios implementaremos el método que localiza al usuario a partir del token: public static function findIdentityByAccessToken($token, $type = null) { return self::findOne(['token' => $token]); } En las acciones donde queramos que el usuario tenga que identificarse, activaremos el behavior authenticator en el método behaviors() del controlador. ... use yii\filters\auth\HttpBearerAuth; public function behaviors() { $behaviors = parent::behaviors(); ... $behaviors['authenticator'] = [ 'class' => HttpBearerAuth::className(), 'except' => ['accion1', 'accion2'], ]; return $behaviors; } accion1 y accion2 serían acciones que no necesitan autenticación. (Por ejemplo, en el controlador user, las acciones options y authenticate no deben requerir autenticación. Desde la aplicación cliente, para solicitar el token haremos un petición POST a **SERVIDOR/user/authenticate**, pasándole como datos username y password. Si es correcto, devolverá un json con el token: {id:"2",nombre:"carlos...",token:"3424jgasdoasdl234lasdasdaasdkhf"} Si ejecutamos una acción protegida por token, recibiremos un error "Unauthorized access". En las peticiones protegidas, el token se envía en los Headers de la petición http (o https) de la forma Bearer 3424jgasdoasdl234lasdasdaasdkhf ==== Personalización de acciones ==== Si queremos personalizar una acción sobreescribiremos el método actions del controlador. Podemos eliminar acciones , añadir o modificar las predefinidas. Por ejemplo, si queremos que un usuario solamente pueda acceder a información suya respecto a la tabla Apuntes: namespace app\controllers; use Yii; use yii\rest\ActiveController; use yii\data\ActiveDataProvider; use app\models\Apuntes; class ApuntesController extends ActiveController { public $modelClass = 'app\models\Apuntes'; public function actions() { $actions = parent::actions(); //Eliminamos acciones de crear y eliminar apuntes. Eliminamos update para personalizarla unset($actions['delete'], $actions['create'],$actions['update']); // Redefinimos el método que prepara los datos en el index $actions['index']['prepareDataProvider'] = [$this, 'indexProvider']; return $actions; } public function indexProvider() { $uid=Yii::$app->user->identity->id; return new ActiveDataProvider([ 'query' => Apuntes::find()->where('usuarios_id='.$uid )->orderBy('id') ]); } public function actionUpdate($id){ // Hacemos lo queramos y devolvemos información con return (un array, un objeto...) $uid=Yii::$app->user->identity->id; $model=Apuntes::findOne($id); if(!$model) {//No existe throw new NotFoundHttpException('No existe ese apunte'); } else { if($uid!=$model->usuarios_id) //No es mío throw new NotFoundHttpException('Acceso no permitido'); $model->load(Yii::$app->getRequest()->getBodyParams(), ''); if ($model->save()) { $response = Yii::$app->getResponse(); $response->setStatusCode(201); } return $model; } } } También podemos añadir nuevas acciones. Para ello, hay que incluirlas en rules.php, incluyendo también el métod OPTIONS en el caso de que la acción vaya por GET. Por ejemplo, para devolver datos estadísticos de entradas mediante una acción llamada entradas/estadistica: ... ['class' => 'yii\rest\UrlRule', 'controller' => ['entradas'], 'pluralize'=>false, 'extraPatterns'=>['OPTIONS estadistica'=>'estadistica','GET estadistica'=>'estadistica'] ], En la acción podemos devolver cualquier cosa: un array, un objecto, el resultado de una SQL: (Ver [[https://www.yiiframework.com/doc/guide/2.0/es/db-dao]] EntradasController extends ApiController ... public function actionEstadistica(){ return Yii::$app->db->createcommand('select fecha,count(*) num from entradas group by fecha order by fecha desc')->queryAll(); } ==== Acceso desde otros servidores. CRSF y CORS ==== Para evitar el problema de referencias cruzadas, CRSF, lo desactivaremos asignando a false $enableCsrfValidation en el controlador. use Yii; use yii\filters\Cors; class UsuariosController extends \yii\rest\ActiveController { public $enableCsrfValidation = false; public function behaviors() { $behaviors = parent::behaviors(); // Si queremos habilitar la autenticación mediante Bearer token $behaviors['authenticator'] = [ 'class' => HttpBearerAuth::className(), 'except' => ['options','authenticate'], ]; return $behaviors; } Si queremos habilitar el acceso a nuestra API desde otros servidores, podemos activar CORS public function behaviors() { $behaviors = parent::behaviors(); ... $behaviors['corsFilter'] = [ 'class' => Cors::className(), 'cors' => [ 'Origin' => ['*'], 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], 'Access-Control-Request-Headers' => ['*'], 'Access-Control-Allow-Credentials' => $this->authenable, ], ]; ... return $behaviors; } Para no tener que repetir esta configuración en todos los controladores, podemos crear una clase base, de la que deriven todos los controladores de nuestra API (salvo SiteController) ApiController.php: Cors::className(), 'cors' => [ 'Origin' => ['*'], 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], 'Access-Control-Request-Headers' => ['*'], 'Access-Control-Allow-Credentials' => !in_array($this->action->id,$this->authexcept), 'Access-Control-Max-Age' => 86400 ], ]; if (!$this->authenable) return $behaviors; $behaviors['authenticator'] = [ 'class' => HttpBearerAuth::className(), 'except' => array_merge(['options','authenticate'],$this->authexcept), ]; return $behaviors; } } y así, creamos el resto de controladores derivando de éste: Si en algún controlador necesitamos desactivar la autenticación del Bearer, pero mantener el chequeo de CORS, asignaremos la variable $authenable a false: Y si hay acciones que se han de autenticar y otras no, las definimos en la propiedad $authexcept: