Inyectar lógica en los métodos de cualquier clase PHP dinámicamente

La Programación Orientada a Aspectos (POA) no es la tendencia más de moda hoy en día, pero tiene bastantes implementaciones muy interesantes en diferentes lenguajes. Lo que a continuación presento es una implementación parcial y experimental de POA, aunque siendo puristas no sería POA realmente, ya que no se declaran los aspectos como tales. Además, solo hay dos join points: preMethod y postMethod (antes y después de un método).

Se trata de inyectar funciones antes y/o después de cualquier método de cualquier clase que queramos. Estas funciones podrán manipular la forma en que se ejecutan los métodos objetivo, modificando los parámetros de llamada o incluso deteniendo su ejecución, así como manipular su salida.

Para ver más claro de qué va todo esto, imaginemos una clase sencilla:

class Coche
{
	private $posicion = 0;

	public function avanzar($incremento)
	{
		$this->posicion += $incremento;
	}

	public function getPosicion()
	{
		return $this->posicion;
	}
}

El método avanzar() aumentará la propiedad $posicion y no hará nada más. No implementa restricciones de máximo, mínimo, etc. En principio, el método está ya escrito y no se puede modificar en tiempo de ejecución.

Pongamos por caso que queremos introducir ciertas restricciones en avanzar(), por ejemplo, que el atributo $posicion nunca sea superior a 10. Sería genial poder inyectar una función que, antes de ejecutar avanzar(), que hiciese la comprobación y cancelase la ejecución del método en caso de que la $posicion resultante fuese mayor a 10.

Pues esto es exactamente lo que vamos a hacer con… ¡tachán! ClassTriggers:

//Instanciamos un ClassTriggers que envuelve nuestro objeto Coche
$cocheInterceptado = new ClassTriggers(new Coche);

//Definimos una acción que se ejecutará antes (preMethod) del método avanzar()
$cocheInterceptado->bind('avanzar', 'preMethod', function(&$arguments) {
	echo 'Has solicitado avanzar(' . $arguments[0] . ")\n";

	if ($this->posicion + $arguments[0] > 10) {
		echo 'No puedes avanzar más de 10. Deteniendo ejecución del método';
		return ClassTriggers::COND_STOP_EXECUTION;
	}
});

$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";

$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";

$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";

$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";

Como puedes ver, tenemos una función anónima que recibe el parámetro $arguments. Este array contendrá los parámetros de la llamada a avanzar(), así podemos interceptar el valor que se quiere incrementar a $posicion. Además, como el array se pasa por referencia tenemos la posibilidad de manipular los parámetros de llamada.

En este caso no lo hacemos, sino que devolvemos el valor especial ClassTriggers::COND_STOP_EXECUTION, que sirve para cancelar la ejecución del método. Es decir, si la suma de $posicion + $incremento es superior a 10, el método avanzar() no se ejecutará realmente.

La salida que producirá el script será la siguiente:

Has solicitado avanzar(3)
3
Has solicitado avanzar(3)
6
Has solicitado avanzar(3)
9
Has solicitado avanzar(3)
No puedes avanzar más de 10. Deteniendo ejecución del método
9

Es decir, tratamos de sumar 3 cuatro veces, pero después de la tercera llamada (cuando $posicion vale 9), el método que hemos inyectado detecta que quieres incrementar $posicion por encima de 10, y cancela la ejecución del método.

Llegados a este punto, el abanico de posibilidades es muy amplio, pero espera, que aún queda más ;-)

Es posible definir más de una acción para un evento (los eventos posibles son preMethod y postMethod) y método, y se irán ejecutando en el mismo orden en que se definieron:

$cocheInterceptado->bind('avanzar', 'preMethod', function(&$arguments) {
	echo "Una acción antes de avanzar()\n";
});
$cocheInterceptado->bind('avanzar', 'preMethod', function(&$arguments) {
	echo "Otra acción\n";
});

Por otra parte, el trigger postMethod se ejecuta después del método real, y las acciones podrán acceder al valor devuelto por el método original, para poder evaluarlo, manipularlo, etc. Siguiendo el ejemplo del coche que no debe avanzar más de 10 posiciones, podemos implementar la restricción después de ejecutar avanzar():

$cocheInterceptado->bind('avanzar', 'postMethod', function(&$arguments, $output) {
	if ($this->posicion > 10)
	{
		$this->posicion = 10;
	}
});

$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";
$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";
$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";
$cocheInterceptado->avanzar(3);
echo $cocheInterceptado->getPosicion() . "\n";

En este ejemplo se llama cuatro veces a avanzar(3), por lo que, después de la primera llamada, $posición valdrá 3, después de la segunda 6, después de la tercera 9, y después de la cuarta 12… en teoría:

3
6
9
10

Como puedes ver, el valor final no es 12 sino 10, debido a la acción que hemos ejecutado.

Valores especiales

Ya hemos visto por encima para qué sirve ClassTriggers::COND_STOP_EXECUTION. Devolviendo este valor en una acción, ClassTriggers hará que no se ejecute realmente el método solicitado.

Otro valor especial que se puede devolver es ClassTriggers::NO_MORE_ACTIONS. Con esto indicaremos que, después de ejecutar la acción que devuelve este valor, no se ejecutará ninguna acción más para ese trigger y ese método. Por ejemplo:

$cocheInterceptado->bind('avanzar', 'postMethod', function(&$arguments) {
	if ($this->posicion > 10)
	{
		$this->posicion = 10;
	}
	return ClassTriggers::NO_MORE_ACTIONS;
});

$cocheInterceptado->bind('avanzar', 'postMethod', function(&$arguments) {
	$this->posicion = 99;
});

$cocheInterceptado->avanzar(11);

Si la primera acción no devolviese ClassTriggers::NO_MORE_ACTIONS, la segunda acción se ejecutaría siempre, y en todos los casos $cocheInterceptado->posicion valdría 99. Pero devolviendo ClassTriggers::NO_MORE_ACTIONS se detiene la ejecución de más acciones.

¡Ojo!

Es importante resaltar que las acciones inyectadas operan a nivel de objeto, y no de clase, por lo que si tenemos dos instancias diferentes de Coche, inyectar lógica en una de ellos no modificará en absoluto a la otra. La posibilidad de reutilizar las acciones de un objeto para otras instancias, agrupándolas en aspectos (según la terminología de POA), no ha sido implementada, pero no sería complicado.

Otro dato importante: ClassTriggers sólo funciona con PHP 5.4, ya que hace uso de Closure::bind() para que las acciones puedan acceder a $this.

Posibles ampliaciones

Algunas ideas factibles siguiendo el modelo de ClassTriggers:

  • Añadir un trigger (join point en la terminología de AOP) que se ejecute cuando el método objetivo lance una excepción.
  • Añadir un trigger que se ejecute cuando un atributo del objeto se modifique (se puede implementar con __set).
  • Añadir un trigger que se ejecute cuando un atributo del objeto se lea (se puede implementar con __get).
  • Join point introspection: Que una acción sepa si para ese trigger hay otras acciones, cuáles son e incluso poder manipular su ejecución. Para ello las acciones deberían poder nombrarse.

Conclusiones

Ha sido divertido pensar e implementar ClassTriggers. Si echas un vistazo al código verás que es bastante sencillo y pequeño (¡140 líneas!), aunque hace uso de closures y métodos mágicos (no es apto para principiantes).

Algunas aplicaciones prácticas pueden ser:

  • Inserción de pre-condiciones y post-condiciones en el acceso a bases de datos (auto-iniciar/terminar transacciones, manipular las sentencias SQL…).
  • Auto-guardado de objetos en base de datos: en un objeto con getters y setters de toda la vida, auto-guardar en la base de datos los valores que cambien. Es decir, que cuando llames a $cliente->setNombre('Pepe') automáticamente se guarde ese valor en la base de datos.
  • Hacer un sistema diabólico de versionado de objetos (sí, sí, como Subversion, pero con POPOs).
  • Validar los parámetros de los métodos sin tener que duplicar un montón de código.

No obstante, en la mayoría de aplicaciones del mundo real es más apropiado usar simplemente la herencia de clases y sobreescritura de métodos para introducir pre/post-condiciones a un método cualquiera. Depende del diseño general, de qué nivel de modularidad que necesites y de las ganas que tengas de usar un montón de closures… porque al fin y al cabo, esto no es JavaScript :-P

¡Espero tus comentarios!

El patrón Strategy y las llamadas dinámicas en PHP

Sigo explorando las novedades de PHP 5.3 y, entre ellas están las llamadas dinámicas a métodos estáticos, según he visto en esta interesante presentación de Ilia Alshanetsky. Veamos un ejemplo:

class Aplicacion {

	public static function getVersion() {
		return "1.2";
	}

}

$metodo = "getVersion";

Aplicacion::$metodo(); //Devuelve "1.2"

Se ve claramente que podemos llamar a un método estático cuyo nombre no definimos al escribir el código, sino que está en una variable. Esta característica ya estaba disponible en versiones anteriores de la rama PHP 5, tanto para clases como para métodos, pero no estáticos:

class Conexion {

	public function conectar($servidor, $usuario, $clave) {
		echo "Mira Mamá, me estoy conectando!";
	}

}

$clase = "Conexion";
$método = "conectar";

$objeto = new $clase();
$objeto->$método("localhost", "pepito", "grill0");
//Imprimirá "Mira Mamá, me estoy conectando!"

Bueno, y esta característica parece interesante, pero ¿cómo podemos aplicarla a la vida real? Uno de los ejemplos más claros que se me ocurre es la implementación del patrón de diseño Strategy. En este sencillo patrón se trata de cargar una u otra implementación de una clase dependiendo de alguna variable o acción del usuario. Por ejemplo, imaginemos que queremos conectarnos a bases de datos Oracle o MySQL, según qué servicio escoja el usuario desde un formulario. Primero definimos una clase abstracta Conexion que declare los métodos que las implementaciones han de desarrollar, y escribimos sendas clases hijas para Oracle y MySQL:

abstract class Conexion {

	private $recurso;

	public function conectar($usuario, $clave) { }
}


class ConexionOracle {

	public function conectar($usuario, $clave) {
		$this->recurso = oci_connect($usuario, $clave);
	}

}

class ConexionMysql {

	public function conectar($usuario, $clave) {
		$this->recurso = mysql_connect("localhost", $usuario, $clave);
	}

}

/*
 * $_POST['dbms'] proviene de un cuadro desplegable (option select) en que se
 * da a escoger entre oracle y mysql.
 * ucfirst() pone en mayúscula la primera letra
 */

$conector = "Conexion" . ucfirst($_POST['dbms']);
$instancia = new $conector;

try {
	$instancia->conectar($_POST['usuario'], $_POST['clave']);
} catch (Exception $e) {
	echo "Lo siento, pero por algún motivo la conexión ha fallado";
}

En fin, esto es todo, creo que es evidente la potencia de esta característica del lenguaje, que viene a intentar evitar el uso de eval() y dar al lenguaje un poco más de elegancia, al estilo del class.forName() de Java. Por cierto, para una descripción más profunda del patrón Strategy te recomiendo este interesante artículo de Jack Herrington sobre patrones implementados en PHP.

El curioso método getDocComment()

Uno de los métodos de la clase ReflectionFunction del API de reflexión de PHP se llama getDocComment() y devuelve los comentarios de documentación de una función o método. Por ejemplo:

/**
 * @param   int  $a Primer sumando
 * @param   int  $b Segundo sumando
 * @return  int  Suma de $a + $b
 */
function suma($a, $b) {
    //Otro comentario
    return $a + $b;
}

$funcion = new ReflectionFunction("suma");
echo $funcion->getDocComment() . "n";

El código anterior devolverá el comentario completo:

isra@isra:~$ php -f reflexion.php /** * @param int $a Primer sumando * @param int $b Segundo sumando * @return int Suma de $a + $b */ 

Hay que recalcar que no devuelve todos los comentarios, sino sólo los de documentación. Por eso si olvidamos poner dos asteriscos al inicio, no devolverá nada:

isra@isra:~$ php -f reflexion.php

Tampoco devolverá nada si utilizamos el símbolo de comentario de una línea //:

// @param   int  $a Primer sumando
// @param   int  $b Segundo sumando
// @return  int  Suma de $a + $b
function suma($a, $b) {
    //Otro comentario
    return $a + $b;
}

$funcion = new ReflectionFunction("suma");
echo $funcion->getDocComment() . "n";
isra@isra:~$ php -f reflexion.php

En fin, un método interesante que puede servir para simplificar y aumentar el rendimiento de los sistemas de documentación como phpDocumentator, y también puede ser útil en el control de errores, combinándolo con demás métodos de ReflectionFunction.

PHP avanza

Hay novedades en el mundillo de nuestro lenguaje de programación web favorito ;-) En primer lugar, hace unas semanas se lanzó la segunda release candidate de la versión 5.3. Es muy probable que la tercera llegue en pocos días, y espero ansioso la versión final para el verano. Algunas de las novedades que se incluyen en esta versión han sido traídas de la 6 beta, ya que muchos no podíamos esperar ;-). Están destinadas principalmente a mejorar la orientación a objetos y la elegancia del lenguaje, y molan. A destacar:

  • Namespaces, para separar conjuntos de clases y poder repetir nombres. Equivalen a los pakages de Java. Eso sí, personalmente, el carácter elegido para separar los espacios de nombres me parece feísimo (manías que uno tiene…). Ejemplo:
    FooBarclase::metodoEstatico()
  • Funciones anónimas: una característica que echaba de menos y que se utiliza bastante en JavaScript. Viene a sustituir la chapuza que tenían en create_function(). Ejemplo:
    var $sumar = function($a, $b) {
      return $a + $b;
    }
  • Type Hint para escalares: la restricción de tipos en los parámetros de las funciones estaba ya disponible para arrays y objetos desde la versión 5.1. Ahora, con PHPTypeSafees posible restringir tipos a escalares. Esta característica es quizá la que más me gusta. Ejemplo:
    function repetir(string $cadena, int $veces) {}
  • Un driver nativo para MySQL, que sustituirá al viejo mysql y al mysqli. Se supone que será más “moderno” y rápido.

Pateando a Ruby on Rails

La otra novedad que quería comentar es la existencia de un proyecto para implementar el patrón ActiveRecord —que tan famoso ha hecho a on Rails— en PHP 5.3, precisamente. Está gestándose aún, pero promete más facilidad y rapidez de uso que los ORM que se estilan hoy en día en PHP, Doctrine (del que hablaré en breve aquí) y Propl. Ya se puede descargar la beta de su web. Espero tener tiempo para jugar un poco con él, porque promete bastante.

Patrones de desarrollo web: Multivista (II)

Aquí tienes la procrastinada segunda parte del patrón Multivista. En este post veremos cómo añadir lógica a las vistas y cuáles son los límites de este patrón. Como caso práctico del Multivista orientado a objetos tendremos este mismo blog.

¿Cuándo es necesario este segundo método?

  • Cuando hay plantillas sin salida de texto: PDF, imágenes, flash…
  • Cuando se utilizan helpers o librerías para generar documentos: Pear-Html, DOMDocument, etc
  • Cuando no se quiere emplear un sistema de plantillas.

El patrón Multivista orientado a objetos es similar al patrón Template Two Step View, y se introduce normalmente en una estructura Modelo-Vista-Control de tres capas, como ya expliqué en la primera entrega. Se basa principalmente en la abstracción de las acciones (“postergar los problemas”, como dice mi compañero Dors). Si en el primer método cargábamos los mismos datos introduciéndolos en una u otra plantilla, ahora tendremos una abstracción de tres niveles, donde podremos cargar unos datos comunes (ahí suelen estar las llamadas a los DAO) en el primero, ejecutar unas acciones específicas en el segundo, y el tercer nivel (opcional) serían las plantillas. Veámoslo con un ejemplo:

class VerPost extends Vista {
    public function cargarDatos() {
        Dao::getPost($_GET['id']);
    }

    public post() {
        //Aquí se opera con los parámetros post, si los hubiese (por ejemplo, registrar un comentario)
    }
}

class VerPostHtml extends VerPost {
    //Esta vista podría haber sido implementada con el 1º método
    public function run() {
        $smarty->assign("post", $this->cargarDatos());
        $smarty->display("verPost.tpl");
    }
}

class VerPostPdf extends VerPost {
    //Esta vista requiere código PHP específico, por lo que el 1º método no valdría
    public function run() {
        header("Content-type: application/pdf");
        $datos = $this->cargarDatos());
        $pdf = new Pdf();
        $pdf->addText($datos['titulo']);
        echo $pdf->flush();
    }
}

No es complicado entender este método: la parte de la vista se segrega en dos niveles, el primero es una clase común a todas las vistas del módulo y el segundo nivel son las vistas específicas. Pero el Multivista no se queda en este sencillo código. Podemos establecer clases horizontales comunes a todas las vistas específicas de un tipo completo, por ejemplo, las vistas en PDF:

class ComunPdf {
    static public function run() {
        //Construye una cabecera común a todas las vistas
        header("Content-type: application/pdf");
        $pdf = new Pdf();
        $pdf->addText("Blog de Israel Viana");
        $pdf->addText("www.israelviana.es");
    }
}
class ComunRss {
    static public function run() {
        header("Content-Type: application/xml+rss");
    }
}

Clases comunes a vistas del mismo tipo

Quizás queramos tener alguna funcionalidad común para todas las vistas RSS, por ejemplo. Esta funcionalidad se puede implementar de varias maneras: por una parte las propias clases específicas pueden llamar a las transversales:

class VerPostPdf extends VerPost {
    public function run() {
        $datos = $this->cargarDatos());
        VistaPdf::cabecera();
        $pdf->addText($datos['titulo']);
        echo $pdf->flush();
        VistaPdf::pie();
    }
}

O bien puede hacerse desde el motor de vistas:

//Instancia la clase que corresponda
$vista = $_GET['vista'];
$plantilla = $_GET['plantilla'];
eval("$v = new $vista$plantilla;");

//Invoca los métodos comunes a la plantilla
eval("Comun$plantilla::run();");

//Ejecuta la plantilla
$v->run();

URL amigables

Siendo ortodoxos, esto no estaría dentro del patrón Multivista en sí, pero puede ser una buena forma de manejar las aplicaciones que funcionen con este patrón: veamos cómo interpretar URL del tipo: http://servidor.net/Accion/Vista/param1/param2/…/paramn/vista. Manejar direcciones con esta estructura tiene varias ventajas:

  • Organizar nuestros módulos de una manera bastante intuitiva, separando los scripts “importantes” para nuestra aplicación y las librerías
  • Poder disponer de un script único de acceso (el dispatcher de URL), que maneje los argumentos, los errores y los ataques de inyección SQL y por el estilo. Es decir, se solicite la URL que se solicite, siempre se ejecutará el dispatcher.
  • Cumplir la recomendación de accesibilidad de Jakob Nielsen de hacer URL “hackeables”, para que un usuario pueda cambiar de plantilla sin necesidad de enlaces.
  • Mejorar el posicionamiento en buscadores de nuestro sitio web (SEO).

Lo primero es redirigir todas las peticiones al dispatcher. En Apache se hace así:

RewriteEngine on RewriteRule .* index.php

Finalmente, el código del dispatcher tendría esta pinta (el código está bastante simplificado, la versión real la publicaré con el resto del código del blog):

//Vistas
$vistas = array(
    'Index'     => array('nombre'=>"Index",  'plantillas'=>array("Html", "Rss", "Rdf")),
    'Post'      => array('nombre'=>"Post",       'plantillas'=>array("Html", "Rss", "Rdf")),
    'Archivo'   => array('nombre'=>"Archivo",    'plantillas'=>array("Html", "Rss", "Rdf")),
    'Pagina'    => array('nombre'=>"Pagina",     'plantillas'=>array("Html", "Rss", "Rdf")),
    'Proyectos' => array('nombre'=>"Proyectos", 'plantillas'=>array("Html"))
);

$GLOBALS['vista_por_defecto'] = "Index";
$GLOBALS['plantilla_por_defecto'] = "Html";

$peticion = $_SERVER['REQUEST_URI'];
$parametros = split('/', substr($peticion, strlen(RUTA_R))); //Quita la URL base

//Obtiene el módulo
$v = (strlen($parametros[0])) ? ucfirst($parametros[0]) : $GLOBALS['vista_por_defecto'];

//El módulo especificado no existe
if (!array_key_exists($parametros[0], $vistas)) {
    include("404.php");
    die();
}

//Elimina el módulo de los parámetros
array_shift($parametros);

//Protege contra el error de no incluir barra final en URL que no contienen vista específica, por ejemplo /Post/31/titulo-del-post
$ultimo_parametro = ucfirst($parametros[count($parametros)-1]);
if (array_search($ultimo_parametro, array_merge(array(null), $vistas[$v]['plantillas']))) {
    $p = $ultimo_parametro;
    array_pop($parametros);
} else {
    $p = $GLOBALS['plantilla_por_defecto'];
}

//Verifica si está existe la vista
if (array_key_exists($v, $vistas)) {
    $vista = $vistas[$v];
    //Verifica si existe la plantilla
    //No vale negar con ! porque si el índice es 0, la condición no se cumpliría
    if (null == array_search($p, array_merge(array(null), $vista['plantillas']))) die("La plantilla especificada no existe");
} else {
    die("La sección especificada $v no existe");
}

//Pasa los parámetros a la vista
$GLOBALS['parametros'] = $parametros;
$smarty->assign("parametros", $GLOBALS['parametros']);

$GLOBALS['vista_actual'] = $v;
$GLOBALS['plantilla_actual'] = $p;

//Carga las clases vista y plantilla
require_once("vistas/Vista$v.php");
require_once("vistas/$v/$v$p.php");

//Instancia la plantilla
eval("$vista = new $v$p();");
//Métodos comunes (transversal) al tipo de plantilla
if (file_exists("vistas/Plantilla$p.php")) include("vistas/Plantilla$p.php");
//Ejecuta la plantilla
$vista->run();

Y con esto, un DAO y poco más (veremos en sucesivos artículos qué es lo que falta) tendremos un marco de trabajo para aplicaciones web multivista. No se trataría de un MVC en toda regla, pero sí de una arquitectura de tres capas válida para aplicaciones robustas.