Mugo Web main content.

Extended attribute filters: advanced content retrieval in eZ Publish

By: Ernesto Buenrostro | June 10, 2013 | eZ Publish development tips

Retrieving content is one of the most common use cases when working with a content management system. eZ Publish provides a powerful way to retrieve content with its fetch functions. Although it provides a powerful framework for filtering and sorting the content based on a wide range of criteria, sometimes you need to extend the framework. Adding new filters to fetch functions is achieved by using extended attribute filters.

Some of our use cases for extended attribute filters have included:

  • Filtering of Object Relations attributes
  • Adding sort options based on relation counts
  • Filtering of users based on an enabled/disabled flag
  • Filtering on a common attribute across several content classes
  • An extended attribute filter to run multiple extended attribute filters

Writing an extended attribute filter enables us to add to the SQL query that is passed to the database. What you're essentially doing is adding fields, tables, and conditions on top of what the base eZ Publish queries are working with. In most cases we advise against directly writing database queries in eZ Publish since there is almost always a proper API call for what you need to accomplish! Also, you need at least a basic understanding of the eZ Publish database structure before you start to write an extended attribute filter. (For a deep dive into the inner workings of eZ Publish related to extended attribute filters, look at eZContentObjectTreeNode::subTreeByNodeID() and eZContentObjectTreeNode::createExtendedAttributeFilterSQLStrings().)

In this post I'll walk through two example extended attribute filters.

Multi-class, common attribute filter

If you want to retrieve pages of the Event content class and pages of the Fundraiser content class but run a filter on a common Date field, you need more than a standard attribute filter.

First, we'll create a new PHP file for our extended attribute filter in our extension's "classes" folder:

 <our extension folder>/classes/multiclassattributefilter.php

If you have many extended attribute filters, you might want to create a sub-folder "extended_attribute_filters".

The PHP skeleton to build an extended attribute filter is:

class MultiClassAttributeFilter
{
    /*
     * Constructor for our new class
     */
    function __construct()
    {
        ;
    }

    /*
     * In this method is where we are going to modify different parts of the SQL query being passed to MySQL
     * 
     * $params gets the information passed from the fetch function
     * and we must return an array with those modifications
     * array(
     *     'tables'    => '<TABLES to use>'
     *     , 'joins'   => '<WHERE filtering> '
     *     , 'columns' => '<COLUMNS to return>'
     * )
     */
    static function createSqlParts( $params )
    {     
        ;
    }
}

Within createSqlParts, we'll define the table(s) and fields needed for our filter. Before we write more code, let's implement the basic settings framework so that eZ Publish can use our extended attribute filter.

First, describe the filter in extendedattributefilter.ini.append.php

<?php /* #?ini charset="utf-8"?

[MultiClassAttributeFilter]
ExtensionName=mugo
ClassName=MultiClassAttributeFilter
MethodName=createSqlParts
FileName=classes/multiclassattributefilter.php

*/
?> 

Then, regenerate the autoloads file:

$> php bin/php/ezpgenerateautoloads.php -e -p

 

Back to the filter. All attributes and at least their basic contents are stored in the ezcontentobject_attribute table. Some important elements to build include:

  • Selecting the current version of content
  • Selecting the appropriate field in ezcontentobject_attribute
  • Translating a class identifier and attribute identifier pair into an attribute ID
  • Supporting basic comparison operators

Our new filter looks like:

class MultiClassAttributeFilter
{
    protected static $classCounter;

    function __construct() { ; }
 
    static function createSqlParts( $params )
    {
        $result = array( 'tables' => '', 'joins' => '' );

        if( isset( $params['classes'] ) && isset( $params['attribute'] ) && isset( $params['value'] ) )
        {
            $classIdentifiers = $params['classes'];
            $attributeIdentifier = $params['attribute'];
            $value = $params['value'];
        }
        else
        {
            return $result;
        }
 
        if( isset( $params['comparison'] ) )
        {
            $comparison = $params['comparison'];
        }
        else
        {
            $comparison = '=';
        }
        if( isset( $params['field'] ) )
        {
            $field = $params['field'];
        }
        else
        {
            $field = 'data_text';
        }
 
        self::$classCounter += 1;
        $ezcoaAlias = 'ezcoaA' . (self::$classCounter);
 
        $filterSQL = array();
        $filterSQL['from'] = ", ezcontentobject_attribute AS $ezcoaAlias";
        $attributeIDs = array();
 
        foreach( $classIdentifiers as $classIdentifier )
        {
            array_push( $attributeIDs, eZContentObjectTreeNode::classAttributeIDByIdentifier( $classIdentifier . '/' . $attributeIdentifier ) );
        }
 
        $classCondition = array();
        foreach( $attributeIDs as $attributeID )
        {
            if( $attributeID )
            {
                array_push( $classCondition, "$ezcoaAlias.contentclassattribute_id = " . $attributeID );
            }
        }
 
        $stringClassCondition = implode( ' OR ', $classCondition );
        $filterSQL['where'] = " ( $ezcoaAlias.contentobject_id = ezcontentobject.id AND ("
                              . $stringClassCondition
                              . ") AND $ezcoaAlias.version = ezcontentobject.current_version "
                              . " AND $ezcoaAlias.$field $comparison '$value' ) AND ";   
 
        return array( 'tables' => $filterSQL['from'], 'joins' => $filterSQL['where'] );
    }
}

This extended attribute filter supports parameters to specify the classes and attributes on which to filter, the comparison operator, the exact database field, and the value.

{def $myfetch = fetch('content', 'list', hash(
    'node_id',                     2
    , 'extended_attribute_filter', hash(
        'id', 'MultiClassAttributeFilter'
        , 'params', hash(
            'classes',        array('event', 'fundraiser')
            , 'attribute',    'date_start'
            , 'comparison', '>'
            , 'field',        'data_int'
            , 'value',        currentdate()
        )
    )
))}

Our example fetch function will return Events and Fundraisers with a start date in the future. However, what if we want to retrieve currently running Events and Fundraisers, which have a start date before today and an end date after today? We'll need a way to use our extended attribute filter twice, or we'll need to modify our current extended attribute filter to support multiple sets of parameters. Since we might sometimes need to combine different extended attribute filters, we'll create an extended attribute filter that can run multiple extended attribute filters.

Extended attribute multi-filter

Our additional extended attribute filter simply needs to call other filters and combine their database query string results.

class ExtendedAttributeMultiFilter
{
    function __construct() { ; }

    function createSqlParts( $filters )
    {
        $columns = '';
        $tables  = '';
        $joins   = '';

        $extendedAttributeFilterINI = eZINI::instance( 'extendedattributefilter.ini' );
        foreach( $filters as $filter )
        {
            $className = $extendedAttributeFilterINI->variable( $filter['id'], 'ClassName' );
            $methodName = $extendedAttributeFilterINI->variable( $filter['id'], 'MethodName' );
            $return = call_user_func( array( $className, $methodName ), $filter['params'] );
            if( isset( $return['columns'] ) )
            {
                $columns .= $return['columns'];
            }
            if( isset( $return['tables'] ) )
            {
                $tables .= $return['tables'];
            }
            if( isset( $return['joins'] ) )
            {
                $joins .= $return['joins'];
            }
        }
        return array( 'columns' => $columns, 'tables' => $tables, 'joins' => $joins );
    }

}

Don't forget to add a new settings block to extendedattributefilter.ini.append.php:

<?php /* #?ini charset="utf-8"?

[MultiClassAttributeFilter]
ExtensionName=mugo
ClassName=MultiClassAttributeFilter
MethodName=createSqlParts
FileName=classes/multiclassattributefilter.php

[ExtendedAttributeMultiFilter]
ExtensionName=mugo
ClassName=ExtendedAttributeMultiFilter
MethodName=createSqlParts
FileName=classes/extendedattributemultifilter.php

*/
?> 

... and regenerate the autoloads file.

Now we can make a more complete fetch function:

{def $currents = fetch('content', 'list', hash(
        'node_id',                      2
        , 'extended_attribute_filter', hash(
            'id', 'ExtendedAttributeMultiFilter'
            , 'params', array(
                hash(
                    'id', 'MultiClassAttributeFilter'
                    'params', hash(
                          'classes',    array('event', 'fundraiser')
                        , 'attribute',  'date_start'
                        , 'comparison', '<'
                        , 'field',      'data_int'
                        , 'value',      currentdate()
                    )
                )
                , hash(
                    'id', 'MultiClassAttributeFilter'
                    , 'params', hash(
                          'classes',    array('event', 'fundraiser')
                        , 'attribute',  'date_end'
                        , 'comparison', '>'
                        , 'field',      'data_int'
                        , 'value',      currentdate()
                    )
                )
            )
        )
    )
)}

Advanced filtering alternatives

Depending on your specific case you might also be able to solve your problem with a workflow to pre-calculate some of the advanced filtering information.

Also, if your site uses eZ Find, you can replace a lot of your more complex fetch functions with eZ Find "search" functions, especially ones that require multi-class attribute filtering. This is because eZ Find stores its information in a flat structure while still supporting attribute-level filtering, making it often more powerful AND faster.