Development

HowtoAddCustomFilterCriteria

You must first sign up to be able to contribute.

Version 9 (modified by skr68, 8 years ago)
--

How to Add Custom Filter Criteria to the Admin Generator (Symfony 1.0)

The admin generator provides filters to limit the number of simultaneously displayed items in the admin list view, which is a very helpful feature. You can customize the appearance of these filters by writing a custom filter partial, as described in the Symfony book.

But what if you need to do more than customize the appearance of the filter? What if the filter also requires custom criteria at the database level? Out of the box, the admin generator allows you to filter only on fields of the table you are administering. It doesn't handle filters that are based on joins with other tables.

Fortunately, there's a way to add such customized criteria when extending the admin generator for your application. The tricky part is recognizing that you must convince the admin generator not to generate unhelpful criteria of its own that don't work.

Let's assume that we are setting up the admin generator to manage Releases, and that releases are associated with categories through a separate Propel class and table called ReleaseCategory in which each row contains a release ID and a category ID. To implement the filter we need to set up criteria based on the existence of ReleaseCategory for each Category.

generate.yml configuration

We'll begin by adding our custom filter to the filters: list in generate.yml in the usual way:

filters: [name, _categories]

Here "name" is an ordinary, not-at-all-custom filter on the name of the release. I'm presenting it to remind us that we must not break normal filters in the process of adding our own.

As you'll know if you have read the admin generator chapter of the Symfony book, to set up "custom" filters that have a unique presentation but ordinary criteria, all you have to do at that point is write a templates/_categories.php partial that presents the filter's UI in the desired way, which I'll present later.

But when the criteria are also unique, things start to get interesting.

Extending an admin generator class: part 1

You can add custom criteria for your "nontrivial" filter by overriding addFiltersCriteria in your subclass of the admin generator class. If you are using the admin generator to manage release objects in your backend app, then you'll add the necessary method to apps/backend/modules/releases/actions/actions.php. That file looks like this to start with:

class releasesActions extends autoreleasesActions
{
  // Initially empty so everything behaves as per normal in the admin generator
}

Adding your custom criteria isn't hard to do. Your extended version of addFiltersCriteria does it in the usual Propel way. For this example let's assume we have a separate ReleaseCategories? class, living in its own table. That means we can't just use the default criteria to implement the filter and we need to provide our own.

Writing the custom filter partial

Let's assume that you are outputting a set of checkboxes in your _categories.php partial to select different release categories to be visible in the filtered results. Keep in mind that in your custom filter partials you can take advantage of the $filters array to retrieve the user's existing filter settings, something the admin generator gives you for free:

<?php
  $categories = CategoryPeer::doSelect(new Criteria());
  $fcategories = isset($filters['categories']) ? $filters['categories'] : array();
?>
<span class="categories_filter">
<?php foreach ($categories as $category): ?>
  <div class="categories_filter_group">
  <?php
    echo checkbox_tag('filters[categories][]',
      $category->getId(), in_array($category->getId(), $fcategories));
  ?>
    <span class="categories_filter_group_label">
    <?php echo $category->getName(); ?>
    </span>
  </div>
<?php endforeach ?>
</span>

Notice that we are making heavy use of PHP's support for arrays of form elements. Naming each checkbox filters[categories][] allows PHP and the admin generator to set up a $filterscategories? array containing the IDs of all of the selected categories with no further effort on our part. This makes it easy to output the current filter settings, and easy to implement the criteria as well.

Extending an admin generator class: part 2

Now that we know what our custom filter settings look like in the template, we're ready to set up the custom criteria in addFiltersCriteria:

  public function addFiltersCriteria($c)
  {
    if (isset($this->filters['categories']))
    {
      $categories = $this->filters['categories'];
      if (count($categories))
      {
        $c->addJoin(ReleasePeer::ID, ReleaseCategoryPeer::RELEASE_ID);
        $orClause = false;
        foreach ($categories as $category)
        {
          $crit = $c->getNewCriterion(ReleaseCategoryPeer::CATEGORY_ID, $category);
          if (!$orClause) {
            $orClause = $crit;
          } else {
            $orClause->addOr($crit);
          }
        }
        $c->add($orClause);
      }
    }
    // But we're NOT done yet! We still have to deal with
    // calling the parent class version here... without
    // generating broken criteria for categories. Read on
  } 

Here I've used addJoin to join the Release table to the ReleaseCategory table, and I've set up criteria that select all releases that are in any of the selected categories. If no categories at all are selected, I don't add any special criteria at all. This means that when no checkboxes are selected, all of the releases appear (assuming no other filters are in effect). This is the standard default behavior for an admin generator filter.

Extending an admin generator class, part 3

So far, so good! But we're not quite done. We still have to call the parent class version of addFiltersCriteria. Otherwise the "name" filter, and any other filters that we're not providing custom won't be implemented.

And here's the big catch: that parent class code will try to set up criteria for a nonexistent field in Release called categories. That's its job, after all. This results in PHP code that fails immediately on execution.

How can we prevent that problem? By saving, deleting and then restoring the appropriate fields in the filters array, like this:

    if (isset($this->filters['categories_is_empty']))
    {
      $categoriesIsEmpty = $this->filters['categories_is_empty'];
      unset($this->filters['categories_is_empty']);
    }
    if (isset($this->filters['categories']))
    {
      $categories = $this->filters['categories'];
      unset($this->filters['categories']);
    }

    // Call the base class implementation to get the other filters
    $result = parent::addFiltersCriteria($c);

    // Restore the categories filter
    if (isset($categoriesIsEmpty))
    {
      $this->filters['categories_is_empty'] = $categoriesIsEmpty;
    }
    if (isset($categories))
    {
      $this->filters['categories'] = $categories;
    }

Here we save and then unset both the categories field and the categories_is_empty field. The admin generator code uses both of these fields to determine the criteria for a filter, so we hide both of them to be sure the generated code is not triggered.

Add this code to the end of addFiltersCriteria() and you're off to the races.

Just in case: making sure the admin generator code is 100% valid PHP

At this point we have a working solution. However, even though we are preventing the actual execution of the admin-generated code in the base class version of addFiltersCriteria that refers to a nonexistent ReleasePeer::CATEGORIES constant when trying to set up "normal" criteria for it, that code is still present in the admin-generated base class. And although current versions of PHP (well, PHP 5.2.x) don't care, someday a pickier version of PHP might refuse to compile it, even though it never actually executes.

If that bothers you (yes, it does bother me), just add that constant to your lib/model/ReleasePeer.class.php file like so:

class ReleasePeer extends BaseReleasePeer
{
  // Make sure a pickier future release of PHP doesn't refuse to
  // compile the admin generated filter code
  const CATEGORIES = 'dummy';
}

Congratulations, your custom admin generator criteria are now fully future-proof.

A working example

This article is based on the techniques I employed to add a "filter by group" feature to the sfGuardUser module of sfGuardPlugin. That code has been accepted and is part of the current releases of sfGuardPlugin for all Symfony versions. You'll see it in action on the right-hand side of the page when managing users.

Proposal for the Future

Today, you'll have to use the techniques above. In the future, I suggest that the admin generator code support the following additional features to make this process painless.

In generate.yml, a custom_criteria_filters option should be added:

# Proposal for future versions of Symfony, see earlier in this
# article for the method that actually works!
filters: [username, _categories]
custom_criteria_filters: [_categories]

All of the filters should still appear on the filters: list, because it controls the order of presentation. But the improved, future version of addFiltersCriteria should not contain auto-generated criteria for filters that are on the custom_criteria_filters list. This way, subclasses extending addFiltersCriteria() don't have to hide and restore stuff in $this->filters[], which is ugly. And there are no references to nonexistent class constants floating around, either.

To make things even easier, addFiltersCriteria() would call addCustomFiltersCriteria, the default (parent class) version of which would be empty. So programmers could simply override addCustomFiltersCriteria() and not worry about calling the parent class version at all.

-Tom Boutell, P'unk Avenue (tom@punkave.com)


Hello, Why don't you use this :

    list:
      fields: 
        firstname: { filter_criteria_disabled: true }
        lastname: { filter_criteria_disabled: true, filter_is_empty: true  }
      title:   User list
      display: [ =username, _name, created_at, last_login ]
      filters: [ username, _firstname, _lastname ]

[MA]Pascal