Implementar Haanga como sistema de plantillas para OpenCart

haanga

OpenCart es una plataforma de comercio electrónica hecha en , con un impecable, 100% MVC. Como prueba de concepto he reescrito el sistema de plantillas de OpenCart a Haanga (plantillas “Django” para PHP, über eficiente) de César Rodas.

¿ Por que Haanga ?, simplemente porque me gusta.
¿ Por qué OpenCart ?, simplemente porque me gusta como está desarrollado.

Implementación, he creado una clase TemplateEngine con el siguiente código:

  1.  class TemplateEngine {
  2.    public $template = null ;
  3.    public $default = array();
  4.    public $config = array();
  5.  
  6.    function __construct( $templateDir = null  ) {
  7.     $this->loadDefaults( $templateDir);
  8.    }
  9.    
  10.  
  11.    protected function loadDefaults( $templateDir) {
  12.     $this->default = array();
  13.      $this->config = array(
  14.     'template_dir' => (( is_null( $templateDir )) ? Settings::getInstance()->getValue('Web.root') .'' : $templateDir),
  15.     'cache_dir' =>  Settings::getInstance()->getValue('Web.root'). 'cache.templates',
  16.     'compiler' => array(  
  17.      'if_empty' => FALSE,
  18.      'autoescape' => FALSE,
  19.      'strip_whitespace' => TRUE,
  20.       'allow_exec'  => TRUE,
  21.      'global' => array( ),
  22.      'use_hash_filename' => FALSE  
  23.     )
  24.    );
  25.    }
  26.  
  27.   function loadEngine() {
  28.    // incluimos  
  29.     require_once Settings::getInstance()->getValue('Web.root') . 'lib/vendors/Haanga.php';
  30.      Haanga::configure($this->config);
  31.  
  32.  
  33.  
  34.   }
  35.     function loadTemplate( $name ) {
  36.     $this->template =   $name  ;
  37.    }
  38.     function display( $vars = array()) {
  39.     $this->loadEngine();
  40.      $vars = array_merge( $this->default, $vars);
  41.      Haanga::Load( $this->template , $vars);
  42.    
  43.    }
  44.  
  45.  }

Ahora sólo queda indicarle al que haga uso de esta clase:
en /system/engine/controller.php

Modificando los métodos render y fetch, renombrando el antiguo fetch a __fetch

  1. []
  2. protected function render($return = FALSE) {
  3.   foreach ($this->children as $child) {
  4.    $action = new Action($child);
  5.    $file   = $action->getFile();
  6.    $class  = $action->getClass();
  7.    $method = $action->getMethod();
  8.    $args   = $action->getArgs();
  9.  
  10.    if (file_exists($file)) {
  11.     require_once($file);
  12.  
  13.     $controller = new $class($this->registry);
  14.    
  15.     $controller->index();
  16.     $this->data[$controller->id] =  $controller->output;
  17.    
  18.     $this->addToModule( $controller->id, $controller->output );
  19.     } else {
  20.     exit('Error: Could not load controller ' . $child . '!');
  21.    }
  22.   }
  23.  
  24.   if ($return) {
  25.    return $this->fetch($this->template);
  26.   } else {
  27.    $this->output = $this->fetch($this->template);
  28.   }
  29.  }
  30.  
  31.   protected function addToModule( $module , $output ) {
  32.    $i = 0 ;
  33.    if ( !isset( $this->data['modules'] )) { return ;}
  34.    foreach( $this->data['modules'] as $item ) {
  35.     if ( $item['code'] == $module ) {
  36.      $this->data['modules'][$i]['output'] = $output;
  37.      return ;
  38.    
  39.     }
  40.     $i++;
  41.    }
  42.  
  43.   }
  44.  
  45.   protected function fetch($filename) {
  46.    if ( substr( $_SERVER['REQUEST_URI'], 0, 7) == '/admin/') {
  47.     return $this->__fetch( $filename);
  48.    
  49.    } else {
  50.           ob_start();
  51.        $this->templateEngine->loadTemplate( $filename );
  52.        $this->templateEngine->display( $this->data);
  53.      $content = ob_get_contents();
  54.         ob_end_clean();
  55.         return $content;
  56.         }
  57.     }
  58. protected function __fetch($filename) {
  59.   $file = DIR_TEMPLATE . $filename;
  60.       if (file_exists($file)) {
  61.    extract($this->data);
  62.     ob_start();
  63.      
  64.      require($file);
  65.      
  66.      $content = ob_get_contents();
  67.  
  68.         ob_end_clean();
  69.  
  70.         return $content;
  71.      } else {
  72.         exit('Error: Could not load template ' . $file . '!');
  73.      }
  74.  }
  75.  
  76. []

Para evitar utilizar Haanga en la administración se comprueba que la url no sea de la administración, de ser así se utiliza el motor de plantillas original.

  1. []
  2.    if ( substr( $_SERVER['REQUEST_URI'], 0, 7) == '/admin/') {
  3.     return $this->__fetch( $filename);
  4. []

Una de las particularidades de las plantillas es que hacen uso de $$variable a la hora de mostrar la salida de los módulos, y es por esto (imposible, por lo que parece, en Haanga) que he añadido un método y comprobación para que cada módulo devuelva a la plantilla su salida en un elemento “output” de la matriz y es el que mostraremos.

Así, al final, una plantilla como la de la visualización de las categorías queda en algo como los siguientes ejemplos:
/catalog/view/theme/default/common/column_right.tpl

  1. <div id="column_right">
  2.  
  3.  {% for module in modules %}
  4.  
  5.     {{ module.output }}
  6.  
  7.   {% endfor %}
  8.  
  9. </div>

/catalog/view/theme/default/product/category.tpl

  1. {% extends "layout/default.html" %}
  2. {% block content %}
  3. <div id="content">
  4.   <div class="top">
  5.     <div class="left"></div>  
  6.     <div class="right"></div>
  7.     <div class="center">
  8.       <h1>{{ heading_title }}</h1>
  9.     </div>
  10.   </div>
  11.   <div class="middle">
  12.     <table style="padding-bottom:10px;">
  13.    <tr>
  14.         {% if thumb %}
  15.         <td><img src="{{ thumb }}" alt="{{ heading_title }}" /></td>  
  16.  {% endif %}
  17.         {% if description %}
  18.       <td>{{ description }}</td>
  19.  {% endif %}
  20.    </tr>    
  21.  </table>
  22.  
  23.  {% if  !categories  &&   !products %}<div class="content">{{ text_error|default:"" }}</div>{% endif %}
  24.      
  25.     {% if categories %}
  26.  
  27.    <table>
  28.     {% for category in categories %}
  29.      
  30.    
  31.           <a href="{{ category['href'] }}"><img src="{{ category['thumb'] }}" title="{{ category['name'] }}" alt="{{ category['name'] }}" style="margin-bottom: 3px;" /></a><br />
  32.           <a href="{{ category['href'] }}">{{ category['name'] }}</a>
  33.  
  34.     </table>
  35.     {% endfor %}
  36.  
  37.  {% endif %}
  38.  
  39.  
  40.  
  41.     {% if products %}
  42.     <div class="sort">
  43.       <div class="div1">
  44.         <select name="sort" onchange="location = this.value">
  45.          {% buffer sort_order %}{{sort}}-{{order}}{% endbuffer %}
  46.  
  47.          {% for sort  in sorts %}
  48.            <option value="{{ sort['href'] }}" {% if sort_order == sort['value'] %} selected="selected"{% endif %}>{{ sort['text'] }}</option>
  49.           {% endfor %}
  50.         </select>
  51.       </div>
  52.       <div class="div2">{{ text_sort }}</div>
  53.     </div>
  54.    
  55.    
  56.    {% inline "elements/lista_productos.html" %}
  57.    
  58.      
  59.      
  60.     <div class="pagination">{{ pagination }}</div>
  61.  
  62.  {% endif %}
  63.  
  64.   </div>
  65.   <div class="bottom">
  66.     <div class="left"></div>
  67.     <div class="right"></div>
  68.     <div class="center"></div>
  69.   </div>
  70. </div>
  71. {% endblock %}

Singleton para PHP 5.3, y parche para 5.2

singleton

Mediante esta clase abstracta de podremos crear nuestras clases simplemente extendiendo esta clase

  1. abstract class Singleton {
  2.  
  3.      protected function __construct() {
  4.      }
  5.      final public static function getInstance( $calledClassName = null ) {
  6.          static $aoInstance = array();
  7.   if ( $calledClassName == null ) {
  8.          $calledClassName = get_called_class();
  9.          
  10.          }
  11.          if (! isset ($aoInstance[$calledClassName])) {
  12.              $aoInstance[$calledClassName] = new $calledClassName();
  13.         }
  14.  
  15.          return $aoInstance[$calledClassName];
  16.      }
  17.      final private function __clone() {
  18.      }
  19.  }

Ejemplo:

  1.  
  2. class DataBase extends Singleton {
  3. []
  4. }
  5.  
  6. $db = DataBase::getInstance();

El problema que nos encontraremos en PHP 5.2.x es que no existe la función get_called_class por lo que deberemos tener esa función.

  1. if(!function_exists('get_called_class')) {
  2.     class class_tools {
  3.         static $i = 0;
  4.         static $fl = null;
  5.         static function get_called_class() {
  6.             $bt = debug_backtrace();
  7.             if(self::$fl == $bt[2]['file'].$bt[2]['line']) {
  8.                 self::$i++;
  9.             } else {
  10.                 self::$i = 0;
  11.                 self::$fl = $bt[2]['file'].$bt[2]['line'];
  12.             }
  13.  
  14.             $lines = file($bt[2]['file']);
  15.  
  16.             preg_match_all('
  17.                /([a-zA-Z0-9\_]+)::'.$bt[2]['function'].'/',
  18.                 $lines[$bt[2]['line']-1],
  19.                 $matches
  20.             );
  21.             $returnValue = $matches[1][self::$i];
  22.    // comprobamos si lo llamamos desde un call_user_func y similar
  23.             if ( empty( $returnValue ) && isset( $bt[3]['function'] ) && in_array($bt[3]['function'],array('call_user_func', 'call_user_func_array'))) {            
  24.               $returnValue = $bt[3]['args'][0][0];
  25.            
  26.             }
  27.             return $returnValue ;
  28.         }
  29.     }
  30.     function get_called_class() {
  31.         return class_tools::get_called_class();
  32.     }
  33. }

Como se puede ver utiliza la funcion debug_backtrace() para determinar a que clase estamos llamando. Con esta función ya no tendremos problemas a la hora de instanciar nuestros “Singleton” en php.

Ejemplos de uso que funcionan perfectamente en PHP 5.3, y ahora en PHP 5.2

  1. $objeto = DataBase::getInstance() ;
  2. []
  3. $miClase = 'DataBase';
  4. $objeto = call_user_func( array( $miClase, 'getInstance'));

PHP-FPM sobre Ubuntu 8.10

fastcgi-fpm

Tomada la decisión de sólo desarrollar, en el caso de , para la familia 5.3 o superior. En gran medidad por todas las mejoras y cambios que aporta. Para instalarlo y ser procesado desde un servidor web , este debe ser compilado como FastCGI.

La elección de frente a la usual de php5 como fastcgi y un wrapper, es simple: Además de eliminarl el wrapper, php-fpm nos aporta muchas ventajas

Partiendo de que para esta distribución, desde los páquetes, está con la familia 5.2, el pasar a la 5.3 significa que:

a) Debemos compilarla desde las fuentes

b) Debemos instalar un nuevo repositorio donde si exista la versión 5.3.x para .

Existe un repositorio con esta familia de php, repositorio que deberemos añadir a nuestro archivo /etc/apt/sources.list

deb http://php53.dotdeb.org stable all

Una vez esto sólo deberemos actualizar y (si no teníamos instalado anteriormente ) el php5 instalar los nuevos paquetes

  1. sudo apt-get update

En nuestro caso instalaremos el php5-fqm: Un binario de php que se ejecuta como un servicio de FastCGI, facilmente configurable y con procesos separados (si así lo queremos) por usuario, grupo, …

  1. sudo apt-get install php5-fqm

Problemas:

Si queremos compilar módulos de php con pecl, necesitaremos instalar el php5-dev. La instalación de este paquete nos dará error por el paquete libtool, por lo que tendremos que hacer una instalación “peculiar”

  1. #instalamos el libtool
  2. sudo apt-get install libtool
  3. #descargamos el paquete de php5-dev
  4. cd /tmp
  5. wget http://php53.dotdeb.org/dists/stable/php5/binary-i386/php5-dev_5.3.2-0.dotdeb.2_i386.deb
  6. sudo dpkg –install –ignore-depends=libtool   php5-dev_5.3.2-0.dotdeb.2_i386.deb

Con esto tenemos el PHP5-Dev (de la 5.3.x). Al tratar de instalar cualquier módulo nos dará problemas del tipo:

  1. root@miservidor:/tmp# pecl install mongo
  2. downloading mongo-1.0.7.tgz …
  3. Starting to download mongo-1.0.7.tgz (53,750 bytes)
  4. ………….done: 53,750 bytes
  5. 16 source files, building
  6. running: phpize
  7. Configuring for:
  8. PHP Api Version:         20090626
  9. Zend Module Api No:      20090626
  10. Zend Extension Api No:   220090626
  11. configure.in:150: warning: LTOPTIONS_VERSION is m4_require'd but not m4_defun'd
  12. aclocal.m4:2943: LT_INIT is expanded from…
  13. aclocal.m4:2978: AC_PROG_LIBTOOL is expanded from…
  14. configure.in:150: the top level
  15. configure.in:150: warning: LTSUGAR_VERSION is m4_require'd but not m4_defun'd
  16. configure.in:150: warning: LTVERSION_VERSION is m4_require'd but not m4_defun'd
  17. configure.in:150: warning: LTOBSOLETE_VERSION is m4_require'd but not m4_defun'd
  18. configure:4584: error: possibly undefined macro: m4_ifval
  19.       If this token and others are legitimate, please use m4_pattern_allow.
  20.       See the Autoconf documentation.
  21. configure:7363: error: possibly undefined macro: _LT_SET_OPTIONS
  22. configure:7363: error: possibly undefined macro: LT_INIT
  23. ERROR: `phpize' failed

Esto se debe a la conf del libtool instalado (rutas y contenido), por lo que deberemos corregirlo:

  1. sudo ln -s /usr/share/libtool/config/ltmain.sh  /usr/share/libtool/ltmain.sh
  2. sudo ln -s /usr/share/aclocal/libtool.m4 /usr/share/libtool/libtool.m4
  3. cd /usr/share/aclocal
  4. cp libtool.m4  libtool.m4.original
  5. cat > bsolete.m4 ltoptions.m4 ltsugar.m4 ltversion.m4 >> libtool.m4

A partir de este momento ya podemos instalar cualquier módulo con pecl:

  1. pecl install mongo
  2. downloading mongo-1.0.7.tgz …
  3. Starting to download mongo-1.0.7.tgz (53,750 bytes)
  4. ………….done: 53,750 bytes
  5. 16 source files, building
  6. running: phpize
  7. Configuring for:
  8. PHP Api Version:         20090626
  9. Zend Module Api No:      20090626
  10. Zend Extension Api No:   220090626
  11. []
  12. uild process completed successfully
  13. Installing '/usr/lib/php5/20090626+lfs/mongo.so'
  14. install ok: channel://pecl.php.net/mongo-1.0.7
  15. configuration option "php_ini" is not set to php.ini location
  16. You should add "extension=mongo.so" to php.ini

Twig, el sistema de plantillas definitivo

Twig

Siempre he sido muy receloso a la hora de utilizar un sistema de plantillas para , pero la llegada de Twig ha sido todo un acontecimiento.

Con un límpio y optimizado  Fabien Potencier, el creador de Symfony ha creado un sistema en el que las plantillas están 100% libres de código (PHP) y siguen la fabulosa sintaxis de las plantillas de  Django. El resultado es un sistema potente y optimizado, ya que genera unas versiones en php de las plantillas con un código para quitarse el sombrero.

Aun estoy arañando la superficie de esta maravilla, pero con la creación de filtros, y añadiendo nuevas funcionalidades en breve tendremos una versión remozada (internamente) de SevillaPress.

Novas CMS

Novas CMS

Desarrollado con 5, Novas CMS es un gestor multi-cabecera de Ediciones digitales.

Características:

  • Gestión de Flujos de Edición: Creación, revisión y publicación
  • Gestión visual de Portada
  • Multiples cabeceras / Dominios.
  • Roles: Administrador, redactor, colaborador, editor, … Ampliables y configurables
  • Sistema de caché y gestión de recursos.
  • Gestión de librería gráfica y sistema visual de cambio de encuadres.