Saving Search Filters in Symfony's Doctrine Admin Generator
Par NiKo le mercredi 2 décembre 2009, 11:43 - Dev
- Lien permanent -
4 commentaires -
Tags :
Yesterday I made some consulting in a company where people asked me if it was possible/hard to setup some kind of search filtering persistence in a doctrine-admin-generated module in Symfony. I told them:
Well, it’s a twenty minutes job.
They logically answered me:
Haha, prove it.
Challenge and stress then started.
The examples below are based on the simple model I took for my previous blog post about embedding relations in Doctrine forms, which I will add fancy bookmarks tagging facilities to:
# 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
No need to say you should rebuild your model, right?
Updated fixtures file:
# 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
Generate the Bookmark admin
Let’s generate a backend app and a Bookmark admin module:
$ ./symfony generate:app backend $ ./symfony doctrine:generate-admin backend Bookmark
Now let’s enhance a bit our admin by modifying the generator.yml file:
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: ~
You should be able to browse the generated bookmarks admin interface:

Storing filters in a dedicated Doctrine table, and managing them from the controller
We’ll use Doctrine to store saved filters, so let’s define a new Doctrine table definition in our schema.yml file to store stored filters:
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()
Of course, still no need to say that you have to rebuild your model, right?
Okay, now we’re going to save the serialized filter values in the filter column, the name one will provide a convenient way to reference a filter set. The type column will reference the Doctrine table familly of the filtered object. Nothing difficult here.
In the admin generator module, the filters are storedin the tableName.filters attribute of the user session (where tableName is the name of the admin module where the filter parameters are operated[1]).
So let’s add a new executeSaveFilter() method in the bookmarkActions controller. And while we’re at it, let’s also add executeLoadFilter() and executeDeleteFilter() methods as well:
<?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'); } }
Of course, we’ll need to add the corresponding routes to our routing.yml file:
# 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 }
Wait, we don’t have any link to save a filter from the admin interface! Let’s add one next to the Reset link of the filters column by overriding the _filters.php generated partial template:
// 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> [...]
Notice that a javascript prompt will ask you for a name before saving a filter set:

So we can now save a filter set in our database. Now, what about listing them below the filters form?
Listing existing saved filters
To me, the best suited place to retrieve saved filtered searches is the bookmarkGeneratorConfiguration class, which has been generated in the lib/ subfolder of the bookmark admin module. Let’s add a new getSavedFilters() method to it:
<?php <?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() ; } }
So in the _filters partial template, which has already access to a generator configuration instance, we’re now able to iterate over retrieved saved filter objects to list them:
// 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>
So you can now save and list filters, load and run them against your objects list, and delete existing ones. If you thoroughly followed the steps of this tutorial, you should now see something like this below the filters form:

Twenty minutes, job done. Even if very quick and dirty.
Conclusion
Yes, I can hear you Symfony nerds, this could be heavily refactored, enhanced and maybe abstracted to provided generic filtering storage accross every generated admin module, but in 20 minutes? Really?
Then, I’d say…
Haha, prove it
Notes
[1] I’m not definitely sure of this, but who really cares?

4 commentaires (Ajouter un commentaire)
and 20' after it was done by super Niko, I was there
A nice feature to store filters in admin gen or elsewhere like Redmine.
My next step would be to make a task to send email alert based on stored filters
Olivier
Trop cool !
L'après midi, je réfléchissais à faire un truc du genre et puis je me connecte le soir, et dans mon flux RSS, que vois-je : ce super post.
Mais au fait, en 20 minutes, serait il aussi possible d'intégrer un bouton permettant pour le filtre en cours de sélectionner les champs à afficher via une pop up box afin de personnaliser la liste en fonction du filtre.
Cela donne une idée de blog : 20minutesofsymfony avec chaque jour en 20 minutes, un nouvel exploit !!!
Merci pour le partage de ton travail.
Very nice read n1k0!
Hope I can try this out in one of my projects. But I'm sure I won't do this in only 20 minutes...
- Dennis
I admit to be bluffed by the possibilities of Symfony. It's pretty powerful, I am impressed.
Twitter: http://twitter.com/xethorn
La discussion continue ailleurs
Social comments and analytics for this post
This post was mentioned on Twitter by n1k0: [blog] Saving Search Filters in Symfony's Doctrine Admin Generator http://n1k.li/sfadminfilters // my own advent calendar starting?...