Al desarrollar un panel de administración con el Admin Generator de symfony, en ocasiones nos ocurre que desearíamos poder incrustar código PHP en el archivo generator.yml para cumplir nuestros objetivos. Por ejemplo, supongamos que quisiéramos mostrar un listado de elementos distintos en función de los permisos del usuario que está logueado. Nos encantaría poder hacer algo como esto

generator:
  config:
    list:
      <?php if(sfContext::getInstance()->getUser()->hasCredentials('manager')): ?>
      table_method: "doSelectForManagers"
      <?php else: ?>
      table_method: "doSelect"
      <?php endif; ?>

El código escrito no daría ningún error y funcionaría … aunque sólo la primera vez ya que una vez compilado este archivo (convertido en PHP) siempre se ejecutaría el método del “table_method” elegido la primera vez.

Una solución no muy buena
Una posible solución para logar nuestro objetivo sería llevar a cabo la lógica para comprobar los credenciales de usuario dentro del propio módelo, algo así:

// lib/model/doctrine/item.class.php
class Item extends BaseItem
{
  public function doSelect()
  {
    $query = Doctrine_Query()::create();
    if(sfContext::getInstance()->getUser()->hasCredentials('manager'))
    {
      // Añadir una seria de condiciones a la query
    }else{
     // Añadir otras condiciones a la query
    }
    return $query
  }	
}

El inconveniente de esta solución es que estaríamos acoplando muy fuertemente nuestro modelo de datos con el usuario de la sesión lo cual nos haría muy difícil hacer pruebas unitarias más adelante, por lo tanto no es muy aconsejable.

Mi solución
La solución que propongo pasa por hacer lo que se hizo al principio sólo que en lugar de sobre el archivo generator.yml sobre el auténtico objeto PHP en el que se convierte este archivo una vez compilado, la clase xxxGeneratorConfiguration, que es precisamente uno de los dos archivos generados bajo la carpeta “lib” en el módulo (si no sabías para que servían estos archivos, ya puedes hacerte una idea, ;) ).

De esta manera el código quedaría de la siguiente manera

// lib/xxxGeneratorConfiguration.class.php
class xxxGeneratorConfiguration extends BaseXxxGeneratorConfiguration
{
  public function getTableMethod()
  {
    $user = sfContext::getInstance()->getUser()->hasCredentials('manager')
    if($user->hasCredentials('manager')){
      return 'doSelectForManagers';
    }else{
      return 'doSelect';
    }
  }
}

Si quieres conocer algo más en profundidad sobre el admin generator y cómo funciona, a lo mejor te ayude algo la charla que dí en Castellón sobre esto mismo, http://vimeo.com/13325576

Pongamonos en situación, tenemos un listado generado por el AdminGenerator. Decidimos añadir una nueva columna a la lista, algo tremendamente fácil con sólo añadir un nuevo getter al objeto del modelo pero …. (siempre hay un pero) no podemos ordenar por ese nuevo campo, :( .

Bueno, esa situación era hasta hoy, ya que intentaré exponer lo tremendamente sencillo que es conseguir ordenar por una columna virtual.

En primer lugar, y para trabajar sobre un ejemplo “real” consideremos el archi-conocido caso en el que tenemos un modelo de datos formado por usuarios (sfGuardUser) y sus correspondientes perfiles (Profile) guardando una relación de 1:1 y teniendo la tabla Profile la siguiente estructura:

Profile:
  columns:
    full_name: { type: string(255) }
    email:     { type: string(255) }

Cómo era la situación hasta hoy

Consideremos el caso hipotético de que para el desarrollo de una aplicación tenemos que mostrar un listado con todos los usuarios con las siguientes columnas: nombre de usuario, nombre real y email. Hasta hoy, el proceso a seguir era,

1) creamos dos nuevos getter en el objeto sfGuardUser para las dos nuevas columnas virtuales:

// lib/model/sfGuardUser.class.php
<?
class sfGuardUser extends BasesfGuardUser{
 
  public function getFullName()
  {
    $this->Profile->full_name;
  }
 
  public function getEmail()
  {
    $this->Profile->email;
  }
?>

Te podías haber ahorrado definir estos getters si en lugar de esto hubieses creado directamente un proxy del objeto Profile, aunque eso es otra historia

2) Modificamos el archivo generator.yml para que muestre estos dos nuevas columnas virtuales:

      list:
        title:   Listado de Usuarios
        display: [=username, full_name, email]

3) El
Listado de Usuarios con columnas virtuales
pero no podríamos ordenar ni por full_name ni email … hasta hoy! Vamos allá!

Desvirtualizando los campos virtuales

Lo primero que debemos hacer es desvirtualizar los campos que eran virtuales, para ello cambiamos el método que utiliza el AdminGenerator para recoger el listado de elementos:

  list:
    title:   Listado de Usuarios
    display: [=username, full_name, email]
    table_method: doSelectAdmin

debiendo de crear este método en el objeto de la tabla correspondiente

// lib/model/sfGuardUserTable.class.php
<?php
class sfGuardUserTable extends Doctrine_Table{
 
  public static function doSelectAdmin()
  {
    return Doctrine_Query::create()
      ->select('u.username as username, p.full_name as full_name, p.email as email')
      ->from('sfGuardUser u')
      ->leftJoin('u.Profile p');
  }
}
?>

Notemos que el nombre de los campos de la sentencia select coincide con el nombre de los campos en list.display.

Después de esto tendríamos el mismo resultado que en el caso anterior, sin poder seguir ordenando por estas dos nuevas columnas.

Ya no es virtual, ahora es real

Para que symfony sustituya el texto de la cabecera de la tabla por un enlace debemos de indicarle de manera explícita que estos campos son reales y no virtuales. Esto se lo indicamos en el generator.yml:

#generator.yml
fields:
  full_name: { is_real: true }
  email:       { is_real: true }

Resulta curioso que este parámetro no venga en ningún sitio en la documentación de symfony (o al menos, yo no lo he sabido encontrar).

Con esto conseguiremos que el texto de la cabecera (i.e: “Email”, “Nombre”) sea ahora un enlace, aunque si haces click verás que no ocurre nada, esto se debe a que symfony pone una traba más que hay que sortear.

Sí, mis campos son reales y además son “ordenables”

Finalmente hay que indicarle a symfony que sí que queremos poder ordenar por esa columna, por lo que será necesario reescribir un método:

<?php
// /modules/sfGuardUser/actions/actions.class.php
 
class sfGuardUserActions extendes autosfGuardUserActions{
 
    protected function isValidSortColumn($column)
    {
      return in_array($column, array('username', 'full_name', 'email');
    }
}
?>

No hay duda de que una de las mejores características de symfony es el Admin Generator, gracias a él podemos tener un panel de administración listo para usar en cuestión de minutos y además, podemos personalizarlo todo lo que queramos.

Hoy veremos como añadir nuevas acciones sobre objetos (object_actions) a la vista del listado y cómo personalizarlas desde el archivo generator.yml

Las object_actions por defecto
Por defecto, symfony incluye dos acciones para cada elemento del listado, eliminar y editar. Estas dos acciones son un tanto especiales en el sentido de que están ya están “personalizadas”, mostrando un icono distinto e incluso saltando javascript al hacer click (el caso del botón eliminar). En el archivo generator.yml:

generator:
  param:
    config:
      list:
        object_actions:
          _delete: ~
          _edit:   ~

Si no queremos mostrar una de las acciones en el listado bastará con borrarla.

Añadiendo una object_action
Como siempre es más sencillo hablar con ejemplos concretos, supongamos que el listado que estamos mostrado es de noticias y queremos añadir una acción que sea “publicar” de manera que al hacer click sobre ella se publique la noticia. Lo primero que habría que hacer sería añadir esta acción al listado, así:

generator:
  param:
    config:
      list:
        object_actions:
          _delete: ~
          _edit:   ~
          publish: ~

De esta forma, aparecerá un nuevo enlace a “modulo/:id/ListPublish” y por tanto hará falta definir la acción “ListPublish” dentro del archivo actions.class.php

// /module/moduleName/actions/actions.class.php
public function executeListPublish(sfWebRequest $request)
{
  $this->getRoute()->getObject()->publish();
  $this->redirect('@moduleName');
}

Con esto estaría el trabajo hecho, pero siempre puede quedar algo más “bonito”, ;-) .

Personalizando el texto
¿Qué ocurre si queremos que el texto que aparezca sea “Publicar Noticia” en lugar de “Publish”?, muy sencillo:

generator:
  param:
    config:
      list:
        object_actions:
          publish: { label: "Publicar Texto" }

Personalizando el HTML generado
¿Y si queremos añadirle una clase de CSS o un ID al anchor tag? Más fácil:

generator:
  param:
    config:
      list:
        object_actions:
          publish: 
            label: "Publicar Texto"
            params: { class: 'myClass',  id: 'publish-button' }

¿Y para que salte un javascript como en el caso del botón eliminar?

generator:
  param:
    config:
      list:
        object_actions:
          publish: 
            label: "Publicar Texto"
            params:
              class: 'myClass'
              id: 'publish-button'
              onclick: 'return confirm('¿Estás seguro?');'

Notar que para cambiar la imagen del icono debemos de hacerlo a través de la hoja de estilos admin.css modificando el atributo background de

#sf_admin_container ul li.sf_admin_action_publish a

Personalizando la acción que se ejecuta
Por último, si por cualquier motivo queremos que se ejecute la acción “publish” en lugar de “ListPublish” bastará con

generator:
  param:
    config:
      list:
        object_actions:
          publish: 
            action: 'publish'

Dos horas de búsqueda en Internet bien merecen un post, por muy breve que este sea. Sirvame esto como nota mental para situaciones futuras.

Para eliminar el bloque de filtros de la derecha en el admin generator en symfony 1.2 basta con modificar el archivo generator.yml de la siguiente manera

# generator.yml
config:
  filters:
    class: false

Y voilá! Ya no aparecerá nada.

Symfony y Desarrollo Web © Copyright 2009, All Rights Reserved.