Patrón plugin en PHP

       Estamos acostumbrados a instalar plugins, addons, complementos o similares en nuestros programas (últimamente sobre todo en navegadores), podemos pensar en utilizar un sistema parecido en nuestra página web, con php.

       Las extensiones de los navegadores permiten ampliar la funcionalidad de manera casi arbitraria. Este no es el caso, vamos a hacer un sistema de plugins para hacer algo de distintas maneras. Imaginemos una página web donde puedes subir una foto y aplicar algún filtro sobre ella (quitar ojos rojos, subir brillo, poner en blanco y negro...) La base es aplicar algo sobre una imagen y cada transformación no es más que una implementación concreta. Podríamos querer un sistema en el que solo con añadir nuevas clases php de transformación en una carpeta, se listasen las distintas transformaciones, el usuario pudiera elegirlas y aplicarlas a una imagen, y todo esto sin tener que tocar ni una línea de código de la web en funcionamiento cada vez que añadimos una transformación nueva.

       Vamos a hacer un ejemplo análogo, pero más sencillo. Vamos a hacer transformadores de texto, con dos implementaciones, una para pasar a mayúsculas y otra para poner las líneas separadas.



       La clase InPluginAbstract será de la que heredará cada transformador de texto y donde debemos especificar qué funcionalidad queremos. En este caso tenemos un string privado donde almacenar el texto que vamos transformando, una función para dar una línea de texto a transformar y otra para recuperar todo el texto transformado.

<?php
abstract class InPluginAbstract{
  protected $_string;
  protected $_pluginName;
  public function __construct(){}
  public function transformLine($line){}
  public function getTransformed(){
    return $this->_string;
  }
  public function getPluginName(){
    return $this->_pluginName;
  }
}

?>

       Las clases Mayusculas y Separado son implementaciones, con transformaciones de texto básicas, el código es muy sencillo, muestro el de Mayusculas, ya que el otro es muy similar.

<?php

require_once 'plugins/InPluginAbstract.php';

class Mayusculas extends InPluginAbstract{
  public function __construct(){
    parent::__construct();
    $this->_pluginName="May&uacute;sculas";
  }
  public function transformLine($line) {
    $this->_string.=strtoupper($line);
  }
}

?>

       Y llegamos a la clase que da vida a todo esto, PluginLoader. Usa un patrón singleton para no tener más de una instancia cargando clases y que al final no sepamos qué instancia a cargado qué clase.

       Por lo demás nos interesan 3 métodos. El que nos dice que plugins hay disponibles.

public static function getExistingPlugins(){
  $folder=opendir(self::pluginListFolder);
  $i=0;
  while ($file = readdir($folder)) {
     if ($file != "." && $file != ".." && !is_dir($file) && $file!="InPluginAbstract.php"){
        $plugs[$i++]= str_replace(self::extensionPlugin,"",$file);
     }
   }
   return $plugs;
}

       Hacemos varias comprobaciones para no cargar la clase padre, ni carpetas, solo ficheros. Y los métodos para cargar una clase dinámicamente que usan los métodos class_exists e interface_exists para comprobar que la clase se ha cargado correctamente.

public static function load($class) {
  $file = self::pluginListFolder . $class . self::extensionPlugin;
  if (!file_exists($file)) {
    throw new Exception('El fichero ' . $file . ' que debe contener la clase o interfaz ' . $class . ' no se ha encontrado.');
  }
  require $file;
  if (!class_exists($class, false) && !interface_exists($class, false)) {
    throw new Exception('La clase o interfaz ' . $class . ' no se ha encontrado.');
  }
}

public static function load_once($class) {
  $file = self::pluginListFolder . $class . self::extensionPlugin;
  if (!file_exists($file)) {
    throw new Exception('El fichero ' . $file . ' que debe contener la clase o interfaz ' . $class . ' no se ha encontrado.');
  }
  require_once $file;
  if (!class_exists($class, false) && !interface_exists($class, false)) {
    throw new Exception('La clase o interfaz ' . $class . ' no se ha encontrado.');
  }
}

       La diferencia entre load y load_once es la habitual de php, ya que internamente se usa require o require_once. Con esto hemos cargado dinámicamente la clase y podemos usar objetos de la clase. En el index.php se puede comprobar el funcionamiento. (Se muestra un extracto):

require_once 'PluginLoader.php';

PluginLoader::getPluginLoader(); //Hacemos que el Plugin Loader cree su instancia si no la tuviera

$plugs = PluginLoader::getExistingPlugins(); //Conseguimos la lista de plugins disponibles.
$cadenas = array("hola", "Adios");
for ($j = 0; $j < count($plugs); $j++) {
   PluginLoader::load_once($plugs[$j]); //Cargamos el fichero del plugin

  $transformador = new $plugs[$j]; //Creamos un nuevo transformador del plugin que toque

  echo ("Transformaci&oacute;n de texto usando el plugin: ".$transformador->getPluginName()."<br/>");
  for ($i = 0; $i < count($cadenas); $i++) {
    $transformador->transformLine($cadenas[$i]);
  }
  echo("Da como resultado: <br/>".$transformador->getTransformed()."<br/><br/>");
}

       Como vemos en el ejemplo tenemos 2 cadenas de texto, “hola” y “Adios”. Cargamos todos los plugins disponibles y aplicamos cada transformación sobre el texto. Como podemos ver, aquí se aplican dos transformaciones, pero podríamos agregar 100 más sin tener que cambiar nada de este código. Solo añadiendo más ficheros a la carpeta de plugins. La línea clave del ejemplo es:

$transformador = new $plugs[$j];

       Ya que $plugs es un array con los nombres de las clases de cada plugin. Esto es así porque en este sistema hemos obligado a usar el mismo nombre (mayúsculas incluidas) para el fichero que contiene una clase y el propio nombre de clase.

       El ejemplo tiene otras cosas a mejorar, como poder cargar plugins desde subcarpetas o que solo liste los ficheros que de verdad son de la extensión de nuestros plugins. Pero ilustra como podemos cargar y usar clases para una finalidad concreta de forma dinámica.

       Se puede descargar el código completo del ejemplo: