Montar un servidor REST (II)

Hace unos meses empecé a explicar cómo montar un servidor REST sencillo para servir datos a una aplicación móvil (o de cualquier otro tipo) de forma rápida y reusable. En esa segunda parte voy por fin a completar la explicación entrando en la parte más interesante del código. Aprovecho para aclarar algo que se me pasó en la anterior entrega: parte de este código y las ideas de cómo estructurar todo el servicio en si provienen de este estupendo artículo.

Nos quedamos por ver la clase que debía procesar la petición. En nuestro proyecto, para cada recurso que necesitemos manejar deberemos crear una clase RecursoController, derivada de la clase RESTcontroller . También será necesario crear una clase RecursoModel, hija de RESTmodel,  que se encargará de la interacción con la base de datos. Pero lo veremos después.

La clase RESTcontroller comienza con las definición de unas pocas propiedades y cuatro métodos  abstractos que deberemos implementar para cada entidad.

abstract class RESTcontroller
{
	protected $classname;
	protected $data;
	public $ID;

	abstract function post(&$error);
	abstract function put(&$error);
	abstract function get(&$error);
	abstract function delete(&$error);

 	.
	.
	.
}

La propiedad $data se utiliza para almacenar los datos recibidos en la petición. $classname se utiliza para que la clase hija indique a RESTController qué tipo de entidad maneja. Se utiliza para crear el objeto JSON o XML adecuadamente. La propiedad pública $ID sirve para identificar el recurso que se está tratando. Cuando se trabaja con una colección de recursos esta propiedad vale NULL.

Los métodos abstratos son los que debemos completar en cada clase derivada para responder a cada unas de las 4 operaciones REST básicas. Deben devolver un array de pares clave-valor (un diccionario hablando con mayor propiedad), cuyas claves son los nombres de las propiedades y los valores los obtenidos en la operación. Si el resultado de la operación es un conjunto de entidades (lo cual ocurre generalmente en las operaciones get en las que no especificamos un ID), estos métodos deben devolver un array de diccionarios como los que he descrito. Si queremos notificar algún error, lo haremos a través del parámetro por referencia $error. Por ahora esto puede resultar demasiado abstracto, pero espero que quede más claro al final de esta entrada.

Como vimos en la anterior entrada, index.php, entrega el control al método processRequest de la clase recursoController:

public function processRequest($request_method, $content_type, $data)
{	
	// Copiamos los parámetros recibidos 
	// para usarlos en la case derivada.
	$this->data = $data;
	// Obtenemos el nombre del recurso
	// a partir de la propiedad o del 
	// nombre de la clase
	$classname = isset($this->classname)?$this->classname:get_class($this);

	// En función del tipo de petición
	// llamamos a un método u otro
	// (definidos en la clase derivada)
	switch($request_method) {		
	case 'get':
		$error = "";
		$result = $this->get($error);
		.	
		.
		.
		break;	
	case 'post':
	case 'put':	
		$error = "";
		if ($request_method == 'post') {
			$result = $this->post($error);
		} else {
			$result = $this->put($error);
		}	
		.
		.
		.
		break;
	case 'delete':
		if($this->delete($error)) {
			.
			.
			.
		}
		break;
	default:
		RESTcontroller::sendError(500, "Unkwon request method", $content_type);
	}
}

El código de arriba está explicado básicamente en sus comentarios. Simplemente en función del método solicitado por el cliente se llama a un método u otro (que debemos implementar en la case derivada). Si os fijáis, utilizo el mismo caso del switch para los métodos putpost, porque como vamos a ver el procesamiento de la respuesta es el mismo. En caso de que el método pedido no sea reconocido, se devuelve un error mediante un método de clase que no tiene mucho misterio, y el poco que tiene será desvelado cuando terminemos con este.

Las clases derivadas de RESTController nos van a devolver los resultados  como diccionarios (pares clave-valor) si se tratan de un solo recurso o bien un array de estos diccionarios si es un conjunto de resultados. En el caso de que haya habido un error, devuelven el valor null. Para devolver esos datos al cliente REST, debemos darles el formato adecuado, bien sea XML o JSON. La conversión de un diccionario en JSON es trivial en PHP usando json_encode($array). Para hacer la conversión a XML necesitamos apoyarnos en una pequeña función recursiva como se muestra en esta respuesta de stackoverflow. Así es como quedaría el tratamiento de las peticiones post y put, en las que si todo es correcto devolvemos al cliente el único registro insertado o modificado, respectivamente.

$error = "";
if ($request_method == 'post') {
	$result = $this->post($error);
} else {
	$result = $this->put($error);
}

if($result) {
	if ($content_type == 'json')
	{
		RESTcontroller::sendResponse(200, json_encode($result), 'application/json');
	}
	else 
	{
		$xml = new SimpleXMLElement("<?xml version="1.0"?><$classname/>");
		RESTcontroller::array_to_xml($result, $xml);					
		RESTcontroller::sendResponse(200, $xml->asXML(), 'application/xml');
	}
} else {
	RESTcontroller::sendError(500, $error, $content_type);
}

Y este sería el código de nuestro método protegido para convertir un array en un documento XML:

protected static function array_to_xml($data, &$xml) {
	    foreach($data as $key => $value) {
	        if(is_array($value)) {
	            if(!is_numeric($key)){
	                $subnode = $xml->addChild("$key");
	                RESTcontroller::array_to_xml($value, $subnode);
	            }
	            else{
	                RESTcontroller::array_to_xml($value, $xml);
	            }
	        }
	        else {
	            $xml->addChild("$key","$value");
	        }
	    }
	}

El tratamiento que hacemos de la respuesta de la clase derivada de RESTController es ligeramente diferente, pues tenemos que devolver correctamente tanto registros individuales como conjunto de registros. La forma que tenemos de diferenciar ambos casos es preguntando si está definida una clave llamada ID en el diccionario. Si existe (en el primer ‘nivel’ de profundidad) estamos tratando un registro individual. En caso contrario se trata de un conjunto de ellos.

case 'get':
$error = "";
$result = $this-&gt;get($error);

if ($result) {
	if ($content_type == 'json')
	{
		 // Conjunto de conjunto de resultados		
		if (!array_key_exists("ID", $result)) {
			$result = array("ArrayOf$classname" =&gt; $result);
		}
		RESTcontroller::sendResponse(200, json_encode($result), 'application/json');
	}
	else
	{	
		// Consulta de un sólo elemento			
		if (array_key_exists("ID", $result)) {
			$xml = new SimpleXMLElement("<!--?xml version="1.0"?-->&lt;$classname/&gt;");
			RESTcontroller::array_to_xml($result, $xml);
		} else { // Conjunto de conjunto de resultados
			$xml = new SimpleXMLElement("<!--?xml version="1.0"?-->&lt;ArrayOf$classname/&gt;");
			foreach($result as $object) {
      				$subnode = $xml-&gt;addChild("$classname");
      				RESTcontroller::array_to_xml($object, $subnode);
			}					
		}

		RESTcontroller::sendResponse(200, $xml->asXML(), 'application/xml');
	}	
} else {
	RESTcontroller::sendError(500, $error, $content_type);
}
break;

En realidad, podríamos tratar peticiones get, post y put en un solo caso del switch, pero por claridad los vamos a mantener separado por ahora.

Por último, en las peticiones de tipo delete se devolverá un mensaje de OK si se ha completado la operación con éxito, y el error en caso contrario.

if($this->delete($error)) {
	if ($content_type == 'json')
	{
		RESTcontroller::sendResponse(200, json_encode(array("message" => "OK")), 'application/json');
	}
	else 
	{						
		RESTcontroller::sendResponse(200, "<?xml version="1.0"?><message>OK</message>", 'application/xml');
	}
	RESTcontroller::sendResponse(200);
} else {
	RESTcontroller::sendError(500, $error, $content_type);
}

El código de los métodos de clase sendResponse y getStatusCodeMessage es exactamente el mismo que el del artículo que mencioné arriba.

¿CÓMO SE UTILIZA ESTA CLASE ABSTRACTA?

Cuando diseñamos un servicio REST, normalmente servirá para proporcionar servicios para la manipulación de datos persistentes, almacenados en bases de datos SQL o no SQL. En ambos casos tendremos recursos que se corresponderán con tablas o colecciones. En cualquier caso, tendremos que proporcionar un recurso con un nombre determinado. Si queremos proporcionar servicios sobre un recurso llamado User, lo primero que debemos hacer es crear un fichero user.php que tendrá el siguiente aspecto inicial:

<?PHP
require_once('RESTcontroller.php');

class UserController extends RESTcontroller {
	function UserController() {
		$this->classname="User";
	}

	function get(&$error) {
		$error = '';
		$response = '':

		return $response;
	}

	function post(&$error) {
		$error = '';
		$response = '':

		return $response;
	}

	function put(&$error) {
		$error = '';
		$response = '':

		return $response;
	}

	function delete(&$error) {
		$error = '';
		$response = '':

		return $response;
	}
}

?>

En este fichero definimos una clase UserController heredada de la clase RESTController. Partiendo de esa plantilla, tendremos que escribir nuestro código en función de la lógica de nuestra aplicación y del motor de datos que tengamos debajo, en parte encapsulado por una clase RESTModel. Para continuar con esta entrada, vamos a suponer que trabajamos con una base de datos MySQL. De esta forma también podremos presentar la clase RESTModel. El código competo de nuestra clase UserController quedaría así:

<?PHP
require_once('RESTcontroller.php');
require_once('RESTmodel.php');

class UserModel extends RESTmodel {
	function UserModel() {
		$this->tablename = "Users";
		$this->properties["ID"] = 0;
		$this->properties["nick_name"] = "";
		$this->properties["password"] = "";
	}
}

class UserController extends RESTcontroller {
	function UserController() {
		$this->classname="User";
	}

	function get(&$error) {
		$model = new UserModel();
		$error = '';

		// Leer los datos de usarios, conocido su ID
		if (isset($this->ID)) {
			$model->properties["ID"] = $this->ID;
			$response = $model->select();	
			if (!$response) {
				$error = "Not found User with ID ".$this->ID; 
			}
		// Login
		} else if (isset($this->data["nick_name"]) && isset($this->data["password"])) {
			$nick_name = $this->data["nick_name"];
			$password = $this->data["password"];
			$response = $model->select("password='$password' AND nick_name='$nick_name'");
			if (!$response) {
				$error = "Authentication failed"; 
			}
		// Usuarios con nick similar a un nombre dado
		} else if (isset($this->data["name"]) ) {
			unset($model->properties["password"]);
			$name = $this->data["name"];
			$response = $model->selectAll("`nick_name` LIKE '%$name%'");
			if (!$response) {
				$error = "No users found"; 
			}	
		// Petición no reconocida
		} else {
			$response = false;
			$error = "No parameters";
		}

		return $response;
	}

	function post(&$error) {
		// Validamos los datos
		if (isset($this->data["password"]) && $this->data["password"] != "" &&
			isset($this->data["nick_name"]) && $this->data["nick_name"] != "") {	

			$model = new UserModel();					

			// Comprobamos que no exista ya un usuario con ese nick
			$response = $model->selectAll("`nick_name`='".$this->data["nick_name"]."'");	
			if ($response && count($response)>0) {
				$error = "User nick already exits";
				return false;
			} 

			// Copiamos los datos en las propiedades del modelo
			$model->properties["nick_name"] = $this->data["nick_name"];
			$model->properties["password"] = $this->data["password"];	

			$response = $model->insert();
			if ($response) {
				$error = "";
			} else {
				$response = false;
				$error = "Database Insert Error";
			}

		} else {
			$response  = false;
			$error = "Imcomplete User data";
		}	

		return $response;
	}

	function put(&$error) {
		$model = new UserModel();

		// Comprobamos si existe el usuario
		// y de paso precargamos los datos existentes
		$model->properties["ID"] = $this->ID;
		$found = $model->select();

		if ($found) {		
			// Cmabiamos los datos que hayamos recibido					 
			if(isset($this->data["password"]) && $this->data["password"] != "") {						
				$model->properties["password"] = $this->data["password"];	
			}

			$response = $model->update();
			if (!$response) {
				$error = "Database Update Error";
			}	
		} else {
			$response = false;
			$error = "Not found resource with ID ".$this->ID; 
		}
		return $response;
	}

	function delete(&$error) {
		$model = new UserModel();
		$model->properties["ID"] = $this->ID;
		$result = $model->delete();
		if ($result) {
			$error = "";
		} else {
			$error = "Error deleting User ".$this->ID;
		}

		return $result;
	}
}

En cada método he añadido los comentarios necesarios para entender el código. En el método get, en función de los parámetros recibidos se realiza una consulta u otra al modelo. En el método post, realizamos una validación de los datos obligatorios para hacer la inserción. En el método put, primero comprobamos que exista el registro, cargando de paso sus datos en las propiedades del modelo, y luego modificamos los datos que hayamos recibido en la petición, manteniendo el resto intacto. El método delete tiene poco que comentar.

Al principio del fichero hemos definido una clase derivada de la clase RESTModel. En ella definimos una propiedad llamada properties, que contiene un diccionario con los nombres de los campos de nuestra base de datos. Definimos además otra propiedad tablename que indica el nombre de la tabla que vamos a manipular. Mediante estas propiedades podemos crear de forma automatizada las consultas SELECT, INSERT, UPDATE y DELETE en la clase RESTModel. Esta clase además expone una serie de métodos que permite hacer estas operaciones básicas:

<?php

abstract class RESTmodel
{
	public $properties = array();
	protected $tablename;

	function update() {

	}

	function insert() {

	}

	function select($where = "") {

	}

	function selectAll($where = "") {

	}

	function delete() {

	}
}

?>

El funcionamiento de la clase RESTModel da para un futuro artículo. Antes de finalizar no hay que olvidar incluir el fichero user.php en index.html.

require_once('user.php');

Que aproveche.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *