pragone.com
desarrollo, comunidad y monetización
agrégalo a del.icio.us

Trucos para Smarty: 3. Reducir el I/O, compiler plugins

Unos de los secretos de la extensibilidad de Smarty es su sistema de Plugins. Son muy útiles en el sentido de que te permiten agregar funcionalidades de una forma simple. Sin embargo, vienen con un precio: Un archivo más a leer a la hora de procesar una petición.

Esto es despreciable en el caso de los Plugins de compilación, pues sólo se utilizan una vez y luego se dejan de lado (de hecho, las funcionalidades que busques agregar a la compilación es bueno hacerlas como plugins).

Pero para las funciones y los modificadores pueden ser un gran problema.

Esto hay dos formas de resolverlo:

La forma simple

Smarty permite a través de los métodos register_function y register_modifier la inclusión de estos “plugins” sin tener que cargar un archivo adicional. Para este caso, de nuevo resulta muy útil el haber creado una función propia que extienda al Smarty, pues simplemente podemos colocar las definiciones de estas funciones en nuestra clase hija de Smarty y cargarlas en el constructor.

La forma no tan simple… pero mejor

Otra forma de lograr mejores resultados es modificar las funciones de Smarty cambiándolas de funciones de tiempo de ejecución a funciones de compilación.

Este es un procedimiento que no es tan trivial, pues hay que tener en cuenta que en tiempo de compilación no tenemos certeza de los valores que tienen las variables.

Para esto veamos un ejemplo.

Veamos este template sencillo que hace uso del tag {counter}. Este es un tag relativamente simple, sin embargo ofrece varios parámetros que modifican su comportamiento.

Este es el template:

Archivo: DOCROOT/templates/ejemplo3.tpl

<br>
<table cellpadding="5" cellspacing="0" border="1">
<tr>
<TD>up:1</TD>
<TD>up:1</TD>
<TD>start:3; up:1</TD>
<TD>up:2</TD>
<TD>down:1</TD>
<TD>up:1 on >=fifth down:1</TD>
<TD>up:1;no-print;using assign</TD>
</tr>
{counter name="third" start="3" print=false}
{section name="myloop" loop=11}
<TR>
<TD>{counter}</TD>
<TD>{counter name="second"}</TD>
<TD>{counter name="third"}</TD>
<TD>{counter name="fourth" skip=2}</TD>
<TD>{counter name="fifth" direction="down"}</TD>
<TD>{counter name="sixth" print=true}{if $smarty.section.myloop.index == 5}{counter name="sixth" direction="down" print=false}{/if}</TD>
<TD>{counter name="seventh" print=false assign="counter6"}{$counter6}</TD>
</TR>
{/section}
</table>

y este el resultado de su ejecución:

up:1 up:1 start:3; up:1 up:2 down:1 up:1 on >=fifth down:1 up:1;no-print;using assign
1 1 4 1 1 1 1
2 2 5 3 0 2 2
3 3 6 5 -1 3 3
4 4 7 7 -2 4 4
5 5 8 9 -3 5 5
6 6 9 11 -4 6 6
7 7 10 13 -5 6 7
8 8 11 15 -6 5 8
9 9 12 17 -7 4 9
10 10 13 19 -8 3 10
11 11 14 21 -9 2 11

Ahora bien, ¿cómo compila Smarty este template?

Archivo: DOCROOT/smartystuff/compile/%%64^64F^64F63ED0%%ejemplo3.tpl.php

<?php /* Smarty version 2.6.19, created on 2008-03-15 14:59:55
         compiled from ejemplo3.tpl */ ?>
<?php require_once(SMARTY_CORE_DIR . 'core.load_plugins.php');
smarty_core_load_plugins(array('plugins' => array(array('function', 'counter', 'ejemplo3.tpl', 12, false),)), $this); ?>
<br>
<table cellpadding="5" cellspacing="0" border="1">
<tr>
<TD>up:1</TD>
<TD>up:1</TD>
<TD>start:3; up:1</TD>
<TD>up:2</TD>
<TD>down:1</TD>
<TD>up:1 on >=fifth down:1</TD>
<TD>up:1;no-print;using assign</TD>
</tr>
<?php echo smarty_function_counter(array('name' => 'third','start' => '3','print' => false), $this);?>
 
<?php unset($this->_sections['myloop']);
$this->_sections['myloop']['name'] = 'myloop';
$this->_sections['myloop']['loop'] = is_array($_loop=11) ? count($_loop) : max(0, (int)$_loop); unset($_loop);
$this->_sections['myloop']['show'] = true;
$this->_sections['myloop']['max'] = $this->_sections['myloop']['loop'];
$this->_sections['myloop']['step'] = 1;
$this->_sections['myloop']['start'] = $this->_sections['myloop']['step'] > 0 ? 0 : $this->_sections['myloop']['loop']-1;
if ($this->_sections['myloop']['show']) {
    $this->_sections['myloop']['total'] = $this->_sections['myloop']['loop'];
    if ($this->_sections['myloop']['total'] == 0)
        $this->_sections['myloop']['show'] = false;
} else
    $this->_sections['myloop']['total'] = 0;
if ($this->_sections['myloop']['show']):
 
            for ($this->_sections['myloop']['index'] = $this->_sections['myloop']['start'], $this->_sections['myloop']['iteration'] = 1;
                 $this->_sections['myloop']['iteration'] <= $this->_sections['myloop']['total'];
                 $this->_sections['myloop']['index'] += $this->_sections['myloop']['step'], $this->_sections['myloop']['iteration']++):
$this->_sections['myloop']['rownum'] = $this->_sections['myloop']['iteration'];
$this->_sections['myloop']['index_prev'] = $this->_sections['myloop']['index'] - $this->_sections['myloop']['step'];
$this->_sections['myloop']['index_next'] = $this->_sections['myloop']['index'] + $this->_sections['myloop']['step'];
$this->_sections['myloop']['first']      = ($this->_sections['myloop']['iteration'] == 1);
$this->_sections['myloop']['last']       = ($this->_sections['myloop']['iteration'] == $this->_sections['myloop']['total']);
?>
<TR>
<TD><?php echo smarty_function_counter(array(), $this);?>
</TD>
<TD><?php echo smarty_function_counter(array('name' => 'second'), $this);?>
</TD>
<TD><?php echo smarty_function_counter(array('name' => 'third'), $this);?>
</TD>
<TD><?php echo smarty_function_counter(array('name' => 'fourth','skip' => 2), $this);?>
</TD>
<TD><?php echo smarty_function_counter(array('name' => 'fifth','direction' => 'down'), $this);?>
</TD>
<TD><?php echo smarty_function_counter(array('name' => 'sixth','print' => true), $this);?>
<?php if ($this->_sections['myloop']['index'] == 5): ?><?php echo smarty_function_counter(array('name' => 'sixth','direction' => 'down','print' => false), $this);?>
<?php endif; ?></TD>
<TD><?php echo smarty_function_counter(array('name' => 'seventh','print' => false,'assign' => 'counter6'), $this);?>
<?php echo $this->_tpl_vars['counter6']; ?>
</TD>
</TR>
<?php endfor; endif; ?>
</table>

El código no es del todo limpio (de hecho es bastante sucio), sin embargo , obviando la parte del {section} es bastante comprensible… y quisiera presta atención a dos cosas: La primera parte precisamente carga el plugin que implementa el tag {counter}; y las distintas invocaciones al plugin que lucen más o menos así: echo smarty_function_counter(array(), $this);

Parece un poco inútil que para un simple counter se cargue un archivo adicional y se invoque a una funcion. Cuando, en su caso más sencillo, eso es algo que se podría lograr con un: $i = 1; echo $i++;

Aquí es donde entra el beneficio de los plugins de compilación de Smarty.

Vamos a reemplazar la función por un plugin de compilación.

El plugin que necesitamos sería este:

Archivo: DOCROOT/classes/smarty/libs/plugins/compiler.counter_compile.php

<?php
/**
 * Smarty plugin
 * @package Smarty
 * @subpackage plugins
 */
 
/**
 * Smarty {compiler} compiler function plugin
 *
 * Type:     compiler function<br>
 * Name:     counter<br>
 * Purpose:  print out a counter value
 * @author Paolo Ragone <pragone at gmail dot com> (initial author)
 * @param string containing name, start, skip, direction, print and assign attributes
 * @param Smarty_Compiler
 */
function smarty_compiler_counter_compile($tag_attrs, &$compiler)
{
	$_params = $compiler->_parse_attrs($tag_attrs);
	$_confvar_prefix = '$this->_tpl_vars[\'__counters_conf__\']';
 
	if (isset($_params['name'])) $name = $_params['name'];
	else $name = '\'default\'';
 
	$myconf = $_confvar_prefix . '[' . $name . ']';
	$myskip = '$_counter[\'skip\']';
	$mydir = '$_counter[\'direction\']';
	$myassign = '$_counter[\'assign\']';
	$myval = '$_counter[\'val\']';
 
	$r = '$_counter =& ' . $myconf .';' ."\n";
 
	if (isset($_params['start'])) $r .= $myval . ' = (int)' . $_params['start'] . '; ';
	else $r .= ' if (!isset(' . $myval . ')) {' . $myval . ' = 1;} ' ."\n";
 
	if (isset($_params['skip'])) $r .= $myskip . ' = ' . $_params['skip'] . '; ';
	else $r .= ' if (!isset(' . $myskip . ')) {' . $myskip . ' = 1;} ' ."\n";
 
	if (isset($_params['direction'])) $r .= $mydir . ' = ' . $_params['direction'] . '; ';
	else $r .= ' if (!isset(' . $mydir . ')) {' . $mydir . ' = \'up\';} ' ."\n";
 
	if (isset($_params['assign'])) $r .= $myassign . ' = ' . $_params['assign'] . '; ' ."\n";
 
	if (!isset($_params['print']) || $_params['print'] != 'false') {
		$r .= 'if (empty(' . $myassign . ')) { print ' . $myval  . ';} ' ."\n";
	}
 
	$r .= 'if (!empty(' . $myassign . ')) { $this->assign(' . $myassign . ',' . $myval . '); } ' ."\n";
 
	$r .= 'if (' . $mydir . ' == \'down\') {'  ."\n" . 
				$myval . '-= ' . $myskip . '; ' ."\n" . 
				'} else {'  ."\n" .
				$myval . '+= ' . $myskip . '; } ' ."\n";
 
	$r .= 'unset($_counter);' ."\n";
	return $r;
}
 
?>

Y el código del template compilado (una vez cambiados los tags de {counter} por {counter_compile} es (he creado una copia del template):

Archivo: DOCROOT/smartystuff/compile/%%C3^C34^C34FA758%%ejemplo3.1.tpl.php

<?php /* Smarty version 2.6.19, created on 2008-03-15 14:59:56
         compiled from ejemplo3.1.tpl */ ?>
<br>
<table cellpadding="5" cellspacing="0" border="1">
<tr>
<TD>up:1</TD>
<TD>up:1</TD>
<TD>start:3; up:1</TD>
<TD>up:2</TD>
<TD>down:1</TD>
<TD>up:1 on >=fifth down:1</TD>
<TD>up:1;no-print;using assign</TD>
</tr>
<?php $_counter =& $this->_tpl_vars['__counters_conf__']['third'];
$_counter['val'] = (int)'3';  if (!isset($_counter['skip'])) {$_counter['skip'] = 1;} 
 if (!isset($_counter['direction'])) {$_counter['direction'] = 'up';} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?>
<?php unset($this->_sections['myloop']);
$this->_sections['myloop']['name'] = 'myloop';
$this->_sections['myloop']['loop'] = is_array($_loop=11) ? count($_loop) : max(0, (int)$_loop); unset($_loop);
$this->_sections['myloop']['show'] = true;
$this->_sections['myloop']['max'] = $this->_sections['myloop']['loop'];
$this->_sections['myloop']['step'] = 1;
$this->_sections['myloop']['start'] = $this->_sections['myloop']['step'] > 0 ? 0 : $this->_sections['myloop']['loop']-1;
if ($this->_sections['myloop']['show']) {
    $this->_sections['myloop']['total'] = $this->_sections['myloop']['loop'];
    if ($this->_sections['myloop']['total'] == 0)
        $this->_sections['myloop']['show'] = false;
} else
    $this->_sections['myloop']['total'] = 0;
if ($this->_sections['myloop']['show']):
 
            for ($this->_sections['myloop']['index'] = $this->_sections['myloop']['start'], $this->_sections['myloop']['iteration'] = 1;
                 $this->_sections['myloop']['iteration'] <= $this->_sections['myloop']['total'];
                 $this->_sections['myloop']['index'] += $this->_sections['myloop']['step'], $this->_sections['myloop']['iteration']++):
$this->_sections['myloop']['rownum'] = $this->_sections['myloop']['iteration'];
$this->_sections['myloop']['index_prev'] = $this->_sections['myloop']['index'] - $this->_sections['myloop']['step'];
$this->_sections['myloop']['index_next'] = $this->_sections['myloop']['index'] + $this->_sections['myloop']['step'];
$this->_sections['myloop']['first']      = ($this->_sections['myloop']['iteration'] == 1);
$this->_sections['myloop']['last']       = ($this->_sections['myloop']['iteration'] == $this->_sections['myloop']['total']);
?>
<TR>
<TD><?php $_counter =& $this->_tpl_vars['__counters_conf__']['default'];
 if (!isset($_counter['val'])) {$_counter['val'] = 1;} 
 if (!isset($_counter['skip'])) {$_counter['skip'] = 1;} 
 if (!isset($_counter['direction'])) {$_counter['direction'] = 'up';} 
if (empty($_counter['assign'])) { print $_counter['val'];} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?></TD>
<TD><?php $_counter =& $this->_tpl_vars['__counters_conf__']['second'];
 if (!isset($_counter['val'])) {$_counter['val'] = 1;} 
 if (!isset($_counter['skip'])) {$_counter['skip'] = 1;} 
 if (!isset($_counter['direction'])) {$_counter['direction'] = 'up';} 
if (empty($_counter['assign'])) { print $_counter['val'];} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?></TD>
<TD><?php $_counter =& $this->_tpl_vars['__counters_conf__']['third'];
 if (!isset($_counter['val'])) {$_counter['val'] = 1;} 
 if (!isset($_counter['skip'])) {$_counter['skip'] = 1;} 
 if (!isset($_counter['direction'])) {$_counter['direction'] = 'up';} 
if (empty($_counter['assign'])) { print $_counter['val'];} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?></TD>
<TD><?php $_counter =& $this->_tpl_vars['__counters_conf__']['fourth'];
 if (!isset($_counter['val'])) {$_counter['val'] = 1;} 
$_counter['skip'] = 2;  if (!isset($_counter['direction'])) {$_counter['direction'] = 'up';} 
if (empty($_counter['assign'])) { print $_counter['val'];} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?></TD>
<TD><?php $_counter =& $this->_tpl_vars['__counters_conf__']['fifth'];
 if (!isset($_counter['val'])) {$_counter['val'] = 1;} 
 if (!isset($_counter['skip'])) {$_counter['skip'] = 1;} 
$_counter['direction'] = 'down'; if (empty($_counter['assign'])) { print $_counter['val'];} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?></TD>
<TD><?php $_counter =& $this->_tpl_vars['__counters_conf__']['sixth'];
 if (!isset($_counter['val'])) {$_counter['val'] = 1;} 
 if (!isset($_counter['skip'])) {$_counter['skip'] = 1;} 
 if (!isset($_counter['direction'])) {$_counter['direction'] = 'up';} 
if (empty($_counter['assign'])) { print $_counter['val'];} 
if (!empty($_counter['assign'])) { $this->assign($_counter['assign'],$_counter['val']); } 
if ($_counter['direction'] == 'down') {
$_counter['val']-= $_counter['skip']; 
} else {
$_counter['val']+= $_counter['skip']; } 
unset($_counter);
 ?><?php if ($this->_sections['myloop']['index'] ==