Defining new data filters

Last updated on
1 November 2021

Data filters are plugins for the Typed Data API Enhancements module that perform operations on Rules tokens. See Token Replacement for more information on where and how to use data filters, and for a list of default data filters made available by the Typed Data module. You may extend the Typed Data module's functionality by providing your own Data filter plugins. The Rules Essentials module defines six new filters, which may be used as examples of how to define your own:

count
Returns the integer length of a string (this is a multibyte-safe filter!) This filter has no arguments.
link
Takes a URL and turns it into markup for an HTML anchor element. This filter has one arguments, which is the link text.
raw
Marks a string token as already sanitized, thus exempting it from HTML-encoding. This filter has no arguments.
replace
Returns a string with some text in that string replaced by alternate text. This filter has two arguments: the text to replace, and the replacement text.
trim
Strips whitespace from the beginning and end of a string variable. This filter has no arguments.
upper
Transforms string value into upper case. This filter has no arguments.

Here, we'll step through the creation of the 'raw' filter. This filter is useful in conjunction with the 'entity_url' filter because, while {{node | entity_url }} will produce a URL for the node, this URL will be HTML-encoded. If we want an actual clickable link, without the HTML-encoding, we may pass this through our new 'raw' filter like this: {{node | entity_url | raw }}

All data filters are TypedDataFilter plugins, therefore they must reside in <modulename>/src/Plugin/TypedDataFilter and have a @DataFilter annotation.

For convenience, we start off by extending \Drupal\typed_data\DataFilterBase

class RawFilter extends DataFilterBase {

The @DataFilter annotation only defines the key "id" and "label". For our 'raw' plugin, this will look like:

/**
 * A data filter which marks string data as sanitized.
 *
 * @DataFilter(
 * id = "raw",
 * label = @Translation("The raw filter prevents HTML-encoding of the input string."),
 * )
 */

Now we need to specify what data types our filter can operate on. We do this by writing a canFilter() method, returning a boolean indicating whether our filter works on the given token data type. The 'raw' filter only operates on data types derived from 'string':

  /**
   * {@inheritdoc}
   */
  public function canFilter(DataDefinitionInterface $definition) {
    return is_subclass_of($definition->getClass(), StringInterface::class);
  }

Next, we write the filtersTo() method, returning a DataDefinition object for the return data type of our filter. The 'raw' filter returns string data:

  /**
   * {@inheritdoc}
   */
  public function filtersTo(DataDefinitionInterface $definition, array $arguments) {
    return DataDefinition::create('string');
  }

Finally, we write the filter() method, which does the actual work of transforming the input data into the filter output. For our 'raw' filter, we pass the input HTML through Xss:filterAdmin() for safety, then wrap the string in a MarkupInterface object which tells Twig not to sanitize the value:

  /**
   * {@inheritdoc}
   */
  public function filter(DataDefinitionInterface $definition, $value, array $arguments, BubbleableMetadata $bubbleable_metadata = NULL) {
    $value = Xss::filterAdmin($value);
    return Markup::create($value);
  }

The complete code for the 'raw' filter may be found at https://git.drupalcode.org/project/tr_rulez/blob/HEAD/src/Plugin/TypedDa...

Test cases

We're not done yet! You should always add PHPUnit test cases, to ensure your data filter works properly and to serve as documentation of how you expect it to work. For our 'raw' filter, our test creates a string containing markup, passes it through the 'raw' filter, then checks to see that the markup has not been removed. We also verify the data types that the filter can filter, and we check the data types that the filter returns. All our filter tests are put into one DataFilterTest class for convenience. This class has separate methods to test each of the data filters provided by our module. The method that tests the 'raw' filter looks like this:

  /**
   * Tests the operation of the 'raw' data filter.
   *
   * @covers \Drupal\tr_rulez\Plugin\TypedDataFilter\RawFilter
   */
  public function testRawFilter() {
    $filter = $this->dataFilterManager->createInstance('raw');
    $data = $this->typedDataManager->create(DataDefinition::create('string'), '<b>Test <em>raw</em> filter</b>');

    $this->assertTrue($filter->canFilter($data->getDataDefinition()));
    $this->assertFalse($filter->canFilter(DataDefinition::create('any')));

    $this->assertEquals('string', $filter->filtersTo($data->getDataDefinition(), [])->getDataType());

    $this->assertEquals('<b>Test <em>raw</em> filter</b>', $filter->filter($data->getDataDefinition(), $data->getValue(), []));
  }

Note that this test is very easy to write, especially when you have an example to follow.

Help improve this page

Page status: No known problems

You can: