RESTful API: ¿Cómo hacer un API con PHP y Mysql?

Continuando con nuestra serie de RESTful API y para que sirve. En este tercer artículo de la serie vamos a construir un API empleando PHP y MySql. Sabemos que es, para qué sirve y la estructura del mismo. Ahora pondremos en práctica nuestro conocimiento de programación orientada a objetos (OOP) con PHP.

Antes de empezar debo aclarar algo, para que un API sea RESTful debe permitir acceso y consumo del servicio vía HTTP. Es decir, desde cualquier navegador o herramienta que permita métodos HTTP como GET, POST, PUT y DELETE y devuelva rutas bajo HATEOAS. Para este ejemplo no emplearemos HEATEOAS ya que será un API donde la intención es proveer los URL como parte de una documentación, como lo hace Facebook, Google, etc; pero es importante que tengan esto presente al momento de hacer un API para producción.

Para este tutorial emplearemos la arquitectura SOA (Service-Oriented Architecture) o arquitectura fija que será documentada. Esta es la arquitectura más común de API en internet. Servicios como Facebook, Google, Microsoft incluso Oracle utilizan esta arquitectura, y si, puede ser incluida como RESTfull ya que las rutas (routes o endpoints) se proveen como documentación y no dentro de la misma respuesta.

1. Requerimientos

Para crear un API, necesitaremos algunas cosas:

  1. Servidor Web: Recomiendo Apache Web Server, pero pueden utilizar cualquier Web server que soporte PHP. Si vas a utilizar IIS, asegurate de utilizar PHP como FastCGI. Personalmente recomiento Wamp Server. Con Wamp puedes omitir instalar PHP y MySql ya que incluye la instalación de las tres herramientas.
  2. PHP: Necesitaremos la versión 5.5 o superior de PHP ya que tulizaremos Apache mod_rewrite.
  3. Mysql 5.6.x. Necesitaremos un motor de base de datos para almacenar la infromación que queremos proveer como servcio.
  4. Slim Framework: Esta librería permite crear rutas (routes) empleando mod_rewrite. Para este tutorial emplearemos la versión 2.6.1. Actualmente la 3.4.2 es la más reciente, puedes utilizar cualquier versión posterior a 2.6.1. Utilizo esta versión por que es más ligera y super sencilla de configurar. Slim será el corazón de nuestro API.
  5. Editor de Texto: Brackets es un editor de texto (código) Open Source muy flexible y poderoso. Puedes utilizar cualquier otro editor de textos que conoces o te sientas cómodo.
  6. RESTClient: Para acceder y probar nuestro API y realizar pruebas durante el desarrollo necesitaremos un cliente API. El plugin para firefox RESTClient es excelente para el trabajo. Puedes utilizar cualquier otro o incluso linea de comando o jquery.

2. Esquema

Como trabajaremos un API super ligero. Utilizaremos una estructura o esquema de archivos bastante simple. Puede hacer esta como se sientan cómodos. Recuerden que la estructura es solo la forma en que organizamos nuestro trabajo. Por lo general utilizo la estructura PHP Zend; pero para este tutotrial haremos algo sólo con los que necesitamos.

Crearemos un folder o directorio en el document root de nuestro Apache Webserver una vez todo esté instalado y configurado. Si optaron por Wamp, esto sería en C:\wamp\www, entonces, dentro de este folder creamos la siguiente estructura:

/restapi
- /include
-- Config.php
-- DbConnect.php
-- DbHandler.php
- /libs
-- /Slim (aquí la libreria Slim framework)
- /v1
-- index.php
-- .htaccess
index.html

Noten el folder “v1”, este folder será nuestro API URL. De esta forma podremos en el futuro hacer actualizaciones de nuestro API, por ejemplo “v2”, “v3”, etc. Esto es muy, muy útil cuando trabajamos con aplicaciones para dispositivos móviles o smartphones o aplicaiones corporativas de acceso remoto.

3. Configuración

La configuración de nuestro API consta de dos partes:

A) .htaccess En nuestro archivo .htaccess activaremos el mod_rewrite (verifica que está habilitado en http.conf o simplemente en el menu de Wamp). Dentro de este archivo tendremos las siguientes líneas para activar mod_rewrite y crear la conversión de ruta o routes de forma que el navegador permita url amigable:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ %{ENV:BASE}index.php [QSA,L]

B) Config.php donde definiremos los datos de conexión a la base de datos y el API Key que utilizaremos para validar el consumo del servicio. Este API Key puedes sustituirlo por un API Key en base de datos por cada cuenta o usuario de tu aplicación por medio de un método dentro del API. Este es el contenido de nuestro archivo Config.php:

define('DB_USERNAME', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', 'localhost');
define('DB_NAME', 'restapi');

//referencia generado con MD5(uniqid(<some_string>, true))
define('API_KEY','3d524a53c110e4c22463b10ed32cef9d');

Para generar el API Key puedes utilizar cualquier método de codificación. Para este ejemplo utilicé MD5() combinado con  uniqid() de PHP.

4. RESTfull API

Ahora estamos listos para iniciar la programación de nuestro RESTful API. Nuestro archivo /v1/index.php será nuestro controlador, por ende, aquí procesaremos la consulta o consumo de los diferentes servicios de nuestro API. Verificamos que todo marche bien corriendo nuestro wamp server y accediendo a la ruta http://localhost/restapi/v1/index.php desde RESTClient o simplemente desde tu navegador favorito. Recuerda que para nuestro API utilizaremos métodos GET y POST por lo que usaremos el plugin RESTClient.

Si no hay errores, estas bien. De haber alguno, puedes activar expose PHP y display errors en el Wamp desde el panel de administración haciendo click en el icono del task bar de Wamp Server.

Este API lo utilizaremos como un servicio de consulta de modelos de autos, con el cual crearemos un auto, consultaremos la lista de autos creados y veremos el detalle de un auto.

Una vez realizado a prueba y que todo marcha bien vamos a crear nuestro controlador utilizando Slim framework. Entonces, iniciaremos con nuestro archivo index.php (v1/index.php) tendría el siguiente código, el cual veremos paso a paso:

Primero, antes de procesar cualquier tipo de información debemos indicar si nuestro API será público, es decir, si el mismo podrá ser consumido desde cualquier URL o aplicación. Aquí entra CORS (Cross-Origin Resource Sharing). Mediante header() indicamos el control de acceso a nuestro API, permitiendo que sea consumido desde cualquier URL “*”.

También permitiremos el uso de credenciales o cabeceras invisibles para oAuth y cualquier otro tipo de control de credenciales que podamos necesitar. De igual forma indicaremos los métodos HTTP que permitiremos ser solicitados a nuestro API.

Indicaremos consultas a los headers o cabeceras de request e indicaremos la codificación charset que permita caracteres latinos o especiales como acentos, etc. Por último nos aseguraremos de que cualquier tipo de navegador web o cliente pueda acceder al API via P3P:

header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS');
header("Access-Control-Allow-Headers: X-Requested-With");
header('Content-Type: text/html; charset=utf-8');
header('P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"');

Luego, procederemos a cargar nuestro archivo de configuración para tener disponible cualqueir tipo de información como rutas, variables, constantes, etc., necesarias para el funcionamiento de nuestro API:

include_once '../include/Config.php';

Como último paso para los requerimientos básicos de funcionalidad, incluiremos la librería Slim y declararemos el Objeto que contendrá todos nuestros métodos como servicio:

require '../libs/Slim/Slim.php';
\Slim\Slim::registerAutoloader();
$app = new \Slim\Slim();

Ahora que tenemos las bases para crear nuestras rutas y métodos que serán los Endpoints o puntos de consumo de nuestro API.

Vamos a crear dos funciones utilitarias o helpers que nos servirán para mostrar la respuesta de nuestros métodos en formato JSON y para validar la consulta de nuestro API por medio del API KEY que hemos definido en nuestro archivo Config.php dentro de la carpeta include:

La función de ayuda echoResponse() para mostrar la respuesta de nuestros métodos o endpoints recibirá dos parámetros: status_code y response. status_code indicará el estado de respuesta basado en los códigos de respuesta http, es decir, 200 ok, 404 Not Found, 500 Internal Server Error, etc., y la respuesta misma en formato estructurado JSON.

function echoResponse($status_code, $response) {
$app = \Slim\Slim::getInstance();
// Http response code
$app->status($status_code);

// setting response content type to json
$app->contentType('application/json');

echo json_encode($response);
}

Esta función echoResponse, declara una instancia de la librería Slim cuyo objeto hemos declarado como $app previamente al inicio, luego procesa el status code y convierte el arreglo PHP de respuesta a JSON por medio de json_encode().

Nuestra segunda función authenticate(), nos permitirá validar la consulta a los métodos de nuestro API como una forma de seguridad y así garantizar que controlemos quien puede acceder y consumir el API partiendo de que hemos declarado un Access-Control-Allow-Origin:*. Esto es muy similar a como se utiliza cualquier API disponible como Facebook y Google. Este API KEY puede ser sustituido por un valor de una base de datos si deseas utilizar acceso controlado o RBAC en tu API.

function authenticate(\Slim\Route $route) {
// Getting request headers
$headers = apache_request_headers();
$response = array();
$app = \Slim\Slim::getInstance();

// Verifying Authorization Header
if (isset($headers['Authorization'])) {
//$db = new DbHandler(); //utilizar para manejar autenticacion contra base de datos

// get the api key
$token = $headers['Authorization'];

// validating api key
if (!($token == API_KEY)) { //API_KEY declarada en Config.php

// api key is not present in users table
$response["error"] = true;
$response["message"] = "Acceso denegado. Token inválido";
echoResponse(401, $response);

$app->stop(); //Detenemos la ejecución del programa al no validar

} else {
//procede utilizar el recurso o metodo del llamado
}
} else {
// api key is missing in header
$response["error"] = true;
$response["message"] = "Falta token de autorización";
echoResponse(400, $response);

$app->stop();
}
}

Como podemos ver, a la función echoResponse() le pasamos solo un valor de $route, que es un método reservado de Slim Framwork que es definido para la ruta de acceso o endpoint que se consumirá. Por ejemplo, http://localhost/restapi/v1/auto, siendo /auto nuestro endpoint o método de consumo. Capturamos las cabeceras o headers, en este caso apache_request_headers() ya que estamos utilizando Wamp o cualquier otro paquete de desarrollo *AMP (LAMP, WAMP).

Declaramos un arreglo $response que utilizaremos como variable para almacenar la respuesta de nuestros métodos y volvemos a instanciar Slim en la variable $app que contiene nuestro servicio con el objeto Slim al cargar la clase maestra de la librería.

Posteriormente, consultareos si existe la cabecera “Authorization” dentro de las cabeceras $headers que hemos capturado previamente de apache_request_headers().  Esta clave “Autorization”es la que utilizaremos para enviar nuestro API_KEY como cabecera al momento de consumir o consultar cualquiera de los métodos o endpoints de nuestro API desde el plugin RESTClient o cualquier otro cliente.

Les recomiendo poner estas funciones al final del archivo. De esta froma mantendremos nuestro archivo index.php organizado.

Bien, ya tenemos las funcones de ayuda necearias para validar el consumo de nuestro API y para mostrar los resultados en JSON junto a el código de respuesta de forma que el cliente sepa que hacer con la respuesta. Por ejemplo, si es un status 200 que procese la respuesta, si es un 404 que muestre un mensaje de “no encontrado”y evitar procesar cualquier tipo de datos innecesarios optimizando así el tiempo de respuesta.

Aunque hemos incluido soporte para Mysql y declarado varios métodos HTTP solo voy a mostrarles dos de estos métodos y dejaré que hagan los demás como práctica para que entienda mejor el funcionamiento de un API. De igual forma, no voy a utilizar una base de datos en este ejemplo; pero si les he dado las bases para utilizarla con los archivos DBConnect.php y DBHandler.php. Y si notan en la función de ayuda authenticate he comentado la línea $db = new DbHandler(); que les ayudaráa inicializar su conexión a la base de datos y posterior uso de la misma.

Vamos a crear el primer método o endpoint de nuestro API el cual nos responderá una lista de autos previamente declarada como un arreglo por medio e una consulta GET:

$app->get('/auto', function () {

$response = array();
//$db = new DbHandler();

/* Array de autos para ejemplo response
* Puesdes usar el resultado de un query a la base de datos mediante un metodo en DBHandler
**/
$autos = array(
array('make'=>'Toyota', 'model'=>'Corolla', 'year'=>'2006', 'MSRP'=>'18,000'),
array('make'=>'Nissan', 'model'=>'Sentra', 'year'=>'2010', 'MSRP'=>'22,000')
);

$response["error"] = false;
$response["message"] = "Autos cargados: " . count($autos); //podemos usar count() para conocer el total de valores de un array
$response["autos"] = $autos;

echoResponse(200, $response);
});

Como verán, nuestro endpoint es /autos el cual hemos declarado como un método de $app. Declarmos una variable $response nuevamente y tenemos un arreglo con dos elementos. Cada elemento un es arreglo de referencia “key=value” con datos de autos.

Luego, extendemos nuestro arreglo $response con tres claves (error, message, autos). La clave error es un booleano que nos indicará si hay un error en el procesamiento. Message es un mensaje de referencia que nos indicará que si hay un error (error = true) nos dirá que tipo de error, esto claro previamente definido o cualquier otro mensaje. La clave autos son los resultados de nuestro procesamiento. Como esperamos una lista de autos “GET”, es de esperar que los valores vengan representados por la referencia de los datos.

Guaramos nuestro progreso y hacemos una prueba. Vamos al utilizar RESTClient para consultar el método que acabamos de hacer y ver los resultados:

restapi

Si has obtenido un resultado como muestra la imagen anterior, estamos bien, de lo contrario puedes revisar el código consultado la misma URL desde tu navegador, ya que como es una consulta GET, es la consulta que utiliza cualquier navegador para visualizar una página web.

Ahora vamos a crear el segundo método o endpoint que nos permitirá enviar datos a nuestro API utilizando el método HTTP POST, para por ejemplo crear un auto el cual podamos almacenar en una base de datos o realizar cualquier otro proceso con los datos recibidos.

Para hacer esto vamos a crear una nueva función utilitaria o helper. Como vamos a recibir información, es buena práctica validar todo lo que entra a nuestro API como a cualquier otro tipo de formas. Vamos a crear una función que valide los campos que necesitamos recibir en nuestro servicio para procesar la data. Algo así como campos requeridos:

function verifyRequiredParams($required_fields) {
$error = false;
$error_fields = "";
$request_params = array();
$request_params = $_REQUEST;
// Handling PUT request params
if ($_SERVER['REQUEST_METHOD'] == 'PUT') {
$app = \Slim\Slim::getInstance();
parse_str($app->request()->getBody(), $request_params);
}
foreach ($required_fields as $field) {
if (!isset($request_params[$field]) || strlen(trim($request_params[$field])) <= 0) {
$error = true;
$error_fields .= $field . ', ';
}
}

if ($error) {
// Required field(s) are missing or empty
// echo error json and stop the app
$response = array();
$app = \Slim\Slim::getInstance();
$response["error"] = true;
$response["message"] = 'Required field(s) ' . substr($error_fields, 0, -2) . ' is missing or empty';
echoResponse(400, $response);

$app->stop();
}
}

Esta función verifyRequiredParams() validará los campos que marcaremos como requeridos. Si alguno de los campos requeridos no está presente, indicaremos al cliente que hay un error 400 como status_code y un mnesaje indicando que existen campos requeridos y enumeramos cuales son para que el cliente provea los datos requeridos. Ahora procedemos a crear nuestro método POST para el mismo endpoint o servicio /auto.

$app->post('/auto', 'authenticate', function() use ($app) {
// check for required params
verifyRequiredParams(array('make', 'model', 'year', 'msrp'));

$response = array();
//capturamos los parametros recibidos y los almacxenamos como un nuevo array
$param['make'] = $app->request->post('make');
$param['model'] = $app->request->post('model');
$param['year'] = $app->request->post('year');
$param['msrp'] = $app->request->post('msrp');

/* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
//$db = new DbHandler();

/* Podemos crear un metodo que almacene el nuevo auto, por ejemplo: */
//$auto = $db->createAuto($param);

if ( is_array($param) ) {
$response["error"] = false;
$response["message"] = "Auto creado satisfactoriamente!";
$response["auto"] = $param;
} else {
$response["error"] = true;
$response["message"] = "Error al crear auto. Por favor intenta nuevamente.";
}
echoResponse(201, $response);
});

Lo que acabamos de hacer es una de las mayores ventajas de los REST API, utilizar múltiples métodos HTTP para un mismo recurso o servicio. En este caso /auto la consultamos con GET y con POST enviamos datos. De igual forma podmeos utilizar PUT y DELETE como métodos de consulta y así indicar en nuestra programación que hacer en cada consulta según el método HTTP utilizado.

Finalmente, ejecutamos la instancia de $app que hemos declarado en cada método creado:

$app->run();

Nuestro archivo index.php tendría el siguiente código:

<?php
/**
*
* @About: API Interface
* @File: index.php
* @Date: $Date:$ May-2016
* @Version: $Rev:$ 1.0
* @Developer: Federico Guzman ([email protected])
**/

/* Los headers permiten acceso desde otro dominio (CORS) a nuestro REST API o desde un cliente remoto via HTTP
* Removiendo las lineas header() limitamos el acceso a nuestro RESTfull API a el mismo dominio
* Nótese los métodos permitidos en Access-Control-Allow-Methods. Esto nos permite limitar los métodos de consulta a nuestro RESTfull API
* Mas información: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
**/
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS');
header("Access-Control-Allow-Headers: X-Requested-With");
header('Content-Type: text/html; charset=utf-8');
header('P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"');

include_once '../include/Config.php';

/* Puedes utilizar este file para conectar con base de datos incluido en este demo;
* si lo usas debes eliminar el include_once del file Config ya que le mismo está incluido en DBHandler
**/
//require_once '../include/DbHandler.php';

require '../libs/Slim/Slim.php';
\Slim\Slim::registerAutoloader();
$app = new \Slim\Slim();
/* Usando GET para consultar los autos */
$app->get('/auto', function () {

$response = array();
//$db = new DbHandler();

/* Array de autos para ejemplo response
* Puesdes usar el resultado de un query a la base de datos mediante un metodo en DBHandler
**/
$autos = array(
array('make'=>'Toyota', 'model'=>'Corolla', 'year'=>'2006', 'MSRP'=>'18,000'),
array('make'=>'Nissan', 'model'=>'Sentra', 'year'=>'2010', 'MSRP'=>'22,000')
);

$response["error"] = false;
$response["message"] = "Autos cargados: " . count($autos); //podemos usar count() para conocer el total de valores de un array
$response["autos"] = $autos;

echoResponse(200, $response);
});

/* Usando POST para crear un auto */

$app->post('/auto', 'authenticate', function() use ($app) {
// check for required params
verifyRequiredParams(array('make', 'model', 'year', 'msrp'));

$response = array();
//capturamos los parametros recibidos y los almacxenamos como un nuevo array
$param['make'] = $app->request->post('make');
$param['model'] = $app->request->post('model');
$param['year'] = $app->request->post('year');
$param['msrp'] = $app->request->post('msrp');

/* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
//$db = new DbHandler();

/* Podemos crear un metodo que almacene el nuevo auto, por ejemplo: */
//$auto = $db->createAuto($param);

if ( is_array($param) ) {
$response["error"] = false;
$response["message"] = "Auto creado satisfactoriamente!";
$response["auto"] = $param;
} else {
$response["error"] = true;
$response["message"] = "Error al crear auto. Por favor intenta nuevamente.";
}
echoResponse(201, $response);
});

/* corremos la aplicación */
$app->run();

/*********************** USEFULL FUNCTIONS **************************************/

/**
* Verificando los parametros requeridos en el metodo o endpoint
*/
function verifyRequiredParams($required_fields) {
$error = false;
$error_fields = "";
$request_params = array();
$request_params = $_REQUEST;
// Handling PUT request params
if ($_SERVER['REQUEST_METHOD'] == 'PUT') {
$app = \Slim\Slim::getInstance();
parse_str($app->request()->getBody(), $request_params);
}
foreach ($required_fields as $field) {
if (!isset($request_params[$field]) || strlen(trim($request_params[$field])) <= 0) {
$error = true;
$error_fields .= $field . ', ';
}
}

if ($error) {
// Required field(s) are missing or empty
// echo error json and stop the app
$response = array();
$app = \Slim\Slim::getInstance();
$response["error"] = true;
$response["message"] = 'Required field(s) ' . substr($error_fields, 0, -2) . ' is missing or empty';
echoResponse(400, $response);

$app->stop();
}
}

/**
* Validando parametro email si necesario; un Extra ;)
*/
function validateEmail($email) {
$app = \Slim\Slim::getInstance();
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$response["error"] = true;
$response["message"] = 'Email address is not valid';
echoResponse(400, $response);

$app->stop();
}
}

/**
* Mostrando la respuesta en formato json al cliente o navegador
* @param String $status_code Http response code
* @param Int $response Json response
*/
function echoResponse($status_code, $response) {
$app = \Slim\Slim::getInstance();
// Http response code
$app->status($status_code);

// setting response content type to json
$app->contentType('application/json');

echo json_encode($response);
}

/**
* Agregando un leyer intermedio e autenticación para uno o todos los metodos, usar segun necesidad
* Revisa si la consulta contiene un Header "Authorization" para validar
*/
function authenticate(\Slim\Route $route) {
// Getting request headers
$headers = apache_request_headers();
$response = array();
$app = \Slim\Slim::getInstance();

// Verifying Authorization Header
if (isset($headers['Authorization'])) {
//$db = new DbHandler(); //utilizar para manejar autenticacion contra base de datos

// get the api key
$token = $headers['Authorization'];

// validating api key
if (!($token == API_KEY)) { //API_KEY declarada en Config.php

// api key is not present in users table
$response["error"] = true;
$response["message"] = "Acceso denegado. Token inválido";
echoResponse(401, $response);

$app->stop(); //Detenemos la ejecución del programa al no validar

} else {
//procede utilizar el recurso o metodo del llamado
}
} else {
// api key is missing in header
$response["error"] = true;
$response["message"] = "Falta token de autorización";
echoResponse(400, $response);

$app->stop();
}
}
?>

Puedes descargar el proyecto restapi como archivo zip. O bien puedes visitar el Github de Weblantropia donde puedes descargar el Git con el Server y un cliente Javascript (AngularJS) para crear tu propio RESTful API.

Si realizamos la consulta sin enviar la credencial Authorization que hemos declarado como valor de credencial en la función de ayuda o preprocesamiento “authenticate”, nos dará status 400 el mensaje “Falta token de autorización” como hemos declarado en la función de preprocesamiento “authenticate”.

restapi_post_no_token

La respuesta sería:

restapi_post_no_token_response

Para hacer la consulta POST, necesitamos enviar dos valores de cabecera. Primero que estamos enviando un arreglo POST:

Content-Type: application/x-www-form-urlencoded

Y, por supuesto, nuestro API_KEY:

Authorization: 3d524a53c110e4c22463b10ed32cef9d

Si no enviamos los parámetros requeridos, la respuesta nos indicará cuales campos necesitamos enviar, como lo definimos en la función de ayuda verifyRequiredParams():

restapi_post_no_params

Al pasar los campos requeridos y validados por nuestra función verifyRequiredParams(), obtendremos el resultado esperado. Un response header 200 OK con la respuesta en formato JSON:

restapi_post_response

Para más información sobre RESTful API pueden visitar este artículo (inglés) de Ben Morris:
http://www.ben-morris.com/the-restafarian-flame-wars-common-points-of-disagreement-over-rest-api-design/

Pueden ver la siguiente presentación sobre diseño de RESTful API: