Documentación continua en la nube

Tenemos Travis para integración continua, Scrutinizer para inspección continua… pero hasta hace unos días no existía un servicio para generar documentación continua de proyectos.

Para mí es un follón generar y subir a la web los phpDocs de mis proyectos de software libre cada vez que hago commit (ya que quiero tenerlos siempre actualizados); y también es un follón administrar y configurar Jenkins u otros servicios de integración continua para que generen los docs con cada build.

Así que he pensado que no sería tan complicado hacer un sistema que genere y aloje los documentos de mis proyectos en GitHub. Y si es útil para mí, también puede serlo para otros.

Dicho y hecho. Aquí lo tienes, tan sencillo como autorizar la aplicación en tu cuenta en GitHub y elegir qué repositorios quieres documentar. Cada vez que hagas push al repositorio, los documentos serán automáticamente generados y podrás verlos en github-phpdoc.israelviana.es/docs/[tu nombre de usuario GitHub]/[nombre del repositorio]:

Github-phpDocs landing page

Github-phpDocs landing repo list

GitHub-phpDoc documentación

Puedes probarlo ya en github-phpdoc.israelviana.es

El servicio es gratuito y open-source; por supuesto, el código está en GitHub y la documentación en la nube ;-)

De momento solo soporta proyectos en PHP alojados en GitHub (repositorios públicos de momento), pero si hay suficiente demanda se puede ampliar a otros lenguajes o alojamientos.

¿Y tú, generas documentación del código de tus proyectos? Si tienes proyectos PHP en GitHub te agradecería que probases el servicio y me dieras todo el feedback posible. ¡Gracias de antemano!

Y por último, una pregunta: ¿Estarías dispuesto a pagar 2€ por documentación privada de repositorios privados?

WordPress multisite sin WordPress multisite

Como sabrás, WordPress permite alojar varios sitios diferentes con una misma instancia del CMS. Pero este sistema tiene algunas limitaciones:

  • Los sitios deben estar en subdominios o subdirectorios, por ejemplo blog1.example.org o example.org/blog1. No puedes usar nombres de dominio únicos (blog1.com, blog2.com). El plug-in MU Domain Mapping resuelve esta limitación, pero hace magia negra, y no mola.
  • Todos los sitios comparten los themes, plug-ins, usuarios y base de datos (problema de escalabilidad: no podrías usar varios servidores de MySQL). Eso no mola nada. En mi caso, porque tengo diferentes sitios con diferentes administradores y cada uno tiene sus themes y plug-ins.

Así que, viendo cómo funciona el proceso de carga de WordPress, me he dado cuenta de que se puede hacer muy fácilmente un hack que nos permita tener bases de datos, themes y plug-ins separados para cada sitio, y que se puedan mapear a dominios diferentes… más o menos como hace Drupal con el multi-site. Un multi-site de verdad.

Dicho así parece complicado, pero solo hay que pararse un segundo a pensar. Imaginemos que tenemos dos instancias completamente independientes, dos WordPress normales y corrientes, cada uno con su base de datos, etc.

¿Qué tienen de diferente esas dos instancias?

La base de datos, el directorio wp-content y el fichero wp-config.php. Todo lo demás es exactamente igual. Duplicado.

Por tanto, si “engañamos” de alguna forma al cargador de WordPress para cargue un wp-content y wp-config.php u otro en función del nombre de dominio, este se conectará a una u otra base de datos, ya que la información de conexión a MySQL está especificada en wp-config.php. Por tanto, habremos logrado WordPress multi-site.

¿Y cómo hacemos ese truco para “engañar” a WordPress y hacer que cargue uno u otro directorio wp-content y fichero de configuración wp-config.php? En primer lugar, organizaremos nuestros sitios. Por ejemplo, en la raíz de la instancia crearemos un directorio sites, dentro del cual pondremos los wp-config.php y wp-content de cada sitio, en sub-directorios nombrados con el nombre de dominio:

Estructura de ficheros para WordPress multisiteCarpetas para cada sitioCarpeta de un sitio en WordPress multisite

Ahora viene lo bueno. Si echamos un vistazo a wp-load.php vemos que aquí se carga wp-config.php y se declara la constante WP_CONTENT_DIR, que indica la ubicación del directorio wp-content.

Modificaciones en el loader (wp-load.php) para WordPress multisite

Modificamos las declaraciones de WP_CONTENT_DIR y WP_CONFIG poniendo las rutas que nos interesen en función del nombre de dominio solicitado. El código final de wp-load.php quedaría así (código completo de wp-load.php en GitHub):

wp-load.php interceptado (2)

wp-load.php interceptado (1)

Para tener todo listo solo faltan dos detalles: la configuración de Apache y la creación de la base de datos.

Crear la base de datos para el WordPress multisite

Para crear la base de datos de cada site, simplemente tendremos que clonar la BD de un WordPress recién instalado para los demás sitios. No sería difícil automatizarlo con un script.

Configurar Apache

La configuración necesaria para un WordPress multi-sitio con esta receta es muy sencilla: crear tantos vhosts como queramos, haciendo que todos ellos tengan el mismo DocumentRoot. Por ejemplo:


        ServerName wp1.com
        ServerAlias  wp2.com wp3.com
        DocumentRoot /var/www/wp-multisite

Otro detalle importante: para que los administradores de los diferentes sitios puedan acceder a sus directorios wp-content particulares (y solo a los suyos), puedes crear usuarios del sistema (Linux, por supuesto) cuyo directorio $HOME es el subdirectorio de sites que le corresponde. Por ejemplo, para el usuario administrador de blog1.com, su directorio $HOME sería /var/www/wp-multisite/sites/blog1.com, suponiendo que tengamos la instancia de WordPress en /var/www/wp-multisite.

Conclusión

Al final, con este sistema lo que tenemos es prácticamente igual a tener instancias independientes de WordPress, pero con algunas ventajas interesantes:

  • Actualizaciones del núcleo centralizadas, ya que tenemos una sola instancia. Ya no tendrás que ir actualizando sitio por sitio. Eso sí, los plug-ins y themes se deben actualizar independientemente.
  • Ahorro de memoria: ahorras disco duro, y sobre todo ahorras RAM si usas APC (si no lo estás usando… ¡deberías hacerlo!). Una instancia de WordPress ocupa más o menos 20MB en APC; imagínate si tenemos 100 instancias de WordPress en el servidor… ¡compartiendo el código podemos ahorrar Gigas!

Desventajas

  • Es un sistema experimental que aún no he probado en producción, así que puede fallar por cualquier lado :-P
  • No existe una forma automatizada y amigable de crear nuevos sitios. Debes crearlos a mano a partir de plantillas de wp-config.php, wp-content y base de datos.
  • Si se actualiza el núcleo WordPress con el sistema automático, las actualizaciones que afecten a la base de datos solo afectarán a la del sitio desde el cual se haya actualizado el núcleo.
  • Seguridad: a no ser que se establezcan mecanismos adicionales, un administrador de un sitio puede romper con facilidad las otras instancias.

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!