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

  • Antonio siles

    James me encanto tu tutorial, descargue el ejemplo y tuve muchos problemas al intentar probarlo, por motivo de compatibilidad de versiones de php, la version de php que uso es la version 5.2.1 por motivo de que en mi empresa usan esa version. pude solucionar todas errores de compatibilidad a excepcion de una. espero me puedas ayudar

    —- en el archivo index.php esta funcion no me reconoce
    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);
    }
    );

    en la pagina oficial php.net verifique que dicha funcion esta disponible desde la version 5, yo la version q uso es 5.2.1 . he buscado en internet y no he pillado como usar esta funcion de la manera que usted lo usa

    este error me sale con la herramienta Advanced Rest
    br />
    Parse error: syntax error, unexpected T_FUNCTION, expecting ‘)’ in

    Cualquier ayuda le agradezco de antemano

  • Antonio siles

    James me encanto tu tutorial, descargue el ejemplo y tuve muchos problemas al intentar probarlo, por motivo de compatibilidad de versiones de php, la version de php que uso es la version 5.2.1 por motivo de que en mi empresa usan esa version. pude solucionar todas errores de compatibilidad a excepcion de una. espero me puedas ayudar

    —- en el archivo index.php esta funcion no me reconoce
    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);
    }
    );

    en la pagina oficial php.net verifique que dicha funcion esta disponible desde la version 5, yo la version q uso es 5.2.1 . he buscado en internet y no he pillado como usar esta funcion de la manera que usted lo usa

    este error me sale con la herramienta Advanced Rest
    br />
    Parse error: syntax error, unexpected T_FUNCTION, expecting ‘)’ in

    Cualquier ayuda le agradezco de antemano

  • Daniel Londoño Sanchez

    Que buen tutorial, pero tengo un problema en el registro, es algo similar con el comentario de adrianux velden, yo mando la petición y el resultado del estado es 200: ok pero no hace nada en la bd y genera estos errores:

    Warning: array_shift() expects parameter 1 to be array, null given in /localhost/api.miapp.com/v1/index.php on line 23

    Fatal error: Class ‘usuarios’ not found in /localhost/api.miapp.com/v1/index.php on line 40
    alguno comento que hacia falta codigo en las imagenes pero tampoco encuentro el link del repo para descargar el codigo. Espero me puedan colaborar con una pronta respuesta. Gracias

  • Gabriel B.

    Hola James. Felicidades nuevamente por tan buenos tutoriales. Tengo unas dudas. Como se podrían subir imágenes al server usando la librería Slim? Tienes alguna referencia o tutorial que me pueda orientar? Ya que mi Web Service completo lo hice con Slim y la carga de imágenes también quisiera hacerla por esa vía.
    Saludos desde Cuba. GB

  • Lewis James

    Muy buen tutorial, donde puedo descargar el codigo completo?

    • Quántico

      ve a la parte inferior compartelo y te permitira bajarlo desde dropbox

      • Daniel Londoño Sanchez

        lo hice y no funciono :( que creo que solo es un comentario para que la gente comparta el blog,

      • Daniel Londoño Sanchez

        lo hice y no funciono :( que creo que solo es un comentario para que la gente comparta el blog,

  • Javier

    Hola! gracias por el tutorial, me ha resultado muy útil. Lo único comentar un pequeño problema.. para ver si podrías orientarme… en mi proyecto, con el móvil conectado a una red wifi, funciona perfectamente, pero cuando estoy conectado a “datos móviles” el móvil no se conecta a el web service, me da el fallo que viene a dar si no existe ningún tipo de conexión(lo único que le cuesta un poco mas dar el fallo) :
    ” E/con =(HttpURLConnection)url.(4853): failed to connect to /xxx.xxx.xxx.xxx (port 80): connect failed: ECONNREFUSED (Connection refused) java.net.ConnectException: failed to connect to /xxx.xxx.xxx.xxx (port 80): connect failed: ECONNREFUSED (Connection refused)”.
    Había probado con anterioridad a hacer conexiones móvil- mysql con jdbc… y con la conexión a datos funcionaba :( gracias de antemano por tu respuesta!

  • esau

    Que tal! Muy interesante tu tutorial solo que tengo un detalle y espracticamente desde el principio, no me muestra contactos/1 y no lo gro comprender porque? lo eh movido a controladores y es lo mismo no me muestra mas que no reconoce la url

  • Rodrigo Ivan Canepa Cruz

    Hola James, una pregunta, por lo que veo cuando un cliente REST le envía un cuestionario a la base de datos, por ejemplo para agregar un nuevo usuario ésta le responde un estado 200 y le dice que el usuario ha sido registrado con éxito (si es que todo se ha hecho correctamente), pero hay forma de que se pueda enviar una misma respuesta a otro cliente además del que hizo la petición? digamos que desde mi celular registro a un usuario nuevo, la base de datos me devuelve el éxito, pero podría devolver ese mismo mensaje digamos a otra computadora que también sea un cliente sin la necesidad de hacer la petición?

    • ¿Será que tu hablas de notificaciones push?

      O sea, tu lo que deseas es tener un aviso inmediato de que un usuario se registró?

      Eso puedes hacerlo con google cloud messaging, donde escribes una petición desde tu servicio web hacia los servidores de google para que tu app android reciba el correspondiente mensaje. En este caso un aviso con los datos del nuevo usuario registrado, supongo yo.

      No se si esto sea lo que preguntas compañero.

  • Hola James,
    Cuando cree el proyecto , abrí un directorio llamado “wisentool” para la aplicación de contactos, pero luego lo eliminé y sitúe mi aplicación en otro directorio. EL caso es que ahora no me muestra la lista de objetos o “contactos”, sale en blanco. En el log me dice que hay un error pues gson fallo y no encotró el directorio wisentool:
    03-30 21:08:49.699 6015-8028/com.amg_eservices.iot.gson E/ActivityThread: Failed to find provider info for com.amg_eservices.wisentool

    He eliminado de gradle “applicationId “com.amg_eservices.iot.gson” y las dependencias y lo he vuelto a insertar despues de limpiar y rebuild el proyecto por si era un problema de caché, también he eliminado el caché, y me sigue diciendo lo mismo. Ni en el manifest ni en ningún archivo he encontrado referencia a dicho directorio pues lo he sustituido en todos los sitios por el actual. Si me pudieras orientar que posible motivo sea el causante que la aplicación busque una y otra vez este directorio que ya no existe…

    Un saludo desde ya y gracias.
    Óscar

  • Laura

    Muy buen artículo, sin embargo, al correrlo localmente me funciona de maravilla, cuando lo corro en un servidor me indica “Response does not contain any data.”, no sé que puede ser

    • Hola Laura, tal vez la base de datos aún no tenga registros o el recurso indicado en la URL no existe

      • Richard K Rivero

        A mi me pasa lo mismo; ya intenté de todas maneras y no lo ejecuta.

  • Martín

    Viejo muchas gracias! es genial lo que expones, gracias por compartirlo abiertamente!
    Abrazo

  • Luisma Benítez

    Felicidades, gran trabajo.

    Sólo un apunte, en local manda los headers correctamente, cuando lo hago a través de Internet, retorna un error 400 porque la APIKEY no se envía.
    Si hacemos un var_dump de apache_request_headers() retorna todas las cabeceras, excepto el Authorization.

    array(6) {
    [“Accept-Language”]=> string(14) “es-ES,es;q=0.8”
    [“Accept-Encoding”]=> string(19) “gzip, deflate, sdch”
    [“Accept”]=> string(3) “*/*”
    [“User-Agent”]=> string(121) “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.103 Safari/537.36”
    [“Connection”]=> string(5) “close”
    [“Host”]=> string(18) “xxxxxxxxx.com”
    }

    Sin embargo, en local si retorna un código 200.

    Saludos!

  • Felicidades por este tuto. Me ha acercado a Restful, una herramienta muy útil.
    Sería posible ver un ejemplo de Registro y Login en Adroid Studio basado en este Restful ?
    Muchas gracias.

    • Que bien que te sirva el tuto Oscar. Claro que es posible un artículo sobre el login y planeo escribirlo, solo que será luego de que escriba otros. Tenme paciencia :D

      Saludos!

      • Gracias James.
        Claro que sí y muchas gracias por tu ayuda.
        En mi caso he adaptado contactos y lo he transformado en objetos (Descripción, marca, modelo, etc..). Cada usuario tiene una lista de objetos que le pertenecen y que se comunican en tiempo real a la base de datos pues lo estoy integrando en un proyecto IOT. Era por ese motivo que me interesaba ver el registro y login pues es un paso previo a la asignación de objetos. Quedo muy pendiente de tus tutos, y de nuevo muchas gracias por tu ayuda que es de gran valor para mi.
        saludos.

  • Angelo

    tu implementación está muy buena! muchas gracias! pero tiene un problema, siempre retorna el estado 400 a la petición porque en el método imprimir() de la clase de la vista siempre haces el http_response_code()

    • Gracias por comentar Angelo. Voy a revisar para cambiar lo que dices, gracias compañero!

  • Marco Antonio Pérez Hernández

    hola gracias por el tutorial espero me salga ya que lo necesito para un trabajo

    tengo una duda por que pones esta direccion api.peopleapp.com/v1/contactos/4

    despues del v1 no existe la carpeta contactos

    tambien para el registro de usuarios pones http://localhost/api.peopleapp.com/v1/usuarios/registro

    y no tenemos la carpeta usuarios porfavor ayuda y gracias :)

    • Ferney Alejandro Gonzalez

      Hola Marco Antonio, contactos y usuarios hacen parte de la sintaxis del webservice según el formato RESTful que se está manejando y hacen referencia a los recursos, es decir en el archivo Index.php se declararon como recursos y a su vez señalan las entidades de la base de datos.

  • Pedro Coyoy

    Hola James: tengo este error disculpa podes sacerme mi duda que puede estar pasando ??

    Strict standards: Non-static method usuarios::registrar() should not be called statically in C:wampwwwapi.peopleapp.comv1controladoresusuarios.php on line 27

    • Edwin Correa

      Saludos, este error me aparece también, debido a la versión de PHP que usa mi servidor; y lo he solucionado cambiando private function a public static function, en las lineas que te relaciona, en este caso la linea 27.

      Tengo un problemita con PATH_INFO, debido a la PATH suministrada por el servidor, y la ubicacion final de los archivos, pero continuamos aprendiendo.

      James, ha sido un tuto muy completo, didactico y sencillo. Muchas gracias y continuaremos leyendote con tan grandes aportes.

  • Richard K Rivero

    Fascinante tutorial amigo; me gustaría saber si puedes orientarme en un REST que reciba archivos; tengo implementado el método $_FILE mediante para subir los archivos desde web, pero aún no hallo la manera de incrustarlo de manera que sea “n” dispositivo el que consuma esa API; ¿es necesario convertir el archivo a un array y parsearlo con JSON?

  • Emiliano Fraile

    Excelente tuto… podria decirme porque puede estar dandome warnings? Non-static method usuarios::loguear() should not be called statically de todas formas funciona correctamente.
    Gracias

  • Arturo Hernández jiménez

    amigo. como para cuando publicas el consumo del web service. imagine que nos lo darías de navidad jejeje. bueno que tengas un buen fin de año. y un buen año nuevo.

  • Adrianux Velden

    Muy buen tutorial…..
    He estado iniciando con webservices ya que mi propósito es de consumirlos para desarrollos DIY con arduino. y me he encontrado con un detalle al momento del registro de un usuario; me está arrojando el código 200 OK en vez de 201. ¿Alguna idea? Saludos!!

    • Ferney Alejandro Gonzalez

      Hola Adrianux, inicialmente a mi me mostraba igual, el código 200 pero no me registraba el usuario, imprimí el mensaje de la excepción y resulto ser que en el código de ejemplo faltaban unas lineas que encontré en el archivo descargable, cuéntame si lo has podido solucionar o puedo ayudarte? un saludo.

  • Irnel Victoria Sosa

    Hola James, es primera vez que escribo un comentario pero desde hace rato sigo tus tutoriales, excelentes hermano.

    Bueno quisera que me ayudaras en un problema que se me presento y aunque no tiene nada que ver con este tema, vi que era el post mas actual.

    En fin el punto es que tengo una Actividad en ella tengo un grid layout con 3 filas y 2 columnas, entonces quiero hacer el uso de fragments, pero aqui esta el problema.

    Hice un layout especifico en XML con varios Group View y view, porque quiero cambiarles las propiedas como el text, background, etc.

    hice mi propia clase que le llame MyFragmet que hereda de Fragment, donde comparto una serie de metodos, para despues llamarlos en otras clases hijas que heredan de MyFragment algo asi:

    class MyFragment extends Fragment {

    inflate(XML file)…

    public void setTitle(title) {

    textview.settitle(title);

    }

    …..
    }

    class Fragment1 extends MyFragment {

    y aqui uso todos los metodos de arriba y cambio las propiedas de los componentes del fragment.

    }

    Espero que hasta aqui me hallas entendido.

    Ahora quiero en cada celda del gridview quiero varias instancias hijas de MyFragment.

    celda1 -> class Fragment1
    celda2 -> class Fragment2

    bueno declare varias fragment en el xml de la actividad, y en el name le asigne la ruta del package de cada clase.

    Pero cuando compilo el codigo se me muestra todos los fragmentos, pero solo uno modificado con el valor de las propiedades que indique.

    No se si esta es la mejor via o hacerlo en la codigo java de la actividad con el fragment manager.

    La idea era que queria hacer un user control como en visual studio, y exportar las propiedades para reutilizarlas y no repetir codigo , que creo que esa es una de las ideas de los fragments.

    Al final es el mismo fragmento el que quiero reutilizar varias veces en las celdas del grid view

    Bueno espero me disculpes toda la charla y espero una de tus brillantes soluciones.
    Gracias de antemano James.

  • Edwin Santiago

    felicidades por este gran tutorial, a la gran espera del proximo tutorial

    • Gracias Edwin. El siguiente ya lo estoy escribiendo, espero tenerlo para dentro de 2 días. Saludos!

  • Arturo Hernández jiménez

    amigo para cuando el consumo de este service?? ya no puedo esperar mas…

  • k

    Animo James, esperamos con ansia el tuto del consumo desde app!! Gracias y muy buen trabajo!

  • Gudnar Huanca

    Muy buen tutorial, te pido puedas arreglar el link tus tutoriales, ya que estan caidos, para su descarga.

  • A Mejia Silva

    Hola James, grande este articulo, felicitaciones que gran aporte. Reune una alta cantidad de conceptos que estan sueltos por internet y requieren mucha lectura. Poder agregarle el protocolo de seguridad oauth2 a estos ejemplos es complejo? que tanto hay que hacer? Con solo agregarle el protocolo oauth2 a un ejemplo como este, ya tenemos algo realmente seguro para reusar desde una aplicacion movil para iOS y android por ejemplo?

  • Adam

    Hola James, primero felicitarte por este estupendo blog y por sus tutoriales, me gustaría saber cuando saldrá el próximo tutorial sobre el consumo de este servicio con una app. Un saludo

    • Hola Adam, gracias :) El tuto saldrá este mes, no podría darte la fecha exacta, pero de seguro que este mes lo publico

  • Exelente blog sobre programación en Android, me encantaría hallar uno parecido pero para las Universal Windows Plataform, “aplicaciones universales windows10” ya que se está popularizando la salida inmediata de w10m

  • Camila

    Vamoss Jamesss, nunca entre todos los dias a un sitio a ver si ya hay algo nuevo, Saludos

    • :( Lo siento Camila, muero por seguir escribiendo el próximo artículo, pero debo terminar unos proyectos primero, tenme paciencia :)

  • Robert Stark

    Hola James gracias por los post tan buenos que entregas… cada día voy aprendiendo mucho con la pagina y en twitter! :D

    te quería hacer una consulta y es que realmente me tiene de cabeza y es como poder mostrar un string proveniente de un servidor c++ al presionar un toggle button que se encuentra en un RecyclerView dentro de un fragment… explicando un poco mi app consta de un una actividad principal que tiene un navigation drawer, el navigation drawer tiene diferentes opciones a varios fragmentos y en uno de ellos tengo un RecyclerView donde hay una lista con items que contienen botones, Texviews entre otros elementos; ahora bien al presionar y cambiar el estado del toggle button mando un mensaje de entrada al servidor (un caracter string “D”) mediante un BufferedReader de salida llegándome la respuesta del servidor y capturándola con mi buffer de entrada, el problema es que el dato al verlo en mi logcat mediante un mensaje de log.v y este aparece, pero cuando quiero mostrarlo mediante un textView con el metodo setText() no sale nada y el cuadro esta vacío y no me lanza ningún error…si tienes alguna sugerencia que me pueda ayudar a resolver este problema me ayudaría mucho ya que hace rato estoy tratando de arreglar eso y no me resulta… y he probado con algunos foros de ayuda pero no me ha resultado ninguna que haya encontrado hasta ahora… Muchas Muchas gracias… de un fan de Hermosa Programación!
    pd: el siguiente extracto es parte de lo que tengo que se lanza al apretar el botón toggle iniciando un thread ya que pensé que podía pasar por problemas de procesamiento…

    @Override

    public void run() {

    try {

    String data = “D”;

    OutputStream oos = DataDialog.clientSocket.getOutputStream();

    oos.write(data.getBytes());

    oos.flush();

    BufferedReader in;

    in = new BufferedReader(new InputStreamReader(DataDialog.clientSocket.getInputStream()));

    Phanton_agenda_fragment.entrada = in.readLine();

    if(Phanton_agenda_fragment.entrada != null) {

    Phanton_agenda_fragment.dato = Phanton_agenda_fragment.entrada;

    final Handler handler = new Handler(Looper.getMainLooper());

    Runnable runnable = new Runnable() {

    public void run() {

    handler.post(new Runnable(){

    public void run() {
    //la linea de abajo es mi problema
    MyRecyclerAdapter.MyRecyclerViewHolder.textView.setText(Phanton_agenda_fragment.dato);
    /* este dato de arriba es el que no se muestra en mi recyclerView no aparece nada… :(

    Log.v(“Robert”, “segunda entrada “+Phanton_agenda_fragment.dato);
    // este dato aparece en mi logcat
    }

    });

    }

    };

    new Thread(runnable).start();

    }

    }

    catch (IOException e) {

    e.printStackTrace();

    }

    }

  • Pedro Joya

    Muchas gracias por este maravilloso artículo, tan bien explicado y detallado. Tu blót es todo un referente sobre programación en español

  • Bienvenido

    Hola James,

    En mi caso el código arroja varios errores, aunque funciona de todos modos.

    Por ejemplo, al hacer un GET de los contactos obtengo lo siguiente:

    http://127.0.0.1:8080/api.peopleapp.com/v1/contactos

    En RAW:

    Strict Standards: Non-static method usuarios::validarClaveApi() should not be called statically in C:Program Files (x86)EasyPHP-DevServer-14.1VC11datalocalwebapi.peopleapp.comv1controladoresusuarios.php on line 224

    Strict Standards: Non-static method usuarios::obtenerIdUsuario() should not be called statically in C:Program Files (x86)EasyPHP-DevServer-14.1VC11datalocalwebapi.peopleapp.comv1controladoresusuarios.php on line 225

    Strict Standards: Non-static method contactos::obtenerContactos() should not be called statically in C:Program Files (x86)EasyPHP-DevServer-14.1VC11datalocalwebapi.peopleapp.comv1controladorescontactos.php on line 26
    {
    “estado”: 1,
    “datos”: [
    {
    “idContacto”: “1”,
    “primerNombre”: “Juanito”,
    “primerApellido”: “Perez”,
    “telefono”: “8884447777”,
    “correo”: “juanitop@example.com”,
    “idUsuario”: “1”
    },
    {
    “idContacto”: “2”,
    “primerNombre”: “Jose”,
    “primerApellido”: “Perez”,
    “telefono”: “3334445555”,
    “correo”: “joselito@example.com”,
    “idUsuario”: “1”
    }
    ]
    }

    **********************************

    En Json:

    Unexpected token <

    Gracias de antemano y gracias por el tutorial, muy bueno e interesante.

  • neo

    muy bien explicado y fácil de entender. gracias por el aporte. ¿tienes previsto publicar algo sobre ORM para android? por ejemplo ¿ormLite?. Saludos desde argentina.

    • Gracias neo. Si claro que lo tengo pensado, solo que puede que demore debido a que estoy un poco ocupado, pero de seguro estarán. Saludos!

  • Carlos Rodriguez

    Muy buen artículo, me quedo con varias cosas,
    Recomiendo esta página:
    Blogs, Artículos y Webinars de TI

    Saludos!!!

  • Jaume Parrot Altisent

    Buenas James, qual es la diferencia entre el webservice de este tutorial y el webservice del tutorial Crear Web Service Para Android Con Php, Mysql y Json?
    Un saludo.

    PD: Para hacer una aplicacion ya real qual es mejor?

    • La diferencia está en las reglas que se usa para construir el servicio. Al inicio menciono las cualidades de REST como las urls agradables, el apoyo sobre los verbos, codigos y cabeceras HTTP y de la escalabilidad que existe cuando existen varios recursos.

      REST crea urls predictivas e intuitivas, que permiten navegar sin tener que esforzarse mucho en el significado. Obviamente tienes mucha mejor organización que realizar un servicio web sin estandares.

      Todo depende de la cantidad de información que vayas a procesar. Si es bastante y con el futuro va a crecer, entonces REST puede ser de utilidad.

  • Luciana

    Excelente James, gracias por tanto ,, el proximo tuto viene php+mysql–>GCM<–android?? daleeeee daleeee daleee :), gracias

    • Gracias Luciana :)

      Mmm el proximo es el consumo de este servicio con una app. El de GCM creo que está lejos, pero obvio tengo que escribirlo.

      • Paul Rugel

        Excelente pagina James!! te felicito, aquí es donde me estoy ilustrando de verdad en Android. Pregunta, “… Ahora solo queda aprender a consumir un servicio web RESTful desde Android.” ya lo escribiste? es precisamente lo que me falta para completar mi proyecto!! Saludos!!

        • La app de ejemplo ya casi está terminada, sin embargo como estoy trabajando en unos proyectos, estoy avanzando lento. Espero poder terminar pronto el artículo.

          • Paul Rugel

            James!! Por favor no te olvides de los que necesitamos de ti y tus muy bien valorados conocimientos!!
            Saludos de tu seguidor desde Perú!!

          • ajajaja, precisamente ando escribiendo el nuevo articulo, pero por partesitas, pero de seguro sale este mes. Saludos!

          • Paul Rugel

            Si por favor James!! aunque no lo creas todos los días ingreso a la página para verificar y esperando la publicación… Saludos y fuerza y buenas vibras mi estimado!!

          • Gracias por tu apoyo Paul :)

  • Gabriel Tito

    Muy buen articulo.

  • dsd

    Menuda mierda.

    • Hubiésemos sacado mas provecho de tu comentario si tan solo pusieras las razones de tu opinión con otro tipo de expresión.

      • Alberto

        ni caso James!!! bravo por tus tutoriales

  • JINC06

    Excelente tutorial, muchas gracias, soy su fan :)

  • Excelentes los tutos de este sitio :D

  • Gon Her

    Muy buen articulo. esperamos en un futuro el articulo para consumir? Muchas gracias. Saludos

  • Samuel Alcaraz

    Excelente artículo. Muchas gracias