Servicio Web RESTful Para Android Con Php, Mysql y Json

Hasta ahora hemos visto como un servicio web puede comunicar aplicaciones creadas con distintas tecnologías con el fin de centralizar los datos de nuestros usuarios. Pero cuando la complejidad aumenta es beneficioso usar estilos como RESTful, SOAP, RPC, etc.

Un servicio web RESTful es una aplicación que se crea a partir de los principios de REST, el cual es un conjunto de ideas para transferir recursos de una forma elegante.

En este artículo veremos cómo aplicar los principios de esta arquitectura para mejorar la legibilidad y escalabilidad de nuestros servicios web.

Aprenderás sobre la importancia del ciclo petición-respuesta dentro de REST, el uso de los métodos de petición, las cabeceras y el rol que juegan los códigos de estado.

Además verás cómo usar múltiples formatos de comunicación como lo son Json y Xml para compartir recursos.

Para consolidar todos estos elementos he decido crear una API REST sobre una base de datos de contactos. La finalidad es consumir los datos desde una aplicación Android para aprovechar el servicio.

Descargar Código Del Proyecto Android Y El Servicio Web

Desbloquea el código del tutorial suscribiéndote al boletín informativo de Hermosa Programación:

¿Qué Es Un Servicio Web RESTful?

La sigla REST significa REpresentational State Transfer y en mis traducciones significaría Transferencia De Estado Representacional.

Como dije anteriormente REST es un estilo de arquitectura. Dio luz por primera vez en un consolidado de Roy Thomas Fielding en la Universidad de California para explicar un nuevo enfoque de la transferencia de recursos entre clientes y servidores a través de HTTP.

Este estilo se enfoca en el recurso como unidad fundamental de los servicios web y estandariza de forma amigable su transferencia entre aplicaciones.

Un recurso es todo aquello relacionado con datos que interesen para comunicar. Puede referirse a un registro de la base de datos como lo sería cada contacto.

De forma extendida un conjunto de recursos del mismo tipo se les llama colecciones. Por lo que al obtener un conjunto de contactos estaríamos hablando de una colección.

Estructura de urls en un web service RESTful

REST utiliza formatos de url para hacerlas más amigables a la vista de quienes desean acceder a la API. Esto con el fin de que sean predecibles y claras ante la vista de un humano, lo que permite navegar a través de los recursos de forma intuitiva.

Por ejemplo, el API de Twitter se refiere a los seguidores de un usuario con la siguiente url:

GET https://api.twitter.com/1.1/followers

O para averiguar los retweets la url estaría construida de la siguiente forma:

GET https://api.twitter.com/1.1/statuses/retweets/:id.json

Normalmente el patrón que se sigue está dado por

version/recurso/identificador

Sin embargo todo depende de cada desarrollador extender los accesos en la ruta de la url.

También puedes hacer uso de parámetros para determinar las consultas que se harán a la base de datos, como la siguiente url solo obtiene 5 registros de la línea de tiempo del home.

GET https://api.twitter.com/1.1/statuses/home_timeline.json?count=5

La anterior Url produciría una respuesta Json como estructura del recurso similar a la siguiente (con autenticación de la cuenta de Hermosa Programación @herprogramacion):

<code><span class="array">[<span class="content">
  <span class="tag">{
    "<span class="param">created_at</span>": "<span class="param">Thu Sep 24 16:08:37 +0000 2015</span>",
    "<span class="param">id</span>": 647080203756380200,
    "<span class="param">id_str</span>": "<span class="param">647080203756380160</span>",
    "<span class="param">text</span>": "<span class="param">"Información oportuna es lo más importante para dar ideas TIC que solucionen problemas del ciudadano de a pie" @MCarolinaHoyosT #Ciudades_i</span>",
    "<span class="param">source</span>": "<span class="param"><a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a></span>",
    "<span class="param">truncated</span>": false,
    "<span class="param">in_reply_to_status_id</span>": null,
    "<span class="param">in_reply_to_status_id_str</span>": null,
    "<span class="param">in_reply_to_user_id</span>": null,
    "<span class="param">in_reply_to_user_id_str</span>": null,
    "<span class="param">in_reply_to_screen_name</span>": null,
    "<span class="param">user</span>": {
      "<span class="param">id</span>": 142454580,
      "<span class="param">id_str</span>": "<span class="param">142454580</span>",
      "<span class="param">name</span>": "<span class="param">MinTIC</span>",
      "<span class="param">screen_name</span>": "<span class="param">Ministerio_TIC</span>",
      "<span class="param">location</span>": "<span class="param">Colombia</span>",
      "<span class="param">description</span>": "<span class="param">Cumplimos lo que prometimos. Hoy el Plan #ViveDigital es una realidad http://t.co/yi7dW6HlON</span>",
      "<span class="param">url</span>": "<span class="param">http://t.co/JrwT209U5H</span>",
      "<span class="param">entities</span>": {
        "<span class="param">url</span>": {
          "<span class="param">urls</span>": [
            {
              "<span class="param">url</span>": "<span class="param">http://t.co/JrwT209U5H</span>",
              "<span class="param">expanded_url</span>": "<span class="param">http://www.mintic.gov.co</span>",
              "<span class="param">display_url</span>": "<span class="param">mintic.gov.co</span>",
              "<span class="param">indices</span>": [
                0,
                22
              ]
            }
          ]
        },
        "<span class="param">description</span>": {
          "<span class="param">urls</span>": [
            {
              "<span class="param">url</span>": "<span class="param">http://t.co/yi7dW6HlON</span>",
              "<span class="param">expanded_url</span>": "<span class="param">http://bit.ly/PlanViveDigital2018</span>",
              "<span class="param">display_url</span>": "<span class="param">bit.ly/PlanViveDigita…</span>",
              "<span class="param">indices</span>": [
                70,
                92
              ]
            }
          ]
        }
      },
      "<span class="param">protected</span>": false,
      "<span class="param">followers_count</span>": 490603,
      "<span class="param">friends_count</span>": 3630,
      "<span class="param">listed_count</span>": 2157,
      "<span class="param">created_at</span>": "<span class="param">Mon May 10 23:08:39 +0000 2010</span>",
      "<span class="param">favourites_count</span>": 2582,
      "<span class="param">utc_offset</span>": -18000,
      "<span class="param">time_zone</span>": "<span class="param">Bogota</span>",
      "<span class="param">geo_enabled</span>": true,
      "<span class="param">verified</span>": true,
      "<span class="param">statuses_count</span>": 67278,
      "<span class="param">lang</span>": "<span class="param">es</span>",
      "<span class="param">contributors_enabled</span>": false,
      "<span class="param">is_translator</span>": false,
      "<span class="param">is_translation_enabled</span>": false,
      "<span class="param">profile_background_color</span>": "<span class="param">FCFCFC</span>",
      "<span class="param">profile_background_image_url</span>": "<span class="param">http://pbs.twimg.com/profile_background_images/531828985588506624/Cpp9fqWP.png</span>",
      "<span class="param">profile_background_image_url_https</span>": "<span class="param">https://pbs.twimg.com/profile_background_images/531828985588506624/Cpp9fqWP.png</span>",
      "<span class="param">profile_background_tile</span>": true,
      "<span class="param">profile_image_url</span>": "<span class="param">http://pbs.twimg.com/profile_images/646831264356372480/ah11nlO9_normal.png</span>",
      "<span class="param">profile_image_url_https</span>": "<span class="param">https://pbs.twimg.com/profile_images/646831264356372480/ah11nlO9_normal.png</span>",
      "<span class="param">profile_banner_url</span>": "<span class="param">https://pbs.twimg.com/profile_banners/142454580/1443029544</span>",
      "<span class="param">profile_link_color</span>": "<span class="param">84329B</span>",
      "<span class="param">profile_sidebar_border_color</span>": "<span class="param">000000</span>",
      "<span class="param">profile_sidebar_fill_color</span>": "<span class="param">C0DFEC</span>",
      "<span class="param">profile_text_color</span>": "<span class="param">333333</span>",
      "<span class="param">profile_use_background_image</span>": true,
      "<span class="param">has_extended_profile</span>": false,
      "<span class="param">default_profile</span>": false,
      "<span class="param">default_profile_image</span>": false,
      "<span class="param">following</span>": true,
      "<span class="param">follow_request_sent</span>": false,
      "<span class="param">notifications</span>": false
    },
    "<span class="param">geo</span>": null,
    "<span class="param">coordinates</span>": null,
    "<span class="param">place</span>": null,
    "<span class="param">contributors</span>": null,
    "<span class="param">is_quote_status</span>": false,
    "<span class="param">retweet_count</span>": 1,
    "<span class="param">favorite_count</span>": 1,
    "<span class="param">entities</span>": {
      "<span class="param">hashtags</span>": [
        {
          "<span class="param">text</span>": "<span class="param">Ciudades_i</span>",
          "<span class="param">indices</span>": [
            128,
            139
          ]
        }
      ],
      "<span class="param">symbols</span>": [],
      "<span class="param">user_mentions</span>": [
        {
          "<span class="param">screen_name</span>": "<span class="param">MCarolinaHoyosT</span>",
          "<span class="param">name</span>": "<span class="param">ViceMinistra TIC</span>",
          "<span class="param">id</span>": 148183388,
          "<span class="param">id_str</span>": "<span class="param">148183388</span>",
          "<span class="param">indices</span>": [
            111,
            127
          ]
        }
      ],
      "<span class="param">urls</span>": []
    },
    "<span class="param">favorited</span>": false,
    "<span class="param">retweeted</span>": false,
    "<span class="param">lang</span>": "<span class="param">es</span>"
  }</span>,
  <span class="tag">{}</span>,
  <span class="tag">{}</span>,
  <span class="tag">{}</span>,
  <span class="tag">{}</span>
</span>]</span></code>

La idea es que cada url exprese de forma limpia el recurso o colección al que se operará y las condiciones especiales para acceder a los registros necesarios.

Si deseas puedes explorar las documentaciones de varias apis en PublicAPIs. Este repositorio tiene el registro de varios sitios que te proporcionarán acceso a gran variedad de datos orientados a negocios o información general.

Repositorio de APIs PublicAPIs

Formato de datos para la transferencia

Es muy común que un servicio web RESTful ofrezca distintos formatos para la comunicación de datos. Los más frecuentes son Json y XML, sin embargo pueden usarse distintas variedades como HTML, CSV, PDF, etc.

Recuerda que la cabecera Content-Type se basa en un registro estándar de tipos llamados MIME TYPES. Simplemente son valores de texto asociados a un significado predefinido por las regulaciones de la web para usarse como la manera correcta de representar datos.

Para Json la cabecera se define con el siguiente tipo.

Content-Type: application/json

Y para XML con:

Content-Type: text/xml

Normalmente el formato se especifica en la cabecera de tipo de contenido, pero también es posible usarlo a través de parámetros en la url como se mostraba en la API de Twitter.

Operaciones de datos con los métodos HTTP

En REST cada uno de los verbos que se usan en las peticiones equivale a una acción sobre un registro o colección.

Como ya sabes, las acciones más frecuentes se representan en la sigla CRUD (create, read, update y delete). Estas cuatro operaciones básicas tienen asignados los siguientes métodos:

Método Acción Seguro Idempotente
GET Obtiene un recurso o una colección Si Si
POST Crea un nuevo recurso No No
PUT Actualiza un recurso específico No Si
DELETE Elimina un recurso específico No Si
PATCH Actualiza parcialmente un recurso especifico No No

La columna Seguro indica si el método no es propenso a la alteración de datos, donde el único que cumple esta condición es GET debido a que solo obtiene recursos sin ningún cambio. Por el otro lado, la creación, modificación y eliminación se basan en cambiar los recursos, así que son propensos a generar inconsistencias en la base de datos.

Ahora en la columna Idempotente se especifica la capacidad de un método para no repetir una misma acción. Por ejemplo, DELETE  es idempotente ya que al eliminarse un recurso no es posible volverlo a hacer.

En contraste, POST puede crear un nuevo recurso y si es llamado de nuevo, entonces creará otro, por lo que al utilizarlo sucesivamente siempre habrá un efecto.

Uso de códigos de estado para respuestas

REST utiliza los códigos de estado de HTTP para determinar si una respuesta fue exitosa o fallida dependiendo del inconveniente sucedido.

Aunque existen gran cantidad de códigos de estados, estos pueden ser clasificados dependiendo del componente que generó el error. Los de la familia 2xx indican que la respuesta tuvo éxito, los de la familia 3xx indican que es necesaria una acción adicional para que el servidor complete la respuesta.

También existen los del conjunto 4xx para representar un error por parte del cliente y los 5xx para errores del servidor. La siguiente tabla tiene algunos de los métodos que más populares en un servicio web RESTful.

Código Significado Utilidad
200 OK Úsalo para especificar que un recurso o colección existe
201 Created Puedes usarlo para especificar que se creó un recurso. Se puede complementar con el header Location para especificar la URI hacia el nuevo recurso.
204 No Content Representa un resultado exitoso pero sin retorno de algún dato (viene muy bien en DELETE).
304 No Modified Indica que un recurso o colección no ha cambiado.
401 Unauthorized Indica que el cliente debe estar autorizado primero antes de realizar operaciones con los recursos
404 Not Found Ideal para especificar que el recurso buscado no existe
405 Method Not Allowed Nos permite expresar que el método relacionado a la url no es permitido en la api
422 Unprocessable Entity Va muy bien cuando los parámetros que se necesitaban en la petición no están completos o su sintaxis es la incorrecta para procesar la petición.
429 Too Many Requests Se usa para expresarle al usuario que ha excedido su número de peticiones si es que existe una política de límites.
500 Internal Server Error Te permite expresar que existe un error interno del servidor.
503 Service Unavailable Este código se usa cuando el servidor esta temporalmente fuera de servicio.

Con estas referencias puedes construir respuestas que los clientes puedan interpretar de la mejor manera. Es superimportante que envíes el cuerpo en el formato que espera el cliente, es decir, si definiste Json, entonces que este sea la regla.

Dependiendo del alcance de tu API, así mismo será el detalle de las respuestas que se produzcan por errores. Lo ideal sería mostrar una descripción corta del problema, un código de referencia y una posible solución a este.

Por ejemplo…

Supongamos que en una inserción el campo del correo del contacto es obligatorio. Teniendo en cuenta es restricción crearíamos un cuerpo de respuesta que ayude al usuario a visualizar su equivocación.

{  
   "estado":"34",
   "mensaje":"Campo requerido: correo contacto"
}

Utilidad de las cabeceras en servicios RESTful

Recuerda que las cabeceras o headers son componentes de las peticiones y respuestas para expresar configuraciones asociadas a cada operación.

Aunque existe una gran lista de ellas con distintos propósitos, me gustaría enfocarme en la importancia de la autorización y el almacenaminento en cache.

Autorización— Recuerda que la autorización es la entrega de permisos a un usuario para que acceda a un recurso luego de comprobar la validez de sus credenciales (Autenticación).

A través de la cabecera Authorization es posible enviar las credenciales del usuario, una clave única de acceso a la API, un token de autorización, etc. Todo depende del tipo de autenticación que vayas a realizar.

Un ejemplo de esta cabecera sería el siguiente. En él ves cómo se envía una clave en la petición del cliente para que el servidor otorgue los permisos necesarios.

Authorization: QWxhZGRpbjpvcGVuIHNlc2FtZQ

Si usas Php podrás obtener el conteido de las cabeceras con getallheaders() o el método apache_response_headers(). Luego puedes extraer el contenido de la autorización con la clave ‘authorization’ y realizar una validación.

En nuestro caso usaremos la generación de una clave manual a través de los encriptados de Php para no extender el artículo. Sin embargo tú puedes elegir frameworks de manejo de autorización, el protocolo abierto de seguridad OAuth 2.0 o servicios como Stormpath.

Caching— Con las cabeceras de cache podemos mejorar el rendimiento de un servicio web RESTful cuando este recibe volúmenes grandes de peticiones.

Aunque la aplicación de este concepto no lo veremos en este artículo, puedes hacerte una idea con un ejemplo del uso de los headers.

Supón que a través de una petición GET hacia los contactos hemos obtenido una lista de 100 registros. Pasado un tiempo la aplicación Android intenta refrescar los registros obtenidos enviando de nuevo una petición GET.

El comportamiento más básico sería dejar que el servicio web envíe de nuevo los 100 registros para comparar cuáles han cambiado y así actualizar la vista.

Pero ¿y si la respuesta no ha cambiado?… ¿es posible usar la caché para optimizar el rendimiento?

Por supuesto. Si marcas el recurso o colección con su último estado y envías este dato en la cabecera Last-Modified (última fecha de actualización) o ETag (última representación hash del recurso), podrás comparar la versión actual con la anterior y determinar si es necesario realizar un proceso de actualización. Con ello evitarás cargas excesivas de trabajo.

En este caso el servidor REST podría enviar un estado 304 de “No modified” sin ningún contenido en el cuerpo que sobrecargue el ancho de banda.

Planificación Del Servicio Web RESTful

La aplicación Android que construiremos requiere que el servicio web le posibilite centralizar la base de datos en un servidor remoto y que permita realizar las 4 operaciones básicas sobre estos datos.

También es necesario que el usuario cree primero una cuenta y luego se loguee para poder obtener una clave de acceso a la API. Sin estas condiciones el usuario no tendrá acceso a la información.

En cuanto a la estructura del servicio, nos guiaremos en un estilo sencillo MVC para manejar las peticiones del cliente. Así que los pasos de desarrollo quedarían de la siguiente forma:

  • Configuración para desarrollo web
  • Diseño e implementación de la base de datos
  • Realización de conexión Mysql con Php
  • Creación de los modelos de datos
  • Definición de las vistas
  • Manejo de las llamadas al servicio web RESTful (CRUD)
  • Realizar pruebas al servicio web

¡Comencemos!

1. Configuración De Herramientas De Desarrollo

En este artículo trabajaremos con nuestra PC como servidor local de pruebas sin tener en cuenta el despliegue del servicio web en un servicio de hosting o computación en la nube.

Para ello usaremos XAMPP como entorno para el desarrollo web y pruebas. Solo entra a la página del enlace y descarga la versión para tu sistema operativo.

Descargar Xampp En Diferentes Versiones De Sistemas Operativos

Para configurar el localhost puedes seguir el artículo Tutorial de XAMPP: Cómo Usar XAMPP Para Ejecutar Su Propio Servidor Web.

Una vez que hayas probado que el entorno está 100% funcional, entonces puedes tomar la decisión sobre el editor o IDE que usarás para la creación del proyecto.

En mi caso usaré PhpStorm ya que me parece una herramienta que soporta perfectamente todas las etapas del desarrollo web con Php. Además trae consigo múltiples funcionalidades del framework de los productos IntelliJ como refactorización, inspecciones, creación de tests, despliegue de características, etc.

Descargar PhpStorm De JetBrains Intellij

Sin importar la herramienta que uses, crea un nuevo proyecto con la siguiente estructura de directorios dentro de la ruta C:\xamp\htdocs (dirección de instalación por defecto del XAMPP, si elegiste otra, entonces modifícala) .

Estructura Proyecto Php Para Servicio Web REST

Usaremos como carpeta raíz un subdominio hipotético para el api REST llamado api.peopleapp.com. Dentro de este añadiremos una carpeta para la primera versión (v1). En su interior tendremos 3 subdirectorios y el archivo index.php,

La carpeta controladores contiene cada uno de las clases que procesan las llamadas de cada recurso. datos contiene los archivos de conexión de bases de datos y las constantes de conexión. En utilidades pondremos todos aquellos scripts que ayuden de forma parcial.

Y en vistas estarán las clases que permiten escribir el cuerpo de las respuestas en los formatos de datos requeridos.

Crear archivo .htaccess

El siguiente paso es crear un archivo .htaccess para indicarle al servidor Apache que deseamos tener Urls donde los archivos no incluyan extensiones.

En Windows la creación de este archivo presenta problemas al realizarla de la forma tradicional. Por lo que tenemos que hacerlo a través de línea de comandos.

Para ello crea en la carpeta v1 un nuevo archivo de texto con el nombre htaccess.txt y luego a través de la consola de comandos llega hasta su ubicación. Una vez allí usa el comando rename de la siguiente manera.

rename htaccess.txt .htaccess

De esta forma conseguirás que el archivo sea reconocido en el sistema de archivos de Apache. Ahora agrega las siguientes instrucciones.

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?PATH_INFO=$1 [L,QSA]

La anterior solución hace parte del módulo mod_rewrite de Apache. No mostraré en detalle la sintaxis de este componente, sin embargo puedes visitar la documentación oficial sobre su uso. Incluso puedes buscar por herramientas que generen automáticamente estas reglas como .htaccess Generator.

A groso modo las instrucciones le dicen al servidor que cuando no se encuentre un archivo o directorio, realice un redireccionamiento al index.php. Adicionalmente podremos procesar la ruta de la url a través del parámetro PATH_INFO que se agregó.

Si todo salió bien, comprueba imprimiendo el valor de PATH_INFO desde tu archivo index.php accediendo a la url http://localhost/api.peopleapp.com/v1/contactos/1.

<?php

print $_GET['PATH_INFO'];

El resultado sería el string

contactos/1

Herramientas para probar el servicio web REST

Los tests son importantes si deseas verificar el comportamiento de tu servicio web sin tener que usar la vista de la app o crear simulaciones propias de peticiones.

Para ello existen varias utilidades online que puedes permitir probar las funcionalidades de un api REST. En nuestro usaremos una extensión muy popular de Google Chrome llamada Advanced REST Client.

Extensión De Chrome Advance REST Client

Solo basta con añadirla al navegador para tener un cliente REST con muchas características.

Por otro lado, PhpStorm también tiene una herramienta integrada llamada Test RESTful Web Service que también puede ser de utilidad si usas este IDE.

Test RESTful Web Service En PhpStorm

Sin importar la herramienta que hayas elegido, a medida que vayamos creando el servicio web podrás interpretar que tipo de acciones realizar para que la construcción de las peticiones se ajuste a tu preferencia.

2. Diseño E Implementación De La Base De Datos

De acuerdo a las condiciones que hemos visto, la aplicación Android People App gestiona contactos de los usuarios registrados.

Esto significa que necesitamos representar estas dos entidades en nuestro modelo de base de datos. Obviamente pueden existir más entidades dependiendo de la complejidad de la información del contacto y el proceso de registro que uses por usuario.

Pero en nuestro caso el diseño de datos se ilustra de la siguiente forma:

Diagrama ER Para Contactos De Usuario

Un usuario puede tener varios contactos, pero no se considerará que un contacto sea común entre varios usuarios.

Crear base de datos con phpMyAdmin: Partiendo del modelo crearemos una nueva base de datos en http://localhost/phpmyadmin/ llamada people.

Ve al panel izquierdo y presiona Nueva, luego digita el nombre y presiona Crear.

Crear Nueva Base De Datos En PhpMyAdmin

Si deseas hacerlo a través de la pestaña SQL, entonces utiliza el comando

CREATE DATABASE people;

La siguiente acción es la creación de las dos tablas. Recuerda que en el editor puedes hacerlo seleccionando la base de datos y luego la pestaña Estructura ingresas el nombre y número de columnas al formulario inicial para confirmar una creación.

Crear Nueva Tabla En PhpMyAdmin

Pero puedes usar el siguiente script para tener el esquema listo.

CREATE TABLE IF NOT EXISTS `usuario` (
`idUsuario` int(11) NOT NULL AUTO_INCREMENT,
 `nombre` varchar(30) NOT NULL,
 `contrasena` varchar(128) NOT NULL,
 `claveApi` varchar(60) NOT NULL,
 `correo` varchar(254) NOT NULL UNIQUE,
 PRIMARY KEY (idUsuario)
);

CREATE TABLE IF NOT EXISTS `contacto` (
`idContacto` int(11) NOT NULL AUTO_INCREMENT,
 `primerNombre` varchar(40) NOT NULL,
 `primerApellido` varchar(40) NOT NULL,
 `telefono` varchar(10) NOT NULL,
 `correo` varchar(254) NOT NULL,
 `idUsuario` int(11) NOT NULL, 
 PRIMARY KEY (idContacto),
 FOREIGN KEY (idUsuario) REFERENCES usuario(idUsuario)
 ON DELETE CASCADE
);

3. Conexión De Mysql Y Php Con PDO

Para crear la conexión entre la base de datos Mysql con una nuestra app Php usaremos PDO. Lo primero es crear un nuevo script php dentro de datos llamado login_mysql.php y añadir las siguientes constantes.

login_mysql.php

<?php
/**
 * Provee las constantes para conectarse a la base de datos
 * Mysql.
 */
define("NOMBRE_HOST", "localhost");// Nombre del host
define("BASE_DE_DATOS", "people"); // Nombre de la base de modelos
define("USUARIO", "root"); // Nombre del usuario
define("CONTRASENA", ""); // Constraseña

Como ves, tenemos localhost como nombre del servidor, people como el nombre de la base de datos y el usuario y contraseña por defecto de Mysql. Recuerda cambiar estos datos si estás siguiendo tu propio rumbo.

Ahora crea una nuevo archivo php con el nombre de ConexionBD.php dentro de utilidades para describir la clase que realizará la conexión a Mysql. La idea es usar el mismo patrón singleton que hemos usado en los artículos sobre servicios web.

ConexionBD.php

<?php
/**
 * Clase que envuelve una instancia de la clase PDO
 * para el manejo de la base de modelos
 */

require_once 'login_mysql.php';


class ConexionBD
{

    /**
     * Única instancia de la clase
     */
    private static $db = null;

    /**
     * Instancia de PDO
     */
    private static $pdo;

    final private function __construct()
    {
        try {
            // Crear nueva conexión PDO
            self::obtenerBD();
        } catch (PDOException $e) {
            // Manejo de excepciones
        }


    }

    /**
     * Retorna en la única instancia de la clase
     * @return ConexionBD|null
     */
    public static function obtenerInstancia()
    {
        if (self::$db === null) {
            self::$db = new self();
        }
        return self::$db;
    }

    /**
     * Crear una nueva conexión PDO basada
     * en las constantes de conexión
     * @return PDO Objeto PDO
     */
    public function obtenerBD()
    {
        if (self::$pdo == null) {
            self::$pdo = new PDO(
                'mysql:dbname=' . BASE_DE_DATOS .
                ';host=' . NOMBRE_HOST . ";",
                USUARIO,
                CONTRASENA,
                array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")
            );

            // Habilitar excepciones
            self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }

        return self::$pdo;
    }

    /**
     * Evita la clonación del objeto
     */
    final protected function __clone()
    {
    }

    function _destructor()
    {
        self::$pdo = null;
    }
}

Puedes probar que la conexión fue exitosa con el atributo errorCode() de la clase PDO. Si todo salió bien tendrás el código 00000.

<?php

//Dentro de index.php

require 'datos/ConexionBD.php';

print ConexionBD::obtenerInstancia()->obtenerBD()->errorCode();

4. Procesar Las Rutas De Un Recurso

Aunque es posible crear varios archivos individuales para procesar cada recurso, esto puede aumentar la complejidad si tienes demasiados recursos.

Es por eso que usaremos el archivo index.php como el núcleo monitor que exponga las rutas de los recursos. Es decir, procesaremos todas las urls desde aquí para mantener el control de forma compacta.

La idea básica es usar una estructura switch para atender los cuatro verbos GET, POST, PUT y DELETE dependiendo del segmento que llega a través de PATH_INFO. El algoritmo preciso sería el siguiente:

La pieza de código anterior es realmente sencilla. Los pasos que seguimos son los siguientes.

1. Obtener el segmento de la url para identificar el recurso. Usa el método explode() con el limitador ‘/’ para separar la ruta que se encuentra en PATH_INFO.

Por ejemplo, si la url es api.peopleapp.com/v1/contactos/4, la separación se crearía en el siguiente array:

Array
(
    [0] => contactos
    [1] => 4
)

2. Determina si el recurso solicitado existe. Una alternativa es usar array_shift() para obtener el primer valor del array (en el caso anterior sería el valor “contactos”) y luego comparar su existencia en un array que contenga los recursos disponibles.

3. Extrae el método de la petición a través de $_SERVER['REQUEST_METHOD'] y usa el valor como parámetro de una estructura switch. Dependiendo del recurso que se obtuvo y el método procesado así mismo se escribirán las instrucciones necesarias.

El marco general quedaría de la siguiente forma:

index.php

<?php

// Obtener recurso
$recurso = array_shift($peticion);
$recursos_existentes = array('contactos', 'usuarios');

// Comprobar si existe el recurso
if (!in_array($recurso, $recursos_existentes)) {
 // Respuesta error
}

$metodo = strtolower($_SERVER['REQUEST_METHOD']);

switch ($metodo) {
    case 'get':
        // Procesar método get
        break;

    case 'post':
        // Procesar método post
        break;
    case 'put':
        // Procesar método put
        break;

    case 'delete':
        // Procesar método delete
        break;
    default:
        // Método no aceptado
}

Recuerda usar default para procesar aquellos métodos que no serán soportados por tu servicio REST.

Este es solo un enfoque para tratar las peticiones. Puedes encontrar más patrones de despacho de llamadas diferentes. Una de las tantas opciones es usar un framework como Slim de la forma en que lo hace el compañero Ravi Tamada en su articulo How to create REST API for Android app using PHP, Slim and MySQL.

Otra forma sería intercambiar el alcance del patrón al reemplazar en el switch el método por el nombre del recurso. De esta forma procesarías las llamadas desde el interior de los recursos externalizando la autorización y tratando como controladores a cada recurso.

5. Manejadores de Salida En La Vista

Los manejadores de salida permiten construir las respuestas hacia el cliente con el formato y las cabeceras necesarias para seguir los estándares de REST.

Con estos componentes aseguramos la misma estructura en todas las respuestas. Por cada formato de datos aceptado construiremos una clase.

Dentro de vistas crea un nuevo archivo llamado VistaApi.php y añade la siguiente definición.

VistaApi.php

<?php

abstract class VistaApi{
    
    // Código de error
    public $estado;

    public abstract function imprimir($cuerpo);
}

La clase abstracta VistaApi representa de forma general los requerimientos básicos para imprimir una respuesta en cualquier formato. El miembro $estado se refiere al código de error HTTP que se enviará en la respuesta. El método imprimir() debería ser sobrescrito para añadir la cabecera Content-Type e imprimir el formato.

Teniendo en cuenta eso, escribir una vista para Json es sencillo. Agrega un nuevo archivo llamado VistaJson.php a vistas y luego incluye la siguiente definición.

VistaApi.php

<?php

require_once "VistaApi.php";

/**
 * Clase para imprimir en la salida respuestas con formato JSON
 */
class VistaJson extends VistaApi
{
    public function __construct($estado = 400)
    {
        $this->estado = $estado;
    }

    /**
     * Imprime el cuerpo de la respuesta y setea el código de respuesta
     * @param mixed $cuerpo de la respuesta a enviar
     */
    public function imprimir($cuerpo)
    {
        if ($this->estado) {
            http_response_code($this->estado);
        }
        header('Content-Type: application/json; charset=utf8');
        echo json_encode($cuerpo, JSON_PRETTY_PRINT);
        exit;
    }
}

Con un formato XML el parsing es un poco más elaborado, ya que no tenemos una función xml_encode(). No obstante es fácil construir un formato con solo dos hijos a través de SimpleXML.

VistaXML.php

<?php

require_once "VistaApi.php";

/**
 * Clase para imprimir en la salida respuestas con formato XML
 */
class VistaXML extends VistaApi
{

    /**
     * Imprime el cuerpo de la respuesta y setea el código de respuesta
     * @param mixed $cuerpo de la respuesta a enviar
     */
    public function imprimir($cuerpo)
    {
        if ($this->estado) {
            http_response_code($this->estado);
        }

        header('Content-Type: text/xml');

        $xml = new SimpleXMLElement('<respuesta/>');
        self::parsearArreglo($cuerpo, $xml);
        print $xml->asXML();

        exit;
    }

    /**
     * Convierte un array a XML
     * @param array $data arreglo a convertir
     * @param SimpleXMLElement $xml_data elemento raíz
     */
    public function parsearArreglo($data, &$xml_data)
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                if (is_numeric($key)) {
                    $key = 'item' . $key;
                }
                $subnode = $xml_data->addChild($key);
                self::parsearArreglo($value, $subnode);
            } else {
                $xml_data->addChild("$key", htmlspecialchars("$value"));
            }
        }
    }
}

Con este mismo patrón puedes crear la cantidad de salidas de formato que quieras. Simplemente debes asegurarte de realizar el parsing efectivamente y setear las cabeceras necesarias.

6. Manejo De Excepciones En La API

Desarrollar un api REST solamente pensando en los resultados ideales hace que nuestro servicio pierda congruencia cuando surgen flujos anómalos de comportamiento.

Por esta razón debemos pensar en la construcción de respuestas que incluyan la información sobre los errores que puedan darse.

En la sección anterior vimos diferentes vistas que pueden sernos de ayuda para transmitir errores al cliente. Pero adicionalmente podemos usar el lanzamiento de excepciones como estrategia.

Una buena forma de hacerlo es añadir al inicio de nuestro archivo index.php un manejador de excepciones personalizado para imprimir la respuesta cada que ocurra una.

$vista = new VistaJson();

set_exception_handler(function ($exception) use ($vista) {
    $cuerpo = array(
        "estado" => $exception->estado,
        "mensaje" => $exception->getMessage()
    );
    if ($exception->getCode()) {
        $vista->estado = $exception->getCode();
    } else {
        $vista->estado = 500;
    }

    $vista->imprimir($cuerpo);
}
);

A través de la función set_exception_handler() invocamos las instrucciones necesarias para crear una nueva vista basada solo en el mensaje y un estado. Este sería el cuerpo común de un error en nuestra API. Así nos aseguramos tener controlados los errores que se nos salgan de la mano.

Puedes extender un poco más el comportamiento si deseas enviar información que le diga al usuario como resolver el problema en cuestión. En esta ocasión usaremos una nueva clase de excepción que contenga el estado de error.

Dentro de la carpeta utilidades crea un archivo llamado ExcepcionApi.php. Este contendrá una pequeña extensión de la clase Exception para cubrir el estado de error que presenten las excepciones de nuestro servicio web REST.

ExcepcionApi.php

<?php

class ExcepcionApi extends Exception
{
    public $estado;

    public function __construct($estado, $mensaje, $codigo = 400)
    {
        $this->estado = $estado;
        $this->message = $mensaje;
        $this->code = $codigo;
    }

}

Con esto claro, cada que surja un problema puedes usar la sentencia throw para enviar al manejador el cuerpo de la excepción. Por ejemplo:

throw new ExcepcionApi(2, "Error con estado 2", 404);

7. Crear Modelo De Datos De Usuarios

Las únicas acciones que deseamos realizar en el recurso usuarios son el registro y un proceso de login. Ambas acciones podemos asociarlas a la url añadiendo un segmento que especifique la acción de la siguiente forma:

Método Descripción
POST api.peopleapp.com/v1/usuarios/registro Crea un nuevo elemento en la colección de usuarios
POST api.peopleapp.com/v1/usuarios/login Autoriza el acceso de un usuario a los recursos

Ambas acciones requieren del método POST para ser procesadas. Dependiendo de la intención así mismo seguiremos el flujo de procesamiento.

Por el momento crea un nuevo archivo llamado usuarios.php dentro de modelos. El esquema general de la clase que representará a los usuarios sería el siguiente.

usuarios.php

<?php

class usuarios
{
    // Datos de la tabla "usuario"
    const NOMBRE_TABLA = "usuario";
    const ID_USUARIO = "idUsuario";
    const NOMBRE = "nombre";
    const CONTRASENA = "contrasena";
    const CORREO = "correo";
    const CLAVE_API = "claveApi";

    public static function post($peticion)
    {
        // Procesar post
    }    
   
}

Como estrategia de nombrado usaremos la cadena usuarios en el nombre de la clase para comparar su existencia como recurso.

También usaremos el nombre de cada verbo de la petición como firma de cada método, es decir, para GET tendremos get(), para POST el método post() y así sucesivamente. Esta condición permitirá generalizar la invocación de estos métodos sin importar la clase.

Debido a que usaremos operaciones sobre la base de datos Mysql, es necesario tener una referencia rápida de la información sobre la tabla usuario, por lo que declaré unas constantes auxiliares como miembros.

Ahora pensemos un poco en la lógica que debemos seguir para que el login y registro se lleven a cabo. Con login nos referimos a la autorización que se da luego de autenticar los datos de un usuario existente. El registro es simplemente la creación de un nuevo usuario.

Aunque suena sencillo, veamos un diagrama general que contemple todas las acciones que debemos realizar.

Diagrama De Flujo Para Login Y Registro Php

Registro de usuarios

El diagrama anterior deja claro la forma en que procederemos para registrar usuarios. En esencia, tenemos 4 pasos para procesar esta acción.

1. Extraer los campos de la petición POST.
2. Validar la sintaxis y restricciones de estos campos.
3. Crear un nuevo registro en la tabla usuario.
4. Imprimir la respuesta

Con estas ideas en mente, entonces comencemos por filtrar el segmento que viene desde la url dentro del método post() de la clase usuarios. Simplemente comparamos el contenido del parámetro de entrada con las acciones correspondientes.

public static function post($peticion)
{
    if ($peticion[0] == 'registro') {
        return self::registrar();
    } else if ($peticion[0] == 'login') {
        return self::loguear();
    } else {
        throw new ExcepcionApi(self::ESTADO_URL_INCORRECTA, "Url mal formada", 400);
    }
}

El código anterior invoca uno de los dos métodos que se crearán para llevar a cabo el registro y el login. Veamos cómo avanzar.

Extraer campos de petición POST— El registro de un usuario requiere los campos nombre, contrasena y correo. Por ello esperamos un objeto Json con una estructura similar a la siguiente:

{
    "nombre":"nick",
   "contrasena":"12345",
   "correo":"carlos@mail.com"
}

Recuerda que para extraer el cuerpo de una petición podemos usar el método file_get_content() con el contexto "php://input" o el método http_get_request_body(). Luego decodifica el formato Json a un objeto php con json_decode().

private function registrar()
{
    $cuerpo = file_get_contents('php://input');
    $usuario = json_decode($cuerpo);

    // Validar campos
    // Crear usuario
    // Imprimir respuesta
}

Validar datos del usuario— Este paso no hará parte del registro que implementaremos en este artículo, ya que no tenemos unas reglas de negocio previamente definidas.

Aquí debes comprobar todos los campos que usarás para el registro con el fin de que tengan el formato y sintaxis acorde al comportamiento natural del servicio web.

Por ejemplo, hay servicios que no permiten caracteres especiales en el nombre de usuario. O contraseñas que requieren obligatoriamente un número y un carácter especial. También comprobar la validez de la estructura de correo, etc.

Crear nuevo usuario en la base de datos— La inserción de un nuevo registro requiere que usemos nuestro singleton ConexionBD.

Para ello crearemos una sentencia preparada con un comando INSERT INTO y luego ligaremos los valores del objeto que viene como parámetro en el método que implementaremos para la creación.

private function crear($datosUsuario)
{
    $nombre = $datosUsuario->nombre;

    $contrasena = $datosUsuario->contrasena;
    $contrasenaEncriptada = self::encriptarContrasena($contrasena);

    $correo = $datosUsuario->correo;

    $claveApi = self::generarClaveApi();

    try {

        $pdo = ConexionBD::obtenerInstancia()->obtenerBD();

        // Sentencia INSERT
        $comando = "INSERT INTO " . self::NOMBRE_TABLA . " ( " .
            self::NOMBRE . "," .
            self::CONTRASENA . "," .
            self::CLAVE_API . "," .
            self::CORREO . ")" .
            " VALUES(?,?,?,?)";

        $sentencia = $pdo->prepare($comando);

        $sentencia->bindParam(1, $nombre);
        $sentencia->bindParam(2, $contrasenaEncriptada);
        $sentencia->bindParam(3, $claveApi);
        $sentencia->bindParam(4, $correo);

        $resultado = $sentencia->execute();

        if ($resultado) {
            return self::ESTADO_CREACION_EXITOSA;
        } else {
            return self::ESTADO_CREACION_FALLIDA;
        }
    } catch (PDOException $e) {
        throw new ExcepcionApi(self::ESTADO_ERROR_BD, $e->getMessage());
    }

}

El método crear() recibe como parámetro el objeto decodificado anteriormente. La primera acción es la obtención de cada columna en variables locales.

En el caso de la contraseña, recuerda que es mejor dificultar su lectura a personajes maliciosos a través de algoritmos de encriptado. Por ello tenemos el método encriptarContrasena() el cual usa el método password_hash() con un encriptado hash simple. Puedes extender la seguridad según te parezca.

private function encriptarContrasena($contrasenaPlana)
{
    if ($contrasenaPlana)
        return password_hash($contrasenaPlana, PASSWORD_DEFAULT);
    else return null;
}

La clave del api para el usuario se genera con generarClaveApi(). Esto se hace con un número aleatorio al cual se le aplica MD5. Es una generación simple, pero tu puedes emplear otros métodos. Ten en cuenta que esta clave es estática, puedes optar por usar un tiempo de expiración para esta y así para aumentar la seguridad.

private function generarClaveApi()
{
    return md5(microtime().rand());
}

La siguiente pieza de código se encarga de insertar un nuevo registro con PDO. Si el resultado de PDO::execute() fue true, entonces retornamos en una constante entera que representa código de éxito. De lo contrario tendremos un código de error.

Por otra parte, si hay excepciones de la base de datos, construiremos directamente la respuesta a la petición. Así que enviaremos un código 404 con http_response_code(), setearemos un contenido tipo Json con header(), luego imprimimos un array con un código de error por base de datos y un pequeño mensaje.

Por ejemplo, si quisiéramos crear un usuario con un correo que ya existe, el cuerpo de la respuesta sería:

{
    "codigo": 3,
    "mensaje": "SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'carlos@mail.com' for key 'correo'"
}

Hay diversas causas para que se provoque un error SQL, sin embargo yo generalicé todo en un solo caso. Si deseas enviar distintos mensajes dependiendo de cada una, entonces debes obtener el código de la excepción y decidirlo con un switch.

Con esto ya podemos completar nuestro método registrar().

private function registrar()
{
    $cuerpo = file_get_contents('php://input');
    $usuario = json_decode($cuerpo);

    $resultado = self::crear($usuario);

    switch ($resultado) {
        case self::ESTADO_CREACION_EXITOSA:
            http_response_code(200);
            return
                [
                    "estado" => self::ESTADO_CREACION_EXITOSA,
                    "mensaje" => utf8_encode("¡Registro con éxito!")
                ];
            break;
        case self::ESTADO_CREACION_FALLIDA:
            throw new ExcepcionApi(self::ESTADO_CREACION_FALLIDA, "Ha ocurrido un error");
            break;
        default:
            throw new ExcepcionApi(self::ESTADO_FALLA_DESCONOCIDA, "Falla desconocida", 400);
    }
}

De acuerdo al código obtenido se imprime la respuesta correspondiente.

Testear el registro de usuarios— Antes de realizar las pruebas, asegúrate de invocar el método post() de la clase usuarios dentro del switch de index.php de la siguiente forma:

switch ($metodo) {
    case 'get':
        break;

    case 'post':
        $vista->imprimir(usuarios::post($peticion));
        break;

    case 'put':
        break;

    case 'delete':
        break;

    default:
        // Método no aceptado

}

Una vez hecho eso, ve a la barra de búsqueda en Google Chrome y tipea chrome://apps.

Ver Aplicaciones En Chrome

Con ello podrás acceder a la aplicación Advanced REST Client.

Iniciar Advance REST Client En Chrome

En la sección Request verás los elementos necesarios para construir la petición POST que necesitamos enviar hacia la url http://localhost/api.peopleapp.com/v1/usuarios/registro. Solo basta añadir texto plano con la opción Raw, configurar el tipo de contenido y luego presionar Send.

Petición POST en Advanced REST Client De Chrome

Si todo sale bien, tendrás la siguiente respuesta:

Respuesta En Advance REST Client De Chrome

En la parte superior verás el atributo Status con el código 201. Response Headers tiene las cabeceras de la respuesta. Si ves bien, Content-Type vino con application/json como lo especificamos en el servicio. Response mostrará el cuerpo de la respuesta en formato Json con el código de éxito que hayas usado.

Login de usuarios

Como se veía en el diagrama de flujo inicial, el login se basa en comprobar las credenciales de un usuario para permitirle acceder a los contactos.

Los pasos que seguiremos serán:

1. Extraer credenciales del cuerpo de la petición POST
2. Verificar la validez de las credenciales
3. Obtener los datos del usuario
4. Imprimir la respuesta

Extraer crendenciales— Para la autenticación usaremos el correo electrónico y la contraseña del usuario. Así que extraemos ambas credenciales desde el objeto decodificado del cuerpo de la petición.

private function loguear()
{
    $respuesta = array();

    $body = file_get_contents('php://input');
    $usuario = json_decode($body);

    $correo = $usuario->correo;
    $contrasena = $usuario->contrasena;


    // Autenticar
    // Obtener datos del usuario
    // Imprimir respuesta
}

Verificar la validez de las credenciales— La autenticación se basa en la comprobación de que exista un registro de la contraseña comprobada según el correo.

Si esta existe, entonces se pasa a comprobar el valor hash que está almacenado en la base de datos con la contraseña plana. Con esto claro creemos el método autenticar().

private function autenticar($correo, $contrasena)
{
    $comando = "SELECT contrasena FROM " . self::NOMBRE_TABLA .
        " WHERE " . self::CORREO . "=?";

    try {

        $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);

        $sentencia->bindParam(1, $correo);

        $sentencia->execute();

        if ($sentencia) {
            $resultado = $sentencia->fetch();

            if (self::validarContrasena($contrasena, $resultado['contrasena'])) {
                return true;
            } else return false;
        } else {
            return false;
        }
    } catch (PDOException $e) {
        throw new ExcepcionApi(self::ESTADO_ERROR_BD, $e->getMessage());
    }
}

El código anterior es sencillo. Luego de ejecutar el SELECT se obtiene el primer y único registro con fetch(), con ello podremos obtener la columna 'contrasena'. Esto permite comparar directamente con la contraseña original con el método validarContrasena().

Este método solo invoca a password_verify() para comprobar el hash con el texto plano. Si es verdadero, entonces el usuario estará logueado.

private function validarContrasena($contrasenaPlana, $contrasenaHash)
{
    return password_verify($contrasenaPlana, $contrasenaHash);
}

Obtener los datos del usuario— una vez comprobado que el usuario es válido, entonces pasamos a obtener sus datos completos. Entre ellos la clave del api para permitir autorizar las operaciones sobre los recursos de los contactos.

private function obtenerUsuarioPorCorreo($correo)
{
    $comando = "SELECT " .
        self::NOMBRE . "," .
        self::CONTRASENA . "," .
        self::CORREO . "," .
        self::CLAVE_API .
        " FROM " . self::NOMBRE_TABLA .
        " WHERE " . self::CORREO . "=?";

    $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);

    $sentencia->bindParam(1, $correo);

    if ($sentencia->execute())
        return $sentencia->fetch(PDO::FETCH_ASSOC);
    else
        return null;
}

Imprimir la respuesta— Finalmente determinamos que tipo de cuerpo enviaremos en la respuesta. Si el método obtenerUsuarioPorCorreo() retorna exitosamente, entonces crearemos un arreglo Json para enviarlo. De lo contrario en cualquier situación adversa, construiremos el formato de error.

private function loguear()
{
    $respuesta = array();

    $body = file_get_contents('php://input');
    $usuario = json_decode($body);

    $correo = $usuario->correo;
    $contrasena = $usuario->contrasena;


    if (self::autenticar($correo, $contrasena)) {
        $usuarioBD = self::obtenerUsuarioPorCorreo($correo);

        if ($usuarioBD != NULL) {
            http_response_code(200);
            $respuesta["nombre"] = $usuarioBD["nombre"];
            $respuesta["correo"] = $usuarioBD["correo"];
            $respuesta["claveApi"] = $usuarioBD["claveApi"];
            return ["estado" => 1, "usuario" => $respuesta];
        } else {
            throw new ExcepcionApi(self::ESTADO_FALLA_DESCONOCIDA,
                "Ha ocurrido un error");
        }
    } else {
        throw new ExcepcionApi(self::ESTADO_PARAMETROS_INCORRECTOS,
            utf8_encode("Correo o contraseña inválidos"));
    }
}

Testear el login de usuarios— Al igual que el registro usaremos el método POST hacia la url http://localhost/api.peopleapp.com/v1/usuarios/login junto a las credenciales correo y contrasena.

Prueba De Login En Advanced REST Client De Chrome

Al enviar la petición se debería recibir la clave del api para la autorización.

Generar Clave Api En Login Php

8. Crear Modelo De Datos Para Contactos

Con los contactos si tendremos un trato basado en CRUD. Los usuarios autorizados podrán realizar cualquier de las cuatro acciones solo si tienen una clave de api asignada.

La siguiente tabla muestra la descripción formal de las operaciones sobre el recurso.

Método Descripción
GET api.peopleapp.com/v1/contactos  Obtiene la colección de contactos
GET api.peopleapp.com/v1/contactos/:id Obtiene un solo recurso de los contactos con el id especificado
POST api.peopleapp.com/v1/contactos Añade un nuevo contacto a la colección
PUT api.peopleapp.com/v1/contactos/:id Modifica el contacto especificado por su id
DELETE api.peopleapp.com/v1/contactos/:id Elimina un contacto especificado por su id.

Ahora crearemos un nuevo archivo con el nombre de contactos.php en la carpeta modelos. La idea es mapear  la información de la tabla asociada a Mysql e incluir métodos estáticos que representen los cuatro verbos.

contactos.php

<?php

class contactos
{

    const NOMBRE_TABLA = "contacto";
    const ID_CONTACTO = "idContacto";
    const PRIMER_NOMBRE = "primerNombre";
    const PRIMER_APELLIDO = "primerApellido";
    const TELEFONO = "telefono";
    const CORREO = "correo";
    const ID_USUARIO = "idUsuario";

    public static function get($peticion)
    {      
    }

    public static function post()
    {
    }

    public static function put($peticion)
    {
    }

    public static function delete($peticion)
    {
    }
   
}

Lo siguiente es construir cada uno de los métodos acudiendo a la base de datos para realizar las acciones correspondientes. No obstante, cada operación depende de la autorización del usuario, por lo que comenzaremos por este proceso.

Autorización de usuarios

La autorización tiene el fin de permitir al usuario el acceso a los recursos que proporciona el servicio web REST. Esto equivale a comparar la clave del api del usuario con su registro en la base de datos.

Quiere decir que la petición del cliente debe enviar la clave luego de que el usuario esté logueado. Un método sería añadir un parámetro a la url con este valor, pero debido a que nuestra api es privada es mejor aislar este componente con la cabecera Authorization.

La implementación es sencilla. Solo extraemos el valor de Authorization con el método apache_request_headers() o getallheaders(). Con este comparamos la clave que se encuentra en la base de datos. Si todo sale bien el permiso será otorgado y retornaremos el id del usuario.

Así que ve al archivo usuarios.php y agrega el siguiente método de autorización.

public static function autorizar()
{
    $cabeceras = apache_request_headers();

    if (isset($cabeceras["Authorization"])) {

        $claveApi = $cabeceras["Authorization"];

        if (usuarios::validarClaveApi($claveApi)) {
            return usuarios::obtenerIdUsuario($claveApi);
        } else {
            throw new ExcepcionApi(
                self::ESTADO_CLAVE_NO_AUTORIZADA, "Clave de API no autorizada", 401);
        }

    } else {
        throw new ExcepcionApi(
            self::ESTADO_AUSENCIA_CLAVE_API,
            utf8_encode("Se requiere Clave del API para autenticación"));
    }
}

Si la cabecera de autorización fue enviada, entonces su valor es comprobado con el método validarClaveApi(), donde contaremos a través de COUNT aquellos registros que tengan una clave igual. Si este valor es mayor a 0, entonces asumiremos que la clave existe.

private function validarClaveApi($claveApi)
{
    $comando = "SELECT COUNT(" . self::ID_USUARIO . ")" .
        " FROM " . self::NOMBRE_TABLA .
        " WHERE " . self::CLAVE_API . "=?";

    $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);

    $sentencia->bindParam(1, $claveApi);

    $sentencia->execute();

    return $sentencia->fetchColumn(0) > 0;
}

Si todo salió bien, entonces se retorna el id del usuario que tenga esa clave de api con el método obtenerIdUsuario().

private function obtenerIdUsuario($claveApi)
{
    $comando = "SELECT " . self::ID_USUARIO .
        " FROM " . self::NOMBRE_TABLA .
        " WHERE " . self::CLAVE_API . "=?";

    $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);

    $sentencia->bindParam(1, $claveApi);

    if ($sentencia->execute()) {
        $resultado = $sentencia->fetch();
        return $resultado['idUsuario'];
    } else
        return null;
}

Obtener la colección de contactos

Tanto para obtener la colección completa de los contactos o uno en particular, es posible usar un solo conjunto de instrucciones que se basen en el segmento de la url que viene en la petición.

Para ello iremos al método get() de contactos y llamaremos al método usuarios::autorizar() para determinar el id del usuario y así construir la consulta correcta en la base de datos.

Si la petición que se recibió está vacía consultaremos todos los contactos, de lo contrario se extrae el supuesto identificador del contacto que viene en la url. Sin importar el caso, imprimimos una respuesta con código 200.

public static function get($peticion)
{
    $idUsuario = usuarios::autorizar();

    if (empty($peticion[0]))
        return self::obtenerContactos($idUsuario);
    else
        return self::obtenerContactos($idUsuario, $peticion[0]);

}

Dentro del método obtenerContactos() se realiza la consulta SELECT para obtener los contactos relacionados al usuario. La respuesta correcta sería un arreglo asociativo con los datos de los registros. Las respuestas fallidas serán controladas a través de excepciones.

private function obtenerContactos($idUsuario, $idContacto = NULL)
{
    try {
        if (!$idContacto) {
            $comando = "SELECT * FROM " . self::NOMBRE_TABLA .
                " WHERE " . self::ID_USUARIO . "=?";

            // Preparar sentencia
            $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);
            // Ligar idUsuario
            $sentencia->bindParam(1, $idUsuario, PDO::PARAM_INT);

        } else {
            $comando = "SELECT * FROM " . self::NOMBRE_TABLA .
                " WHERE " . self::ID_CONTACTO . "=? AND " .
                self::ID_USUARIO . "=?";

            // Preparar sentencia
            $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);
            // Ligar idContacto e idUsuario
            $sentencia->bindParam(1, $idContacto, PDO::PARAM_INT);
            $sentencia->bindParam(2, $idUsuario, PDO::PARAM_INT);
        }

        // Ejecutar sentencia preparada
        if ($sentencia->execute()) {
            http_response_code(200);
            return
                [
                    "estado" => self::ESTADO_EXITO,
                    "datos" => $sentencia->fetchAll(PDO::FETCH_ASSOC)
                ];
        } else
            throw new ExcepcionApi(self::ESTADO_ERROR, "Se ha producido un error");

    } catch (PDOException $e) {
        throw new ExcepcionApi(self::ESTADO_ERROR_BD, $e->getMessage());
    }
}

Como ves, el parámetro idContacto tiene como valor predefinido NULL, ya que si se desea consultar todos los registros no será de utilidad. Esto lo decidimos con un if que evalúa que tipo de consulta se realizará para crear la sentencia preparada correcta.

Testear petición GET sobre contactos— Es necesario que tengas al menos un usuario registrado en la base de datos para conseguir una clave de api válida. Por otro lado no olvides invocar el método get() en index.php.

switch ($metodo) {
    case 'get':
        $vista->imprimir(contactos::get($peticion));
        break;

    case 'post':
        break;

    case 'put':
        break;

    case 'delete':
        break;
    default:
        // Método no aceptado

}

Esta vez prepara la url http://localhost/api.peopleapp.com/v1/contactos y en la sección Headers añade la clave Authorization y en su valor pega la clave de api del usuario.

Prueba De Petición GET En Advanced REST Client

Para añadir la cabecera Authorization puedes usar el formato Raw para escribir su definición en texto plano. Pero también puedes añadir un par clave-valor para facilitar el trabajo con la opción Form.

Si todo sale bien y aún no tienes creados contactos para el usuario registrado, la respuesta sería en blanco.

Respuesta Petición GET Contactos En Php

Si ves las cabeceras de la petición contiene la autorización que determinamos. La respuesta es un código 200 ya que no se produjeron errores en la base de datos, sin embargo puedes imprimir una respuesta que resalte la no existencia de registros por el momento.

El siguiente test es añadir un identificador al segmento de la url. Por ejemplo, probemos contactos/1.

Prueba De GET Con Id En Php

Con ello tendrás de nuevo una respuesta en blanco con código 200. Con ello nuestro método queda asegurado de forma básica.

Añadir un nuevo contacto

Añadir un nuevo usuario requiere que construyamos una petición POST hacia la url de los contactos con un cuerpo que contenga el primer nombre, el primer apellido, teléfono y correo del contacto.

La respuesta común sería un mensaje que traiga consigo el id del nuevo registro, un mensaje y código de éxito. Adicionalmente si deseas, puedes usar la cabecera Location para especificar al url de acceso del recurso como complemento.

La respuesta se vería de esta forma:

{
    "codigo": 1,
    "mensaje": "Contacto creado",
    "id": "4"
}

Teniendo en cuenta estas restricciones, dentro de post() haremos lo siguiente:

  • Autorizaremos al usuario
  • Extraeremos el cuerpo de la petición en forma de objeto
  • Crearemos el nuevo registro en la base de datos con un nuevo método llamado crear()
  • Si todo salió bien, entonces imprimimos la respuesta.

Veamos:

public static function post($peticion)
{
    $idUsuario = usuarios::autorizar();

    $body = file_get_contents('php://input');
    $contacto = json_decode($body);

    $idContacto = contactos::crear($idUsuario, $contacto);

    http_response_code(201);
    return [
        "estado" => self::CODIGO_EXITO,
        "mensaje" => "Contacto creado",
        "id" => $idContacto
    ];

}

Para retornar el id del registro que se añade con el método crear() puedes retornar en el método PDO::lastInsertId(). Las demás acciones ya nos son familiares así que no hace falta entrar en detalles.

private function crear($idUsuario, $contacto)
{
    if ($contacto) {
        try {

            $pdo = ConexionBD::obtenerInstancia()->obtenerBD();

            // Sentencia INSERT
            $comando = "INSERT INTO " . self::NOMBRE_TABLA . " ( " .
                self::PRIMER_NOMBRE . "," .
                self::PRIMER_APELLIDO . "," .
                self::TELEFONO . "," .
                self::CORREO . "," .
                self::ID_USUARIO . ")" .
                " VALUES(?,?,?,?,?)";

            // Preparar la sentencia
            $sentencia = $pdo->prepare($comando);

            $sentencia->bindParam(1, $primerNombre);
            $sentencia->bindParam(2, $primerApellido);
            $sentencia->bindParam(3, $telefono);
            $sentencia->bindParam(4, $correo);
            $sentencia->bindParam(5, $idUsuario);


            $primerNombre = $contacto->primerNombre;
            $primerApellido = $contacto->primerApellido;
            $telefono = $contacto->telefono;
            $correo = $contacto->correo;

            $sentencia->execute();

            // Retornar en el último id insertado
            return $pdo->lastInsertId();

        } catch (PDOException $e) {
            throw new ExcepcionApi(self::ESTADO_ERROR_BD, $e->getMessage());
        }
    } else {
        throw new ExcepcionApi(
            self::ESTADO_ERROR_PARAMETROS, 
            utf8_encode("Error en existencia o sintaxis de parámetros"));
    }

}

El parámetro $peticion de post() no lo usamos ya que no permitimos que el cliente defina el identificador del nuevo registro. En caso contrario puedes emplearlo para procesar la inserción de forma distinta.

Testear creación de contacto— Por último probemos el funcionamiento añadiendo un registro simple.

Aquí cabe aclarar que en el archivo index.php debemos realizar una generalización de métodos a través de la función call_user_func(), ya que no sabemos que recurso fue accedido desde la url. Con esto evitamos usar estructuras de decisión y solo llamamos el método de la clase necesitada.

// Filtrar método
switch ($metodo) {
    case 'get':
    case 'post':
    case 'put':
    case 'delete':
        if (method_exists($recurso, $metodo)) {
            $respuesta = call_user_func(array($recurso, $metodo), $peticion);
            $vista->imprimir($respuesta);
            break;
        }
    default:
        // Método no aceptado
        $vista->estado = 405;
        $cuerpo = [
            "estado" => ESTADO_METODO_NO_PERMITIDO,
            "mensaje" => utf8_encode("Método no permitido")
        ];
        $vista->imprimir($cuerpo);

}

Como puedes notar, al confirmar de que el método exista en la clase con method_exists() procedemos a llamarlo. De lo contrario arrojamos una excepción con código 405.

Ahora agrega la clave del api en la petición de Advanced REST Client y luego usa el siguiente objeto Json como cuerpo de la petición.

{
    "primerNombre":"James",
   "primerApellido":"Revelo",
   "telefono":"312090934",
   "correo":"james@mail.com"
}

Presiona Send y verifica el estado de la respuesta y su cuerpo.

Respuesta POST De Servicio Web REST

La ubicación del nuevo registro puede ser de utilidad como referencia rápida en la aplicación Android cliente si es necesaria un acceso inmediato.

Editar un contacto a través de su id

La edición se realiza a través del método PUT hacia la url del recurso contacto junto al identificador del elemento que se actualizará.

El método put() es mucho más simple que post() o get(). Solo debemos extraer el segmento del id y luego realizar una consulta UPDATE sobre la base de datos. Al igual que los demás métodos se requiere una autorización primero.

public static function put($peticion)
{
    $idUsuario = usuarios::autorizar();

    if (!empty($peticion[0])) {
        $body = file_get_contents('php://input');
        $contacto = json_decode($body);

        if (self::actualizar($idUsuario, $contacto, $peticion[0]) > 0) {
            http_response_code(200);
            return [
                "estado" => self::CODIGO_EXITO,
                "mensaje" => "Registro actualizado correctamente"
            ];
        } else {
            throw new ExcepcionApi(self::ESTADO_NO_ENCONTRADO,
                "El contacto al que intentas acceder no existe", 404);
        }
    } else {
        throw new ExcepcionApi(self::ESTADO_ERROR_PARAMETROS, "Falta id", 422);
    }
}

El método actualizar() procesa la operación en la base de datos y maneja los posibles errores que se puedan presentar.

private function actualizar($idUsuario, $contacto, $idContacto)
{
    try {
        // Creando consulta UPDATE
        $consulta = "UPDATE " . self::NOMBRE_TABLA .
            " SET " . self::PRIMER_NOMBRE . "=?," .
            self::PRIMER_APELLIDO . "=?," .
            self::TELEFONO . "=?," .
            self::CORREO . "=? " .
            " WHERE " . self::ID_CONTACTO . "=? AND " . self::ID_USUARIO . "=?";

        // Preparar la sentencia
        $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($consulta);

        $sentencia->bindParam(1, $primerNombre);
        $sentencia->bindParam(2, $primerApellido);
        $sentencia->bindParam(3, $telefono);
        $sentencia->bindParam(4, $correo);
        $sentencia->bindParam(5, $idContacto);
        $sentencia->bindParam(6, $idUsuario);

        $primerNombre = $contacto->primerNombre;
        $primerApellido = $contacto->primerApellido;
        $telefono = $contacto->telefono;
        $correo = $contacto->correo;

        // Ejecutar la sentencia
        $sentencia->execute();

        return $sentencia->rowCount();

    } catch (PDOException $e) {
        throw new ExcepcionApi(self::ESTADO_ERROR_BD, $e->getMessage());
    }
}

Esta actualización recibe todos los datos del contacto asumiendo que la mayoría de ellos cambiará. Pero es posible usar el método PATCH para especificar únicamente los campos que se actualizarán y así consumir menos ancho de banda.

Testear edición de un contacto— Supongamos que el contacto que añadimos en el apartado anterior tiene el identificador 5. Esto quiere decir que la url para aplicar el PUT sería http://localhost/api.peopleapp.com/v1/contactos/5.

Por lo que si cambiamos el nombre antiguo de primerNombre por "Pedro" deberíamos tener una respuesta satisfactoria.

Petición PUT En Servicio Web REST

Si todo salió bien, la respuesta tendrá el código 1 y el registro habrá cambiado en Mysql.

Registro Actualizado Correctamente En Servicio REST

Eliminar un contacto

Al igual que la actualización, la eliminación requiere el id del contacto ya sea como parámetro o en el segmento de la url. El método que usaremos para representar esta operación será DELETE.

El método delete() tiene una forma similar a put(), por lo que el siguiente código no te parece extraño. La única diferencia es que no recibimos un cuerpo en la petición.

public static function delete($peticion)
{
    $idUsuario = usuarios::autorizar();

    if (!empty($peticion[0])) {
        if (self::eliminar($idUsuario, $peticion[0]) > 0) {
            http_response_code(200);
            return [
                "estado" => self::CODIGO_EXITO,
                "mensaje" => "Registro eliminado correctamente"
            ];
        } else {
            throw new ExcepcionApi(self::ESTADO_NO_ENCONTRADO,
                "El contacto al que intentas acceder no existe", 404);
        }
    } else {
        throw new ExcepcionApi(self::ESTADO_ERROR_PARAMETROS, "Falta id", 422);
    }

}

La operación de eliminación en la base de datos se da en el método eliminar(). Aquí se usara una sentencia preparada con el comando DELETE para borrar aquel registro que tenga el id del usuario y del contacto procesado.

private function eliminar($idUsuario, $idContacto)
{
    try {
        // Sentencia DELETE
        $comando = "DELETE FROM " . self::NOMBRE_TABLA .
            " WHERE " . self::ID_CONTACTO . "=? AND " .
            self::ID_USUARIO . "=?";

        // Preparar la sentencia
        $sentencia = ConexionBD::obtenerInstancia()->obtenerBD()->prepare($comando);

        $sentencia->bindParam(1, $idContacto);
        $sentencia->bindParam(2, $idUsuario);

        $sentencia->execute();

        return $sentencia->rowCount();

    } catch (PDOException $e) {
        throw new ExcepcionApi(self::ESTADO_ERROR_BD, $e->getMessage());
    }
}

Testear la eliminación de un usuario— Intenta borrar un registro existente a través del identificador y cambiando el método de Advanced REST Client a DELETE.

En mi caso borraré el número 2, así que acuso a la url http://localhost/api.peopleapp.com/v1/contactos/2.

Petición DELETE En Servicio Web REST

Con ello tendría una respuesta de éxito así:

{
    "codigo": 1,
    "mensaje": "Registro eliminado correctamente"
}

Por último podemos simplificar el archivo index.php para que el switch solo actúe como filtro de los métodos aceptados y así referirnos a un recurso generalizado.

index.php

<?php

require 'controladores/usuarios.php';
require 'controladores/contactos.php';
require 'vistas/VistaXML.php';
require 'vistas/VistaJson.php';
require 'utilidades/ExcepcionApi.php';

// Constantes de estado
const ESTADO_URL_INCORRECTA = 2;
const ESTADO_EXISTENCIA_RECURSO = 3;
const ESTADO_METODO_NO_PERMITIDO = 4;

$vista = new VistaJson();

set_exception_handler(function ($exception) use ($vista) {
    $cuerpo = array(
        "estado" => $exception->estado,
        "mensaje" => $exception->getMessage()
    );
    if ($exception->getCode()) {
        $vista->estado = $exception->getCode();
    } else {
        $vista->estado = 500;
    }

    $vista->imprimir($cuerpo);
}
);

// Extraer segmento de la url
if (isset($_GET['PATH_INFO']))
    $peticion = explode('/', $_GET['PATH_INFO']);
else
    throw new ExcepcionApi(ESTADO_URL_INCORRECTA, utf8_encode("No se reconoce la petición"));

// Obtener recurso
$recurso = array_shift($peticion);
$recursos_existentes = array('contactos', 'usuarios');

// Comprobar si existe el recurso
if (!in_array($recurso, $recursos_existentes)) {
    throw new ExcepcionApi(ESTADO_EXISTENCIA_RECURSO,
        "No se reconoce el recurso al que intentas acceder");
}

$metodo = strtolower($_SERVER['REQUEST_METHOD']);

// Filtrar método
switch ($metodo) {
    case 'get':
    case 'post':
    case 'put':
    case 'delete':
        if (method_exists($recurso, $metodo)) {
            $respuesta = call_user_func(array($recurso, $metodo), $peticion);
            $vista->imprimir($respuesta);
            break;
        }
    default:
        // Método no aceptado
        $vista->estado = 405;
        $cuerpo = [
            "estado" => ESTADO_METODO_NO_PERMITIDO,
            "mensaje" => utf8_encode("Método no permitido")
        ];
        $vista->imprimir($cuerpo);

}

9. Formato XML En El Servicio Web REST

Implementar XML para las respuestas requiere que emplees algún mecanismo para exigir esta característica.

Una de ellas es añadir una extensión del formato al final del recurso en la url. Por ejemplo:

http://localhost/api.peopleapp.com/v1/contactos.json
http://localhost/api.peopleapp.com/v1/contactos.xml

También podrías simplemente añadir un parámetro concatenado para expresar la intención:

http://localhost/api.peopleapp.com/v1/contactos?formato=json
http://localhost/api.peopleapp.com/v1/contactos?formato=xml

Otra forma de hacerlo es usar la cabecera Accept con el tipo MIME:

Accept: text/xml

Según el método que hayas elegido así mismo será la forma de obtener los formatos.

En mi caso usaré el ejemplo 2 para conseguir el formato. Debido a que solo es un parámetro en url usaremos el arreglo $_GET con la clave 'formato':

$formato = $_GET['formato'];

Con este parámetro podemos crear un manejador de errores basado en este formato con tan solo comparar el valor. Obviamente es importante dejar un formato por defecto para prever todas las vías alternas:

// Preparar manejo de excepciones
$formato = isset($_GET['formato']) ? $_GET['formato'] : 'json';

switch ($formato) {
    case 'xml':
        $vista = new VistaXML();
        break;
    case 'json':
    default:
        $vista = new VistaJson();
}

Probemos obtener los contactos de un usuario registrado en formato xml añadiendo el parámetro ?formato=xml.

Respuesta XML En Servicio Web REST

Conclusión

En este artículo has aprendido los conocimientos base para crear tu propio servicio web RESTful basados en un ejemplo que servirá datos sobre contactos.

REST es un estilo que facilita la escritura y legibilidad de nuestros servicios web, ya que sus normas se relacionan a los componentes del protocolo HTTP como lo son los códigos de error, las cabeceras y los verbos de aplicación. Lo que le da un significado más entendible y ordenado.

Recuerda que la autenticación de las cuentas a los usuarios puede basarse en la sola comprobación de un nombre y password si es que te encuentras en una red privada, sin embargo al hacer publica tu API REST necesitarás añadir más seguridad.

Aunque Json y Xml son dos formatos de datos muy reconocidos y utilizados, es posible usar otras modalidades para la propagación de los datos. Todo depende de las necesidades que tengas.

De la misma manera, no todas las aplicaciones requieren que uses REST como arquitectura, dependiendo de los propósitos así mismo determinarás el mejor estilo a seguir.

Ahora solo queda aprender a consumir un servicio web RESTful desde Android.

¿Necesitas Otro Ejemplo De Servicio Web?

Hace unos días lancé un tutorial detallado para crear un servicio web REST para productos, clientes y pedidos. Donde consumo sus recursos desde una aplicación llamada App Productos. Échale un vistazo a todo lo que incluye (tutorial PDF, código completo en Android Studio, código completo PHP, script MySQL con 100 productos de ejemplo).

Comprar app productos