Sauvegarde/Restauration de filtres Symfony

De e-glop

Les exemples à venir sont basés sur un modèle simple que pris sur les précédents articles que NiKo a publié précédemment, concernant l'embriquement de relations dans des formulaires Doctrine, auxquelles il va ajouter des facilitations pour ses marque-pages :

# in ./config/doctrine/schema.yml
User:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
  relations:
    Bookmarks:
      type: many
      class: Bookmark
      local: id
      foreign: user_id
      onDelete: CASCADE

Bookmark:
  actAs:
    I18n:
      fields: [name]
      actAs:
        Sluggable:
          fields: [name]
          uniqueBy: [name, lang]
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
    url:
      type: string(255)
      notnull: true
    user_id:
      type: integer(4)
      notnull: true
  relations:
    User:
      type: one
      local: user_id
      foreign: id
    Tags:
      class: Tag
      refClass: BookmarkTag
      local: bookmark_id
      foreign: tag_id
      foreignAlias: Bookmarks

Tag:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true

BookmarkTag:
  columns:
    bookmark_id:
      type: integer(4)
      primary: true
      notnull: true
    tag_id:
      type: integer(4)
      primary: true
      notnull: true

Pas besoin de dire que vous devriez reconstruire votre modèle...

Le fichier de fixtures mis à jour :

# in ./data/fixtures/fixtures.yml
User:
  niko:
    name: niko

Bookmark:
  niko_bookmark1:
    User: niko
    name: Slashdot
    url: http://slashdot.org/
    Tags: [geek_tag, tech_tag, php_tag]
  niko_bookmark2:
    User: niko
    name: Delicious
    url: http://delicious.com/
    Tags: [geek_tag, tech_tag]
  niko_bookmark3:
    User: niko
    name: Digg
    url: http://digg.com/
    Tags: [geek_tag, php_tag]

Tag:
  geek_tag:
    name: geek
  php_tag:
    name: php
  tech_tag:
    name: tech


Génération de l'admin du modèle Bookmark

Générons une application "backend" et un module "Bookmark" :

$ ./symfony generate:app backend
$ ./symfony doctrine:generate-admin backend Bookmark

Maintenant améliorons un peu ce module d'admin en modifiant le fichier generator.yml :

generator:
  class: sfDoctrineGenerator
  param:
    model_class:           Bookmark
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              Bookmark
    plural:                Bookmarks
    route_prefix:          bookmark
    with_doctrine_route:   true
    actions_base_class:    sfActions

    config:
      actions: ~
      fields:  ~
      list:
        display: [=name, url, User]
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

Vous devriez à présent pouvoir naviguer dans l'interface d'admin ainsi générée :

Generated-bookmarks.png

Enregistrer les filtres dans une table Doctrine dédiée, et les gérer depuis le contrôleur

Nous allons utiliser Doctrine pour stocker les filtrer à enregistrer, ajoutons donc la définition d'une nouvelle table dans notre schema.yml :

SavedFilter:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
    type:
      type: enum
      values: [Bookmark, User]
      notnull: true
    filter:
      type: string()

Bien entendu, pas besoin de rappeler qu'il faut reconstruire votre modèle...

Ok, maintenant nous allons enregistrer les valeurs sérialisées du filtre dans la colonne "filter" de la table et la colonne "name" sera pratique pour donner un nom aux filtres. La colonne "type" va référencer la table Doctrine dont fait référence le filtre. Rien de difficile ici.

Dans le module d'admin, les filtres sont stockés dans l'attribut tableName.filters de la session utilisateur (où tableName est le nom du module d'admin d'où les paramètres du filtre sont donnés).

Donc ajoutons maintenant une méthode executeSaveFilter() dans le controlleur bookmarkActions. Et tant que nous seront là-dessus, ajoutons également les méthodes executeLoadFilter() et executeDeleteFilter()...

<?php
# in apps/backend/modules/bookmark/actions/actions.class.php
class bookmarkActions extends autoBookmarkActions
{
  public function executeDeleteFilter(sfWebRequest $request)
  {
    $this->forward404Unless($filter = Doctrine::getTable('SavedFilter')->findOneByTypeAndId('Bookmark', $request->getParameter('id')), sprintf('Bookmark filter #%d not found', $request->getParameter('id'))) ;
    
    $filter->delete();
    
    $this->getUser()->setFlash('notice', sprintf('Bookmark saved filters "%s" deleted', $filter->getName()));
    
    $this->redirect('bookmark');
  }

  public function executeLoadFilter(sfWebRequest $request)
  {
    $this->forward404Unless($filter = Doctrine::getTable('SavedFilter')->findOneByTypeAndId('Bookmark', $request->getParameter('id')));
    $this->setFilters(unserialize($filter->getFilter()));
    
    $this->getUser()->setFlash('notice', sprintf('Bookmark saved filters "%s" loaded', $filter->getName()));
    
    $this->redirect('bookmark');
  }
  
  public function executeSaveFilter(sfWebRequest $request)
  {
    $name = trim($request->getGetParameter('name'));
    $savedFilter = new SavedFilter();
    $savedFilter->fromArray(array(
      'name'   => $name ? $name : 'Untitled filter',
      'type'   => 'Bookmark',
      'filter' => serialize($this->getUser()->getAttribute('bookmark.filters', array(), 'admin_module')),
    ));
    $savedFilter->save();
    
    $this->getUser()->setFlash('notice', 'Bookmark filters saved');
    
    $this->redirect('bookmark');
  }
}

Bien entendu, nous allons avoir besoin d'ajouter les routes correspondantes dans le fichier routing.yml de l'application (app/YOURAPP/config/routing.yml) :

# in apps/backend/config/routing.yml
bookmark_filter_delete:
  url: /bookmark/filter/:id/delete
  param: { module: bookmark, action: deleteFilter }
  requirements:
    id: \d+

bookmark_filter_load:
  url: /bookmark/filter/:id/load
  param: { module: bookmark, action: loadFilter }
  requirements:
    id: \d+

bookmark_filter_save:
  url: /bookmark/filter/save
  param: { module: bookmark, action: saveFilter }

Attendez, nous n'avons aucun lien pour enregistrer un filtre depuis l'interface d'admin ! Ajoutons-en un près du bouton Reset de la partie Filtres, en réécrivant le partial _filters.php (dont l'original se trouve dans cache/YOURAPP/YOURENV/modules/autoBookmark/templates/_filters.php) :

// in apps/backend/modules/bookmark/templates/_filters.php from line 11
[...]
<tfoot>
  <tr>
    <td colspan="2">
      <?php echo $form->renderHiddenFields() ?>
      <a href="<?php echo url_for('@bookmark_filter_save') ?>" onclick="document.location = this.href+'?name='+prompt('Enter a name:');return false">
        <?php echo __('Save') ?>
      </a>
      <?php echo link_to(__('Reset', array(), 'sf_admin'), 'bookmark_collection', array('action' => 'filter'), array('query_string' => '_reset', 'method' => 'post')) ?>
      <input type="submit" value="<?php echo __('Filter', array(), 'sf_admin') ?>" />
    </td>
  </tr>
</tfoot>
[...]

Notez qu'un prompt javascript va vous demander un nom pour votre filtre avant de le sauvegarder...

Prompt.png

Ainsi nous pouvons enregistrer un filtre en base de donnée. Maintenant, listons-les après le formulaire de filtres !

Lister les filtres existants

Pour moi, le meilleur endroit pour retrouver des filtres enregistrés c'est la classe bookmarkGeneratorConfiguration, qui a été générée dans le sous-répertoire lib/ du module Bookmark d'admin. Ajoutons-y une nouvelle méthode getSavedFilters() :

<?php
# in apps/backend/modules/bookmark/lib/bookmarkGeneratorConfiguration.class.php
class bookmarkGeneratorConfiguration extends BaseBookmarkGeneratorConfiguration
{
  public function getSavedFilters()
  {
    return Doctrine::getTable('SavedFilter')
      ->createQuery()
      ->where('type = ?', 'Bookmark')
      ->execute()
    ;
  }
}

Ainsi, dans le partial _filters.php, qui a déjà accès à une instance de la classe bookmarkGeneratorConfiguration, nous sommes maintenant capables d'afficher en boucle les filtres sauvegardés que nous avons récupéré afin d'en afficher la liste :


 // in apps/backend/modules/bookmark/templates/_filters.php from line 11
   [...]
   <tr>
     <td colspan="2">
       <h3><?php echo __('Saved filters') ?><h3>
       <?php if (count($savedFilters = $configuration->getSavedFilters())): ?>
       <ul>
       <?php foreach ($savedFilters as $filter): ?>
         <li>
           <a href="<?php echo url_for('@bookmark_filter_load?id='.$filter['id']) ?>">
             <?php echo $filter['name'] ?>
           </a>
           (<a href="<?php echo url_for('@bookmark_filter_delete?id='.$filter['id']) ?>"></a>)
         </li>
       <?php endforeach; ?>
       </ul>
       <?php else: ?>
         <p>No filters saved</p>
       <?php endif; ?>
     </td>
   </tr>
 </tbody>

Maintenant vous êtes donc capable de sauvegarder en base et de lister des filtres, les charger et les faire fonctionner sur votre liste d'objets, et bien entendu, supprimer les filtres existant. Si vous avez suivi méticuleusement les étapes de ce tutoriel, vous devriez voir quelque chose comme cela sous le formulaire de filtres :

Withlist.png

20 minutes, le boulot est fait. Même si ce fût rapide et même un peu trop parfois.

Conclusion

Oui, je peux vous entendre, vous les Symfony nerds : on peut faire vraiment mieux, mieux écrit, mieux inclus dans la structure de Symfony, et probablement abstrait pour offrir une solution générique de stockage de filtres à travers l'ensemble des modules admin-generated... mais en 20 minutes ? Vraiment ? ;c) Mais alors je dirais :

- Allez-y, prouvez le !! :c)

Références

Ceci est une traduction libre de l'article datant de 2009 de NiKo sur son blog Prendre Un Café. Merci à lui pour l'original : http://prendreuncafe.com/blog/post/2009/12/02/Saving-Search-Filters-in-Symfony-s-Doctrine-Admin-Generator

[Catégorie:Informatique]