Centralización de constantes para Symfony2 y Twig

A la hora de trabajar con proyectos grandes nos encontramos generalmente con la necesidad de usar ciertos códigos o IDs (PKs) en duro dentro de la lógica de la programación. El caso que posiblemente sea el más usado, es cuando intentamos validar ciertas acciones dependiendo de los estados de los objetos.

Supongamos un caso sencillo como por ejemplo la lista de usuarios del sistema, en donde necesitamos agregar un botón para activarlos o inactivarlos de acuerdo a esta pequeña lógica:

  • Si el usuario se encuentra “activo”, el botón debería ser rojo con un texto “Inactivar Usuario”
  • Si el usuario se encuentra “inactivo”, el botón debería ser normal con un texto “Activar Usuario”

Esto por lo general se traduce en un código de lógica de presentación de una manera parecida a la siguiente, tomando en cuenta que el ejemplo se encuentra dentro de una layout de Symfony2 con twig y Twitter Bootstrap:


{% if usuario.estado == 'A' %}
    <button type="button" class="btn btn-danger">Inactivar Usuario</button>
{% elseif usuario.estado == 'I' %}
    <button type="button" class="btn">Activar Usuario</button>
{% endif %}

Lógicamente, en otro lugar de la aplicación, nos damos cuenta que necesitamos validar algo en la lógica de negocios para efectuar una u otra acción, esto lo podemos hacer con un código parecido al siguiente dentro de un controller por ejemplo:


if($usuario['estado'] === 'A'){
    // Realizar una acción
}elseif($usuario['estado'] === 'I'){
    // Realizar otra acción
}

Como podemos notar este tipo de validaciones se puede tener tanto en lógicas de presentación como también en lógicas de negocios. Esto también es una práctica que no es recomendable, ya que el problema se presenta cuando tenemos esto en muchos lugares de nuestra aplicación y luego, por algún cambio en los requerimientos, es necesario cambiar el código del estado, por ejemplo porque los usuarios requiren ahora que ya no se llame Inactivo sino Bloqueado.

Aquí es donde generalmente optamos por usar la famosa opción “Search in files” de los IDEs de desarrollo y empezamos a buscar todos los lugares en el código donde estamos usando la letra “A” o la letra “I” o buscamos patrones como por ejemplo [=== ‘A'”] o [== ‘A’] para encontrar tanto en archivos php como twig y vamos encontrando los lugares para corregir las validaciones.

Por este motivo, una práctica que siempre utilizo es centralizar estos códigos en constantes de una clase y no escribir los textos dispersos por toda la aplicación sino ir a cambiar en la clase que centraliza todas las constantes. Supongamos una clase como la siguiente:


<?php

namespace AppBundle\Framework;

class Constantes
{
    const USUARIO_CON_ACCESO = 'A';
    const USUARIO_SIN_ACCESO = 'I';
}

De esta manera cada vez que necesito usar un estado, simplemente puedo hacer referencia a la constante y si necesito cambiarla, cambio el texto definido dentro de la clase:


use AppBundle\Framework\Constantes;

if($usuario['estado'] === Constantes::USUARIO_CON_ACCESO){
    // Realizar una acción
}elseif($usuario['estado'] === Constantes::USUARIO_SIN_ACCESO){
    // Realizar otra acción
}

Esto me resulta sumamente útil, pero el problema que se me presentó es que por más que estas constantes pueden ser utilizadas en el código PHP, también las quiero utilizar en la vista usando twig y para que esto sea posible, una opción es crear una variable global dentro de twig para que la clase “Constantes” exista en las plantillas. Para esto lo primero que tenemos que hacer es que nuestra clase se convierta en un servicio de Symfony. Esto lo hacemos muy fácilmente en el archivo services.yml


services:
    app.constantes:
        class:            AppBundle\Framework\Constantes

Con esta configuración estamos diciendo que en el contenedor existirá un servicio llamado app.constantes que será directamente una instancia de la clase en cuestión.

El siguiente paso será crear la variable global de twig en el archivo de configuraciones del proyecto:


twig:
    globals:
        const: "@app.constantes"

Con esto estamos diciendo que dentro de las plantillas twig existirá un objeto llamado const que será una referencia al servicio creado en el paso anterior. La idea entonces sería modificar nuestro código de la vista para que se vea así:


{% if usuario.estado == const.USUARIO_CON_ACCESO %}
    <button type="button" class="btn btn-danger">Inactivar Usuario</button>
{% elseif usuario.estado == const.USUARIO_SIN_ACCESO %}
    <button type="button" class="btn">Activar Usuario</button>
{% endif %}

El segundo problema con el que nos enfrentamos es que twig no reconoce las constantes y al decirle const.USUARIO_CON_ACCESO piensa que existe una propiedad publica o un método público del objeto en lugar de una constante, por lo que retorna un error 500 con el texto: “Method “SHORT” for object “AppBundle\Framework\Constantes” does not exist in …”.

Para esto podemos engañar a twig haciendo uso de los métodos mágicos de PHP __get() e __isset, modificando nuestra clase de Constantes resultando en el siguiente código (los comentarios de cada método lo explican):


<?php

namespace AppBundle\Framework;

class Constantes
{
    const USUARIO_CON_ACCESO = 'A';
    const USUARIO_SIN_ACCESO = 'I';

    private $constants;
    private $constantKeys;

    /**
     * Por reflection obtengo la lista de constantes de la clase y las 
     * agrega a un array privado a nivel del objeto al momento de instanciarse el objeto
     */
    public function __construct()
    {
        $class = new \ReflectionClass(__CLASS__);
        $this->constants = $class->getConstants();

        $this->constantKeys = array_keys($this->constants);
    }

    /**
     * Al momento de invocarse a la constante, PHP interpreta que debería ser una
     * propiedad pública del objeto y como efectivamente no existe, se llama al método 
     * __isset para que nosotros decidamos si existe o no. Lo que hacemos es fijarnos 
     * si el nombre de la constante que es invocada existe dentro de nuestro array de 
     * constantes descubiertas por reflection
     */
    public function __isset($name)
    {
        if (in_array($name, $this->constantKeys)) {
            return true;
        }

        return false;
    }

    /**
     * Cuando el método __isset devuelve true, entra al método __get y devuelve 
     * el valor de la constante
     */
    public function __get($name)
    {
        if (in_array($name, $this->constantKeys)) {
            return $this->constants[$name];
        }

        return null;
    }
}

Con esto ya podemos usar nuestras constantes tanto en PHP como en Twig e incluso los IDEs de desarrollo nos ayudan como se muestra en las siguientes imágenes las capturas de pantalla de PhpStorm.

Constantes - Código PHP

Constantes - Código Twig

6 comentarios en “Centralización de constantes para Symfony2 y Twig”

  1. Esta misma técnica se puede emplear para usar para utilizar estas constantes en nuestros formularios, por ejemplo si desea mostrar un formulario para añadir o quitar los permisos de determinado usuario, básicamente transformamos nuestro arreglo de constantes en un choices en el formulario:

        static public function toChoices()
        {
            return array_map(sprintf("%s::%s", __CLASS__, 'alterKey'), array_flip(self::toArray()));
        }
    
    1. Yes @C0il, you could do this:

          {% if usuario.estado == constant('AppBundle\\Framework\\Constantes::USUARIO_CON_ACCESO') %}
      

      But I was looking for a short way because I use this a lot, And I don’t use an instance like this:

          {% if usuario.estado == constant('USUARIO_CON_ACCESO', constantes) %}
      

      because I would have to pass it an instance in several controllers.

      Also the IDE helps with autocomplete function as you can see in the last two images at the final of the article.

Comenta este artículo