Skip to main content
Menu

Get Wild With Facets – Extending Facet API

Daniel Nolde 2012-01-17

Facet API is a great tool for building facetted search solutions working on several search modules for Drupal. We’d like to share with you how you can extend it with custom plugins, how we did exactly that and introduced the brand new Facet API Bonus module as a place for all those small and useful plugins that may be out there …

Facet API

Facets are an important aspect of search solutions today, used to explore and drill down a search result set in multiple category dimensions at once. The Facet API module for Drupal 7 is a highly extensible framework for supporting, configuring and using facets with several Drupal search modules. We use Facet API with the Search API framework, and the combination has proven very flexible and handy.


(Example of end user interface to Facet API facets on a Search API search)

 

Facet API lets you customize theme functions for facets and their items, you can use different widgets for your facets (there is a slider widget, a tagcloud widget, and even a chart widget). And you can configure sorting (order of displayed items), filters (e.g. exclusion or alteration of items) and dependencies (enable/disable facets based on specific conditions) – all through a nice and clean UI:

(Example of Facet API configuration options via the administrative interface)

Facet API internally provides all this via a plugin system that you can use and extend yourself. That’s the charm: You can extend those existing plugins or come up with totally new solutions for widgets, sorts, filters or dependencies.

(We henceforth assume you’re familiar with setting up search facets using Facet API – see Yuri Gerasimov’s excellent Trellon blog article on setting up and customizing Facet API configuration with Search API and Apache Solr, for example)

How to extend Facet API

Writing extensions for Facet API involves their definition via special hooks, which you can implement, and the plugin’s actual implementation code which lives in classes.

Part 1: The Hooks

First, you make your extensions known to Facet API via a number of hooks:

  • Sort plugins: hook_facetapi_sort_info()
    Provide systematics for ordering facet items.
  • Dependency plugins: hook_facetapi_dependencies()
    Provide configurable conditions on which your facets can be activated/deactivated.
  • Empty behavior plugins: hook_facetapi_empty_behaviors()
    What to do or show when a facet has no items.
  • Filter plugins: hook_facetapi_filters()
    Alter the facet items before rendering.
  • Widget plugins: hook_facetapi_widgets()
    Define how facets and their items are visually represented (Links, Slider, Tagcloud...)

Beside these, you could define more advanced stuff, like custom facet definitions, searchers (i.e. facet environments), realms (groups of similar facets rendering, e.g. "block" or "fieldset"), adapters etc. (which all go beyond this article’s scope). For details see facetapi.api.php in the Facet API’s module directory.

If you intend, for example, to implement filter plugins, you simply implement a hook_facetapi_filter in your module and return a simple array structure, like this:



function facetapi_bonus_facetapi_filters() {
return array(
'exclude_items' => array(
'handler' => array(
'label' => t('Exclude specified items'),
'class' => 'FacetapiFilterExcludeItems',
),
),
'rewrite_items' => array(
'handler' => array(
'label' => t('Rewrite facet items via callback function'),
'class' => 'FacetapiFilterRewriteItems',
),
),
);
}

The module that houses these hook implementations can be very lean and only consist of the hook implementations themselves. The “magic” happens in files containing the classes you define in the hook’s return data.

Part 2: The Class(es)

The actual code and implementation of  Facet API plugins is done in PHP classes. Your plugin classes can exist in any file, just make sure you add it to your module’s .info file via the files[] directive (see e.g. Facet API Bonus). The class implementation looks something like this:



class FacetapiFilterExcludeItems extends FacetapiFilter {
// ...
}

You choose the class’s name (which you state in the data returned by your implementation of the hook mentioned above), and use one of the base classes provided by Facet API to use as a base class: FacetapiFilter for filter plugins, FacetapiDependency for dependency plugins, and so on.

Your plugin class typically consist of at least three methods:



public function execute() {
// Does the actual work ...
}

public function settingsForm(&$form, &$form_state) {
// Build form elements for settings dialog UI …
$form = array( /* ... */ );
return $form;
}

public function getDefaultSettings() {
// Declare _all_ possible settings and their defaults.
return array('settings_name' => 'default_value', /* ... */);
}

These are pretty common in most Facet API plugins. However, the different plugin types have different base classes, and different additional methods. Have a look at the .inc class files in the Facet API’s plugin directory at facetapi/plugins/facetapi to get a comprehensive idea of all possibilities.

There are some important differences and details to note:

  • The structure of the $settings property is a bit different from plugin type to plugin type. While for example a dependency plugin stores its settings data in $this->settings[‘name_of_setting’], a filter plugin uses the structure $this->settings->settings[‘name_of_setting’].
  • The same kind of inconsistency goes for the declaration of form UI elements in the “settingsForm” methods: $form['name_of_setting'] in filter plugins vs. $form[$this->id]['name_of_setting'] in dependency plugins, for example.
  • If you need to have a dynamic number of settings for your plugin, you have to make getDefaultSettings() method dynamic (see the Facet API Bonus’ class “FacetapiDependencyFacet”, e.g.).
  • The signature of the “execute” method varies from type to type.
  • For some types, the “execute” method alters data, for some it returns a value or data.
  • The dependency system uses a three-state logic for influencing the activation/deactivation of a facet by a cascade of user-configured dependency plugins. You can return:

    NULL, meaning “ leave activation/deactivation of the facet open to other configured dependencies
    => Return this if your dependency plugin concludes “according to this dependency, the facet should can be activated”

    FALSE, meaning “force facet’s deactivation” (dependency not met)
    => Return this if you’d like to conclude “according to this dependency, deactivate the facet in any case”

    TRUE, meaning “force the facet’s activation”

    A dependency plugin is discouraged from using TRUE – in most cases, the NULL return value will be the better alternative to TRUE.
     

  • The base classes for plugins provide some (varying) important contextual information you can tap, and there are some good Facet API tools to get more contextual information, among them:

    $this->facet
    Get information about the facet this plugin is configured for and working on

    $this->activeItems
    Get information about active facets/items

    $this->adapter
    Get information about the facet’s adapter (searcher the facet is working on etc.)

    facetapi_get_enabled_facets($this->adapter->getSearcher());
    Get information about the facets enabled/configured to the current context your plugin in working in (adapter/searcher)

How to put all that together? The easiest way is to look at existing plugins, either the basic filters, dependencies etc. of the Facet API module itself, or the additional plugins provided by the Facet API Bonus module.

The Facet API Bonus module

In our work, we needed a number of additional filter and dependency plugins for some generic use cases that are reusable. Now we want to share them with you. Since Chris Pliakas, Facet API’s maintainer, tries to keep Facet API module itself as lean as possible, we used the quiet time of the Christmas holidays to package that little collection of plugins as the Facet API Bonus module for you:

  • Facet Dependency: Dependency plugin to make one facet (say “Hybrid drive technology”) to show up depending on other facets or specific facet items being active (say if taxonomy facet “engine type” is set to “Hybrid engine”). Very flexible, supports multiple facets to be dependencies, as well as regexp for specifying facet item dependencies, and an option how to behave if a dependency is being lost.
  • Exclude Items: Filter plugin to exclude certain facet items by their markup/title or internal value (say excluding "page" from "content types"). Regexp are also possible.
  • Rewrite Items: Filter plugin to rewrite labels or other data of the facet items by implementing a new dedicated hook_facet_items_alter (in a structured array, before rendering). Very handy to rewrite list field values or totally custom encoded facet values for user friendly output.

Facet API Bonus is – much like the former Views Bonus module – primarily intended as a place to collect any number of handy little facet filter, sort, widget and dependency plugins, that others might create, which would be too small to be a module themselves and need a place to exist cosily in the contrib space (The Facet API Extra module, which contains a single simple dependency plugin at the moment, was created two days earlier seems to aim for the same goal, so their maintainers will talk about consolidating).

Get involved!

Chime in with your own Facet API plugins, get on board and let other’s share your custom plugins – all with the least possible amount of work for you (module and project already exist ;).

Just post a patch in the Facet API Bonus issue queue: http://drupal.org/project/issues/facetapi_bonus

Or just try the Facet API Bonus module yourself, at http://drupal.org/project/facetapi_bonus.

And in any case state your feedback, wishes or suggestions – what do you want to see or need in Facet API Bonus?