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();
    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

Consideremos que tenemos un modelo de datos en el que existen relaciones entre nuestras entidades, como siempre, un ejempo: dentro de nuestra aplicación tendremos noticias, perteneciendo cada noticia a un autor.

Ahora supongamos que generamos el panel de administración de esta aplicación mediante el admingenerator de symfony. Tal como está escrito el código del AdminGenerator nos encontraremos con que, cada vez que el usuario intente eliminar un autor al que se le haya asignado alguna noticia se producirá un error debido a una violación de la restricción de clave externa.

El usuario que utilice la aplicación no debe, ni tiene, porque saber esto, y es más que posible que intente borrar un autor que lo cumpla. Al ver la pantalla con un error 500 (a él no le aparecerá el error detallado que nosotros sí podemos ver en el entorno de desarrollo) nos llamará asustado y querrá una solución ya!

La mala solución

Una forma de proceder es mediante la famosísima solución conocida como muerto-el-perro-se-acabó-la-rabia. Si quito el botón de eliminar, el usuario no podrá borrar el registro, y si no puede borrar el registro no aparecerá el error. Fin del problema.

Bueno, no tan rápido. Qué ocurre si el usuario descubre que jugando con las URLs puede seguir borrando el registro. No pasa nada, basta con darle credenciales especiales a esa acción, por ejemplo en security.yml.

Bueno, vale, y si se ha equivocado al crear el autor y quiere borrarlo justo después de haberlo creado. En ese caso no habría problemas para eliminarlo ya que al no tener asociado ninguna noticia no surgiría tal error. Parece pues, que esta solución “hace aguas”.

La buena solución

Desde mi punto de vista la solución óptima pasa por permitirle borrar autores al usuario pero indicarle cuando no pueda. Para ello lo que hacemos es sobreescribir el método executeDelete de la acción. Quedaría algo como esto:

<?php
 
    // app/backend/modules/author/actions/actions.php
 
    public function executeDelete(sfWebRequest $request)
    {
        $request->checkCSRFProtection();
 
        $this->dispatcher->notify(new sfEvent($this, 'admin.delete_object', array('object' => $this->getRoute()->getObject())));
 
        try {
            $this->getRoute()->getObject()->delete();
            $this->getUser()->setFlash('notice', 'The item was deleted successfully.');
        }catch (Exception $e){
            $this->getUser()->setFlash('error', 'The item can not be deleted.');
        }
 
        $this->redirect('@author');
    }

Creo que debo una explicación

Creo que siempre he tenido un don (si es que se le puede llamar así) para poner nombres “rimbonbantes” a las cosas. El título de este post comienza con “… and if you just try”. Creo que el chiste (no comment) se entiende sólo si sabemos que el método executeDelete de la acción en baseAuthorActions es:

  // /cache/modules/author/actions/actions.class.php
 
  public function executeDelete(sfWebRequest $request)
  {
    $request->checkCSRFProtection();
 
    $this->dispatcher->notify(new sfEvent($this, 'admin.delete_object', array('object' => $this->getRoute()->getObject())));
 
    if ($this->getRoute()->getObject()->delete())
    {
      $this->getUser()->setFlash('notice', 'The item was deleted successfully.');
    }
 
    $this->redirect('@author');
  }

Para lo más lentos, obsérvese el cambio del “if” por el “try”. Es obvio que para entenderlo se requiere un mínimo de inglés, ;) .

La otra solución

Casi se me olvidaba! Aunque la solución expuesta aquí es ORM-independente, existe una solución para aquellos que utilizan Doctrine basada en la utilización del behaviour SoftDelete, aunque eso es ya otra historia.