Jump to content

Filter system for extensible data modification


gyo
 Share

Recommended Posts

Hello everyone,

After playing a week with ProcessWire I'm really loving it. As a first project I wanted to create a Profile module based on the existing FrontendUserProfile trying to make it suit my needs, and I found the need for a filter system like the one WordPress has.

I couldn't find anything similar or maybe I didn't completely grasp the Hooks concept, so I've created a simple ProcessWire module to handle that:

Filter

https://github.com/gyopiazza/pw.filter

The module is autoloaded, and once installed you can use the 2 functions provided to filter data. This feature is very useful in certain situations where you want to be able to change some values in a simple way.

function my_function1($value)
{
	return 'filtered by my_function1';
}

function my_function2($value)
{
	return 'filtered by my_function2';
}

// Parameters: filter name, function name or array(class, method), priority
// Higher priority means later execution, default is 10
addFilter('some_filter_name', 'my_function1', 10);
addFilter('some_filter_name', 'my_function2', 10);

$result = 'default value';
$result = applyFilter('some_filter_name', $result);

// $result = 'filtered by my_function2'

Hope you like it, cheers! :)

Giordano

  • Like 2
Link to comment
Share on other sites

Hello everyone,

After playing a week with ProcessWire I'm really loving it. As a first project I wanted to create a Profile module based on the existing FrontendUserProfile trying to make it suit my needs, and I found the need for a filter system like the one WordPress has.

I couldn't find anything similar or maybe I didn't completely grasp the Hooks concept, so I've created a simple ProcessWire module to handle that:

Hello and welcome!

The Hooks are meant to extend (or even replace) the functionality of ProcessWire and it's modules. They are a general concept and not something that provide features on their own. Instead you attach to them, to implement the feature you are needing.

In your case, one example for their usage could be hooking into Fieldtype::formatValue and automatically filtering some specific fields when they are output in a template. I don't know how you are using the filters, so this might not be very useful. Here's a quick example anyway

// Filter.module

public function init()
{
  $this->addHookAfter("FieldType::formatValue", function(HookEvent $e) {
    $page = $e->arguments(0);
    $field = $e->arguments(1);
    $value = $e->arguments(2);

    // Execute for my_field only
    if($field->name != "my_field")
      return;

    $e->return = applyFilter("my_field", $value);
  });
}

// Demonstration in CLI

require 'index.php';

addFilter('my_field', function($value) { return strtoupper($value); });
addFilter('my_field', function($value) { return md5($value); });

// If you are inside a template this isn't needed
$page = $pages->get('/some-example-page');
$page->setOutputFormatting(true); 

echo $page->my_field; // Would print a MD5-hash of the uppercase version of the original value

It would be quite trivial to extend this so that you could do

$field = $fields->get('my_field');
$field->addFilter(function($value) { ... });
$field->addFilter(function($value) { ... });
  • Like 2
Link to comment
Share on other sites

Hi sforsman, actually that's the very reason I made the plugin for: fields. :)

It's useful to format the fields values, because you don't always need the same exact value coming from the database.

For instance the Select fields: since the keys get stored, you could retrieve their corresponding labels by using a filter.

Will FieldType::formatValue be attached to any field? That gave me an idea...

public function init()
{
    $this->addHookAfter("FieldType::formatValue", function(HookEvent $e)
    {
        $page = $e->arguments(0);
        $field = $e->arguments(1);
        $value = $e->arguments(2);
        
        // Apply filter for any field
        $value = applyFilter('formatValue', $value);
        // Apply filter for a specific field type
        $value = applyFilter('formatValue_'.$field->type, $value); // Example: formatValue_FieldtypeText
        // Apply filter for a specific field name
        $value = applyFilter('formatValue_'.$field->name, $value); // Example: formatValue_myfield
        
        // Finally return the value
        $e->return = $value;
    });
}

I'm just thinking if that's flexible enough. Surely you can remove the hook at runtime if it's not needed or there could be 2 more methods in the plugin to remove/get the filters. So – to disable the filters temporarily – you could: store the current filters, remove them, do whatever needs to go "unfiltered", and add them back if necessary.

If you think the Filter plugin is useful I'll definitely expand it, and thanks for the feedback!

Link to comment
Share on other sites

Will FieldType::formatValue be attached to any field?

Yes it will be - it's a method provided by the class all Fieldtypes are extending from. This method will be called when you echo the field inside a template, or to be more precise, when output formatting has been turned on.

There is also a hook for FieldType::wakeupValue, which is called when the value for the field is retrieved from the database and attached to the Page object. In other words, if you want the filters to run regardless of the state of output formatting, that's where you want to hook. Or you could even hook both.

I'm just thinking if that's flexible enough

Since your module is an autoloading singular module, there's a very easy way to provide a flexible way to disable/enable the filters temporarily - globally or per field. Here's an example

// Filter.module

protected static $disabled = false;
protected static $disabled_fields;

// Turn the whole filtering system off
public static function setFiltersOff($disable = true)
{
  self::disabled = $disable;
}

// Turn filters off for a specific field
public static function setFieldFiltersOff($field, $disable = true)
{
  // If you pass a string instead of a Field-object, it has to be the name of the field
  if(is_string($field))
    $field = wire('fields')->get($field);

  if(!($field instanceof Field))
    throw new Exception("Invalid field");

  // Initialize
  if(!isset(self::disabled_fields))
    self::disabled_fields = new SplObjectStorage();

  if($disable)
    self::disabled_fields->attach($field);
  else
    self::disabled_fields->detach($field);
}

// Modify the init() to support these features
public function init()
{
  $class = __CLASS__;

  $this->addHookAfter("FieldType::formatValue", function(HookEvent $e) use ($class) {
    if($class::disabled)
      return;

    $page = $e->arguments(0);
    $field = $e->arguments(1);
    $value = $e->arguments(2);

    if(isset($class::disabled_fields) and $class::disabled_fields->contains($field))
       return;

    // The rest is business as usual...

  });

  // Just a bonus hook for another shortcut
  $this->addHook("Field::setFiltersOff", function(HookEvent $e) use ($class) {
    $field = $e->object;
    $disable = ($e->arguments(0) !== null) ? $e->arguments(0) : true;
    $class::setFieldFiltersOff($field, $disable);
  });
}

// Now you can use these like this

// Disable all filters
wire('modules')->get('Filter')->setFiltersOff();

// Turn them back on
wire('modules')->get('Filter')->setFiltersOff(false);

// Disable a specific field
wire('fields')->get('my_field')->setFiltersOff();

// Turn it back on 
wire('fields')->get('my_field')->setFiltersOff(false);

Edit: Added examples

You could obviously implement this in other ways too.

Edited by sforsman
  • Like 1
Link to comment
Share on other sites

I'm not sure I really understand it enough, is it just to format values? But isn't that what TextFormatter modules are in PW. There can be multiple and sorted, attached to a per field basis. If there's any it will run the value through the Textformatters. 

Fieldtype::formatValue is used to sent through a value of a field after wakeup, but only when outputformatting is on ($page->of(true), you can also disable it or get the unformatted value). Some fields use that to format a value like Datetime or Page. It can also be hooked via modules or in templates. 

To get a unformatted value you can use $page->getUnformatted("fieldname");

A hook in a module example

$this->addHookAfter("Fieldtype::formatValue", $this, function(HookEvent $event){
     $value = $event->return;
     $value = $event->arguments("value");
     $page = $event->arguments("page");
     $field = $event->arguments("field");
     $event->return = "new value";
});

About your module, I still don't understand the real world use case for this or and if it's really best way to go for it. Anyway I think it's a little problematic (?) to have the helper functions outside the module in the global namespace (no namespace yet in PW) and if it's good to name a module  just "Filter" ?

Edit: I'm maybe also confused by the word "filter" in context to PW, cause there's a $wirearray->filter(selector); but it's not about the same thing?

  • Like 1
Link to comment
Share on other sites

Edit: I'm maybe also confused by the word "filter" in context to PW, cause there's a $wirearray->filter(selector); but it's not about the same thing?

I agree, but the term and the concept is originating from his WP -background - so each to their own, I guess :)

Link to comment
Share on other sites

I'm not sure I really understand it enough, is it just to format values? But isn't that what TextFormatter modules are in PW. 

As you call it, they are TEXTformatters. The given example with the select field, which has a label and a key, is more than text. I could also image using this, if you use more than one date format throughout your layout and you would like the ability to quickly customize each of the formats. 

Link to comment
Share on other sites

@Soma

The idea is not to just format values but to modify (filter) a variable, whatever that might be. I understand the confusion, because the hook in the init method we were discussing actually restricts the plugin usage to fields. Just brainstorming. :)

The real difference between using the Filter module and standard hooks is that the first was meant to run "on demand" (only when the function applyFilter() is executed), while the second is attached to any possible field that is output, even if you don't need it. About the naming convention, yeah, the word "filter" is some heritage from the WP concepts, but for the modules names, whenever is possible and if they don't collide, I personally prefer them short and single-worded. And yeah2 those helper functions might pollute the global namespace, they're probably not necessary as one could just call wire('Filter')->add(...);

Anyways, I believe that to use hooks would be fine, just a little more verbose but they would just work:

$this->addHookAfter("Fieldtype::formatValue", $this, function(HookEvent $event){
    $field = $event->arguments("field");

    if ($field->name = 'myfield') {
        $value = $event->return;
        $value = $event->arguments("value");
        $page = $event->arguments("page");
        $event->return = "new value";
    } 
});

With Filter it would be:

wire('Filter')->add('formatValue_myfield', function($value){
    return 'new value';
});

But it only makes sense if somewhere else that value is filtered (like the init method of the Filter module, or inside template files etc...):

$value = wire('Filter')->apply('formatValue_myfield', $value);

I think I can go with the standard hooks even if it takes more code because I prefer to use core features, my only concern is: can we add many hooks without excessive overhead, for example due to the huge size of the $event object?

Thanks for the feedback!

Link to comment
Share on other sites

I think I can go with the standard hooks even if it takes more code because I prefer to use core features, my only concern is: can we add many hooks without excessive overhead, for example due to the huge size of the $event object?

Hey gyo! I'm a bit busy at the moment so I'll just quickly comment on this part only. You have absolutely nothing to worry about on the size of the $event-object - it is a tiny little object that will basically just hold references to other objects (that already exist in the memory anyway). This does not increase the size of the overall memory usage (in a way that really matters, at least).

It sure is true that every single function call is theoretically "overhead", but we are talking about a very, very small overhead (in the context of hooks). The only thing you need to make sure of (to reduce the overhead), is that if you hook into something that's used a lot and your hook isn't always applicable, you need to return from the hook asap (i.e. check the conditions for execution in the first possible moment).

If it makes you feel better, there's about a hundred hooks added by the PW "core" modules themselves ;)

On a side note, I think it's very cool that ProcessWire uses it's own core architecture to provide many of the "core" services we are used to (e.g. the admin-site).

PS. I have put the word "core" inside quotes because the actual PW core is pretty lightweight. Much of the "core" that ships with the installation is actually just normal modules that hook in to the system.

Edited by sforsman
  • Like 1
Link to comment
Share on other sites

Hey sforsman! :)

Of course it's a nice thing that PW core uses it's own architecture to provide functionalities, and the whole "modular" idea it's really a big selling point. The modular admin is just awesome, it's like anything is possible. Which I love.

I'll have to dive more into hooks and get better used to it, because that's where the extensibility power really lies.

In any case thanks everybody for the precious help  ^-^

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...