Servicios Web en PHP

19:32 manu 0 Comments

Si hablamos de servicios web o web services, hablamos de interoperabilidad, es decir, intercambio de información entre aplicaciones independientemente del lenguaje en que se hayan desarrollado o las plataformas donde corran. Esto se consigue mediante el uso de determinados protocolos y estándares que veremos más adelante.

La información que proporcionan los servicios web se basa en texto, y el lenguaje utilizado es el XML y JSON. Lo que hace que el rendimiento de estos servicios no sea muy alto, aunque por otro lado además de la independencia entre aplicaciones y sistemas, nos permiten mantener intercambios de información sobre HTTP, lo cual es muy cómodo ya que no encontraremos problemas con firewalls.

Hasta aquí tenemos claro que los servicios web intercambian información a través de peticiones HTTP. Y antes de meternos más en profundidad en el tema, vamos a hacer un pequeño recordatorio sobre qué es una petición HTTP para que nadie se me pierda.

Una llamada HTTP podríamos decir que se compone de una Petición (Request) y su correspondiente Respuesta (Response). ¿Qué elementos participan en esa llamada?

  • La URL sobre la que lanzamos la Petición.
  • El método de petición o verbo, como pueden ser GET, POST, PUT, DELETE, etc.
  • Una cabecera, que son los metadatos que se envían en las peticiones o respuesta HTTP para proporcionar información esencial sobre la transacción en curso. Como pueden ser User-Agent, Content-Type, Set-Cookie, Accept-Language, etc.
  • El código de la respuesta. Como 200 (OK), 204 (No content), 400 (Bad Request), 404 (Not found), etc.


Dicho esto, ¿Cómo podemos hacer una llamada HTTP con PHP? Las dos formas más habituales son mediante la librería cURL y mediante la función file_get_contents.

Petición HTTP mediante cURL

cUrl nos permite además modificar la cabecera de estas peticiones y usar diferentes métodos de petición (verbos). Como por ejemplo, enviar mediante POST los valores "nombre" e "email" en formato JSON (JavaScript Object Notation, que no es más que una mejora de XML, muy extendido debido a que javascript lo lee mucho mejor que el XML. Y que no os confunda el nombre, no es una adaptación especifica creada para Javascript). Veamos un ejemplo de una petición POST en PHP.

$url = "http://tecnomola.com/ejemplo";
$data = ["nombre" => "Manu", "email" => "manu@ejemplo.com"];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER,['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);

Petición HTTP mediante la función file_get_contents

Originariamente concebida para el tratamiento de ficheros, con el parámetro allow_url_fopen=on en el php.ini, nos permite también hacer peticiones HTTP.

$url = "http://tecnomola.com/ejemplo";
$data = ["nombre" => "Manu", "email" => "manu@ejemplo.com"];
$context = stream_context_create([
	'http' => [
		'method' => 'POST',
		'header' => ['Accept: application/json',
			'Content-Type: application/json'],
		'content' => json_encode($data)
	]
]);
$result = file_get_contents($url, false, $context);

Es posible que ahora te estés preguntando, entonces ¿Cuál de los dos debería usar? ¿Cuál es "mejor"? La recomendación es que para realizar simples peticiones GET, uses sin duda file_get_contents, y para todo lo demás cURL.

Y ¿Cuándo utilizar GET y cuando POST? Una regla bien sencilla: si la petición es segura o al usuario le puede interesar guardarla como marcador entonces GET, en caso contrario POST. Una petición segura e interesante para guardar como marcador es por ejemplo una búsqueda: http://tecnomola.com/getPost.php?mes=2&tag=php

Ahora que está claro como realizar peticiones HTTP es hora de echarle un vistazo a la seguridad de estas peticiones. Es decir como autenticar al usuario en cada petición. Pues bien, existen principalmente tres métodos para la autenticación de usuarios a través de HTTP: basic, digest y OAuth. Es fundamental conocerlos antes de continuar con los servicios web.

Autenticación de Acceso Básica

La autenticación básica, como su nombre lo indica, es la forma más básica de autenticación disponible para las aplicaciones web. Fue definida por primera vez en la especificación HTTP en sí y no es de ninguna manera elegante, pero cumple su función. Este tipo de autenticación es el tipo más simple disponible pero adolece de importantes problemas de seguridad que la hace exclusivamente utilizable bajo HTTPS, ya que el usuario y contraseña van en claro, sin encriptar.

Veamos un ejemplo de una petición utilizando la Autenticación de Acceso Básica. En el cliente tendríamos:

$url = "http://tecnomola.com/basic-auth.php";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC ) ;
curl_setopt($ch, CURLOPT_USERPWD, "usuario:pass");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
curl_close($ch);

El usuario y el password deben ir incluidos en ese formato "usuario:password". En nuestro lado del servidor podremos acceder a esos valores mediante el array de entorno $_SERVER.

if (isset($_SERVER['PHP_AUTH_USER'])) {
	$usuario = $_SERVER["PHP_AUTH_USER"];
	$pasword = $_SERVER["PHP_AUTH_PW"];
}

Autenticación de Acceso Digest

Muy similar a la autenticación básica. Aunque este método soluciona el problema del envío de datos en claro. Se aplica una función hash a la contraseña antes de ser enviada sobre la red, lo que resulta más seguro que enviarla en texto plano como en la autenticación básica.

Cuando se trabaja con Digest, el servidor debe hacer el primer movimiento cuando descubre que un cliente quiere acceder a una zona restringida. Entonces el servidor es quien provee una serie de valores al cliente en la directiva WWW-Authenticate de la cabecera. Estos valores son:

  • Valor llamado nonce ("number used once" único para cada request), que se utilizara para el hash.
  • Valor llamado opaque, otro valor único proporcionado por el servidor, y que se espera que sea devuelto por el cliente sin modificaciones.
  • Valor llamado realm, que no es mas que una cadena para informar al cliente sobre el ámbito de la autenticación.

En PHP quedaría de la siguiente manera:

$nonce = md5(uniqid());
$opaque = md5(uniqid());
$realm = 'Usuarios autorizados a tecnomola.com';

if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
	header('HTTP/1.1 401 Unauthorized');
	header(sprintf('WWW-Authenticate: Digest realm="%s", 
		nonce="%s", opaque="%s"', $realm, $nonce, $opaque));
	header('Content-Type: text/html');
	echo 'Necesitas autenticarte';
	exit;
}

Cuando el cliente recibe esta respuesta, tiene que crear el hash con los valores recibidos, y esto es combinando el nombre de usuario y la contraseña con el realm, un valor llamado nonce ("number used once" único para cada request) y otra información:

$A1 = md5("$username:$realm:$password");
$A2 = md5($_SERVER['REQUEST_METHOD'] . ":$uri");
$response = md5("$A1:$nonce:$A2");

El cliente responde ahora de vuelta en una cabecera Authorization el nombre de usuario, el realm, el nonce, el opaque, la uri, y el hash que hemos generado:

Authorization: Digest username="%s", realm="%s", nonce="%s", opaque="%s", uri="%s", response="%s"

Cuando el servidor recibe esta información realiza los mismos pasos realizados con el cliente para ver si la información de autenticación es la misma. Explicado esto, veamos un ejemplo de autenticación Digest mediante cURL. La parte cliente quedaría de la siguiente manera:

$url = "http://tecnomola.com/digest-auth.php";
$post_info = array(
	'campo1' => 'value1',
	'campo2' => 'value2'
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST) ;
curl_setopt($ch, CURLOPT_USERPWD, "usuario:pass");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$response = curl_exec($ch);
echo $response;
curl_close($ch);

Y en la parte servidor:

$realm = 'Area restringuida';

// Array de usuarios autorizados
$users = array('admin' => 'adminpass', 'guest' => 'guestpass');

if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.
        '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

    die('Necesitas autenticarte');
}

if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset($users[$data['username']]))
    die('Credenciales erróneas!');

$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if ($data['response'] != $valid_response)
    die('Credenciales erróneas!');

echo 'Ok login correcto: ' . $data['username'];

function http_digest_parse($txt)
{
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));

    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }

    return $needed_parts ? false : $data;
}

Autenticación OAuth

Esta autenticación propone una solución a un problema muy concreto y bastante frecuente actualmente: Cómo permitimos a aplicaciones de terceros acceso seguro a la información del usuario. Un ejemplo de esto son los sitios web donde puedes acceder con tu cuenta de Google o Facebook, por ejemplo. En este post no voy a hablaros de como funciona este método, sin embargo es dejo este pequeño diagrama para que veáis como es el flujo con Google.



Visto ya cómo establecer comunicaciones entre el cliente-servidor, le toca el turno a la información que enviamos. Ya comenté que la información compartida entre cliente y servidor esta basada en texto, pero ¿Cómo formateamos esa información? Tenemos básicamente XML y JSON. Vamos a ver un ejemplo de la misma información, en ambos lenguajes:

XML

<agenda>
	<evento>
		<dia>23</dia>
		<mes>12</mes>
		<invitados>
			<invitado>
				<nombre>Anton</nombre>
				<apellido> Sanchez </apellido>
			</invitado>
			<invitado>
				<nombre> Manu </nombre>
				<apellido> Tome </apellido>
			</invitado>
		</invitados>
	</evento>
</agenda>

JSON

{agenda:[{"dia":23, "mes":12, "invitados": [{"nombre":"Anton", "apellido":"Sanchez"},{"nombre":"Manu", "apellido":"Tome"}]}}]

¿Cuándo usar XML y cuando usar JSON?

Si la exactitud de la información no es crítica, si existe necesidad de que la información sea poco pesada (aplicaciones móviles por ejemplo), o el cliente que va tratar esa información es un cliente javascript, la respuesta es JSON. En caso contrario XML. Vamos a ver como tratar ambos lenguajes en PHP.

JSON

Tratar JSON con PHP es realmente sencillo:
echo json_encode(array("saludo" => "hola mundo"));
Produciría el siguiente JSON:
{"saludo":"hola mundo"}
Y para convertir un JSON en información que podamos tratar tenemos la siguiente función:
$data = json_decode('{"saludo":"hola mundo"}');
Esto nos devuelve un objeto, en el cual accederemos de la siguiente manera:
echo $data->{'saludo'};
Si preferimos tener la información en un array, deberemos establecer el segundo parámetro de la función a TRUE.
$data = json_decode('{"saludo":"hola mundo"}', true);
En cuyo caso accederemos de la siguiente manera:
echo $data['saludo'];

Trabajar con JSON en PHP como has podido observar, resulta increíblemente sencillo. Pero si estamos trabajando con objetos un simple json_encode() no nos servirá. Para esta situación PHP proporciona el interfaz JsonSerializable que nos da control sobre la información a convertir de nuestro objeto. Un ejemplo sencillo sería:

class tiendaObject implements JsonSerializable
{
	public function jsonSerialize() {
		// tratamos nuestro objeto
		// como ejemplo quitamos las verduras
		unset($this->verdura);
		return $this;
	}
}

$tienda = new tiendaObject();
$tienda->fruta = array("manzanas", "peras", "limones");
$tienda->verdura = array("lechuga", "espinacas");
$tienda->bebida = array("agua", "refrescos","vino","zumos");
echo json_encode($tienda);

XML

Trabajar con XML no es tan sencillo como con JSON, pero tampoco es complicado. Existen 3 maneras de trabajar con XML en PHP:

1. SimpleXML: recomendado para casi todos los casos, es muy sencillo y fácil de manejar. Veamos un ejemplo:

$document = new SimpleXMLElement("");
$kings = $document->addChild("hotel");
$kings->addAttribute("name", "Kings Hotel");
$kings->addChild("rooms", 12);
$kings->addChild("price", 150);
$queens = $document->addChild("hotel");
$queens->addAttribute("name", "Queens Hotel");
$queens->addChild("rooms", 17);
$queens->addChild("price", 150);
$grand = $document->addChild("hotel");
$grand->addAttribute("name", "Grand Hotel");
$grand->addChild("rooms", 27);
$grand->addChild("price", 100);

2. DOM: Cuando SimpleXML se queda corto, podemos usar DOM, mucho más potente aunque también un poco más complicado.

$document = new DOMDocument();
$hotels = $document->createElement('hotels');
$document->appendChild($hotels);
$kings = $document->createElement("hotel");
$name = $document->createAttribute('name');
$name->value = "Kings Hotel";
$kings->appendChild($name);
$rooms = $document->createElement("rooms", 12);
$kings->appendChild($rooms);
$price = $document->createElement("price", 150);
$kings->appendChild($price);
$hotels->appendChild($kings);
$queens = $document->createElement("hotel");
$name = $document->createAttribute('name');
$name->value = "Queens Hotel";
$queens->appendChild($name);
$rooms = $document->createElement("rooms", 17);
$queens->appendChild($rooms);
$price = $document->createElement("price", 150);
$queens->appendChild($price);
$hotels->appendChild($queens);
$grand = $document->createElement("hotel");
$name = $document->createAttribute('name');
$name->value = "Grand Hotel";
$grand->appendChild($name);
$rooms = $document->createElement("rooms", 27);
$grand->appendChild($rooms);
$price = $document->createElement("price", 100);
$grand->appendChild($price);
$hotels->appendChild($grand);

3. XMLReader, XMLWriter, y XMLParser: muy útiles cuando el tamaño de los XML son muy grandes ya están funcionan con streams y por tanto no cargan todo el documento en memoria de una vez.

Ok, ya estamos en condiciones de entrar el los distintos modelos de servicios web dispobibles.

RPC

El servicio RPC o Llamada a Procedimiento Remoto, es básicamente eso, llamar a funciones remotamente. Generalmente un API RPC suele estar escuchando en el mismo punto, por lo que todas las peticiones HTTP van a la misma URL. Cada petición incluye el nombre de la función que se desea invocar, así como los parámetros que se quieran incluir en la llamada. Por ejemplo:

http://api.ejemplo.com/services/rest/?method=fotos.buscar&tags=coche

Crear un servicio RPC como se puede ver es bastante sencillo, tan fácil como seleccionar la clase que nos interesa y exponerla en HTTP. Por ejemplo supongamos que tenemos la clase Eventos:

class Eventos
{
	protected $events = array(
		1 => array("name" => "Excellent PHP Event",
			"date" => 1454994000,
			"location" => "Amsterdam"
		),
		2 => array("name" => "Marvellous PHP Conference",
			"date" => 1454112000,
			"location" => "Toronto"),
		3 => array("name" => "Fantastic Community Meetup",
			"date" => 1454894800,
			"location" => "Johannesburg")
	);
	/**
	* Obtener todos los eventos
	*
	* @return array Coleccion de eventos
	*/
	public function getEvents() {
		return $this->events;
	}
	/**
	* Obtener un evento por su identificador
	*
	* @param int $event_id Identificador del evento
	*
	* @return array Los datos del evento
	*/
	public function getEventById($event_id) {
		if(isset($this->events[$event_id])) {
			return $this->events[$event_id];
		} else {
			throw new Exception("Event not found");
		}
	}
}

Sería tan sencillo como implementar algo como:

require "Events.php";

if(isset($_GET['method'])) {
	switch($_GET['method']) {
		case "eventList":
			$events = new Events();
			$data = $events->getEvents();
			break;
		case "event":
			$event_id = (int)$_GET['event_id'];
			$events = new Events();
			$data = $events->getEventById($event_id);
			break;
		default:
			http_response_code(400);
			$data = array("error" => "bad request");
			break;
	}
} else {
	http_response_code(400);
	$data = array("error" => "bad request");
}

header("Content-Type: application/json");
echo json_encode($data);

Este ejemplo muestra como usar el servicio RPC para devolver un JSON, que no es lo mismo que JSON-RPC. JSON-RPC es un estándar al igual que XML-RPC, protocolos muy similares, que siguen una definición muy estricta. Si los requerimientos de lo que necesitas no son muy estrictos, es un proyecto el que sólo trabajas tú, o no tienes ninguna restricción por parte de algún otro servicio, RPC es suficiente para ti. Trabajar bajo estándares siempre es aconsejable, pero no siempre obligatorio o la mejor opción.

SOAP

SOAP es un servicio del estilo RPC que se apoya sobre un muy especificado formato de XML. Como está tan bien especificado no es necesario hacer grandes esfuerzos para implementarlo. PHP tiene unas librerías muy completas tanto para cliente como para servidor. Vamos a ver un ejemplo:

En la parte servidor tendríamos:
class MyAPI {
	function hello() {
		return "Hello";
	}
}

$options=array('uri'=>'http://localhost/');
$server = new SoapServer(NULL,$options);
$server->setClass('MyAPI');
$server->handle();

Y en la parte cliente tendríamos:
$options = array('location' => 'http://localhost/server.php', 
		'uri' => 'http://localhost/');
$api = new SoapClient(NULL, $options);
echo $api->hello();


REST

REST o Transferencia de Estado Representacional es una arquitectura que, haciendo uso del protocolo HTTP, proporciona una API que utiliza cuatro métodos HTTP (POST, GET, PUT, and DELETE ) para las operaciones CRUD(Creación, Lectura, Actualización y Borrado) entre el cliente y el servidor, en cualquier formato (XML, JSON).

Los sistemas que siguen los principios REST se llaman con frecuencia RESTful.

Un concepto importante en REST es la existencia de Recursos (elementos de información), que pueden ser accedidos utilizando un identificador global.

Pero ¿Qué es un recurso? Todo lo es, cada registro individual de información es un registro. Podríamos considerar cada fila de una base de datos como un recurso. El un blog, cada post, categoría o autor podría considerarse un recurso. Cada recurso tiene una URI, la cual la identifica de forma única.

Una Colección contiene multiples recursos (del mismo tipo); generalmente suele ser una lista de recursos. Continuando con el ejemplo del blog, podríamos considerar una colección de posts de una determinada temática.

Vamos a ver un ejemplo de como quedaría en el servidor: Pensemos en una lista de deseos, donde almacenaremos el nombre de lo que deseamos y el link en donde se puede adquirir el producto.

require("ItemStorage.php");

set_exception_handler(function ($e) {
	$code = $e->getCode() ?: 400;
	header("Content-Type: application/json", NULL, $code);
	echo json_encode(["error" => $e->getMessage()]);
	exit;
});

$verb = $_SERVER['REQUEST_METHOD'];
$url_pieces = explode('/', $_SERVER['PATH_INFO']);
$storage = new ItemStorage();

if($url_pieces[1] != 'items') {
	throw new Exception('Unknown endpoint', 404);
}
switch($verb) {
	case 'GET':
	...
	break;
case 'POST':
case 'PUT':
	...
	break;
case 'DELETE':
	...
	default:
		throw new Exception('Method Not Supported', 405);
}

header("Content-Type: application/json");
echo json_encode($data);

En esta código podemos ver varias buenas prácticas que merece la pena comentar: Tenemos declarado un gestor de excepciones el cual devuelve el error en el mismo formato que la respuesta que espera el cliente. Tenemos una clase ItemStorage.php que se encarga de manipular la información recibida, dejando las partes RESTful claras y separadas.

Analicemos ahora los métodos básicos que hemos comentado e implementado para realizar nuestro servicio.

1. Crear un recurso con POST

El recurso será creado mediante una petición POST a la colección a la que pertenezca el recurso. El cuerpo del mensaje contendrá una representación del nuevo recurso, y el Content-Type de la cabecera bien establecido para que el servicio sepa como interpretarlo. Cuando un recurso es creado debemos devolver una respuesta con el código de estado a 200 ("OK"), 201 ("Created") o 202 ("Accepted"). En caso de que el recurso no se haya podido crear, el código 400 ("Bad Request") es bastante apropiado. Si el formato que envía el cliente no es legible, debemos responder con un 406 ("Not Acceptable”).

case 'POST':
case 'PUT':
	$params = json_decode(file_get_contents("php://input"), true);
	if(!$params) {
		throw new Exception("Data missing or invalid");
	}
	if($verb == 'PUT') {
		$id = $url_pieces[2];
		$item = $storage->update($id, $params);
		$status = 204;
	} else {
		$item = $storage->create($params);
		$status = 201;
	}
	$storage->save();

	header("Location: " . $item['url'], null,$status);
	exit;
	break;

2. Obtener un Recurso o Coleccion con GET

Como es lógico en este caso no importa el contenido de lo que enviemos, ya que se trata de una lectura. El código de estado será 200 si los datos se han obtenido correctamente. Aunque también podrías recibir los estados 302 ("Found") o 304 ("Not Modified"). En caso contrario podremos recibir 404 para indicar que el registro no existe o 403 si se ha denegado el acceso a ese recurso o colección.

case 'GET':
	if(isset($url_pieces[2])) {
		try {
			$data = $storage->getOne($url_pieces[2]);
		} catch (UnexpectedValueException $e) {
			throw new Exception("Resource does not exist", 404);
		}
	} else {
		$data = $storage->getAll();
	}
	break;

Ejemplo de llamadas:
http://localhost:8080/rest.php/items para recuperar todos los recursos de una colección
http://localhost:8080/rest.php/items/dd44d para recuperar un recurso

3. Actualizar un Recurso con PUT

La manera adecuada de realizar actualizaciones en RESTful es obteniendo primeramente el Recurso, y luego haciendo el PUT tras alterar los campos deseados. Incluso si una parte pequeña del recurso debe ser modificada.

4. Borrado de un Recurso con DELETE

El más sencillo y a la vez peligroso de todos. Esta petición se envía sin cuerpo de mensaje sobre la URI del recurso que se quiere eliminar. Muchos servicios devuelven un 200 o 204 ("No content") para informar de que el borrado se ha realizado con éxito. Y un 404 ("Not found") en caso de no encontrar el recurso.

case 'DELETE':
	$id = $url_pieces[2];
	$storage->remove($id);
	$storage->save();
	header("Location: http://localhost:8080/items", null, 204);
	exit;
	break;

Ahora que conocemos los distintos servicios web, te preguntarás: ¿Cuál es el más indicado para mi proyecto? Si el servicio va a estar mayormente realizando consultas, creando y manipulando información, entonces RESTful es la respuesta. Para un servicio que realiza acciones mas que trabajar con cosas, un servicio RPC encajaría mejor. Otra posible consideración sería si necesitas crear una solución fácilmente y deseas un mantenimiento sencillo, en cuyo caso SOAP se ajustaría a estas necesidades.


0 comentarios: