Skip to content

Commit

Permalink
Merge pull request #208 from gsteel/v3/immutable-filter-chain
Browse files Browse the repository at this point in the history
Refactor `FilterChain`
  • Loading branch information
gsteel authored Dec 17, 2024
2 parents 0a44105 + eb02bec commit 8f75078
Show file tree
Hide file tree
Showing 13 changed files with 681 additions and 336 deletions.
194 changes: 128 additions & 66 deletions docs/book/v3/filter-chains.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,155 @@
# Filter Chains

> MISSING: **Installation Requirements**
> The following examples use the [`Alpha` filter from the `laminas/laminas-i18n`](https://docs.laminas.dev/laminas-i18n/filters/alpha/).
> Make sure to install the required package before running the examples.
>
> ```bash
> $ composer require laminas/laminas-i18n
> ```
Often, multiple filters should be applied to some value in a particular order.
For example, a login form accepts a username that should be lowercase and
contain only alphabetic characters.
`Laminas\Filter\FilterChain` provides a simple method by which filters may be
chained together. The following code illustrates how to chain together two
filters for the submitted username and fulfill the above requirements:
Filter chains provide a way of grouping multiple filters together.
They operate on the input by fulfilling the `FilterInterface` contract in the same way as all the other filters.

You can enqueue as many filters as you like via a straight-forward api, attaching either ready-to-use filter instances, arbitrary callables or by using a declarative array format.

## Getting a Filter Chain Instance

The filter chain has a constructor dependency on the `FilterPluginManager`, so whilst you can manually create a filter chain from scratch, it is normally easier to retrieve one from the plugin manager

```php
// Create a filter chain and add filters to the chain
$filterChain = new Laminas\Filter\FilterChain();
$filterChain
->attach(new Laminas\I18n\Filter\Alpha())
->attach(new Laminas\Filter\StringToLower());
assert($container instanceof Psr\Container\ContainerInterface);
$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);

// Filter the username
$username = $filterChain->filter($_POST['username']);
```
// Create a Filter Chain instance manually:
$chain = new Laminas\Filter\FilterChain($pluginManager);

Filters are run in the order they are added to the filter chain. In the above
example, the username is first removed of any non-alphabetic characters, and
then any uppercase characters are converted to lowercase.
// Fetch an empty filter chain from the plugin manager
$chain = $pluginManager->get(Laminas\Filter\FilterChain::class);

Any object that implements `Laminas\Filter\FilterInterface` may be used in a
filter chain.
// Build a ready-to-use filter chain via the plugin manager
$chain = $pluginManager->build(Laminas\Filter\FilterChain::class, [
// ...(Filter Chain Configuration))
]);
```

## Setting Filter Chain Order
## Adding Filters to the Chain

For each filter added to the `FilterChain`, you can set a priority to define
the chain order. Higher values indicate higher priority (execute first), while
lower and/or negative values indicate lower priority (execute last). The default value is `1000`.
The following examples provide 3 ways of building a chain that performs the following:

In the following example, any uppercase characters are converted to lowercase
before any non-alphabetic characters are removed.
- Trim the input string using `Laminas\Filter\StringTrim`
- Make the string lowercase using `Laminas\Filter\StringToLower`
- Reverse the string using a closure

### 1. Attaching Filter Instances to the Chain

```php
// Create a filter chain and add filters to the chain
$filterChain = new Laminas\Filter\FilterChain();
$filterChain
->attach(new Laminas\I18n\Filter\Alpha())
->attach(new Laminas\Filter\StringToLower(), 500);
use Laminas\Filter\FilterChain;
use Laminas\Filter\StringToLower;
use Laminas\Filter\StringTrim;

$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);

$chain = $pluginManager->get(FilterChain::class);

$chain->attach(new StringTrim()); // directly instantiate the filter
$chain->attach($pluginManager->get(StringToLower::class));
$chain->attach(static fn (string $value): string => strrev($value));

print $chain->filter(' OOF '); // 'foo'
```

## Using the Plugin Manager
### 2. Building the Chain with Array Configuration

A `FilterPluginManager` is attached to every `FilterChain` instance. Every filter
that is used in a `FilterChain` must be known to the `FilterPluginManager`.
```php
use Laminas\Filter\FilterChain;
use Laminas\Filter\StringToLower;
use Laminas\Filter\StringTrim;

$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
$chain = $pluginManager->build(FilterChain::class, [
'filters' => [
['name' => StringTrim::class],
['name' => StringToLower::class],
],
'callbacks' => [
['callback' => static fn (string $value): string => strrev($value)],
],
]);

print $chain->filter(' OOF '); // 'foo'
```

To add a filter to the `FilterChain`, use the `attachByName()` method. The
first parameter is the name of the filter within the `FilterPluginManager`. The
second parameter takes any options for creating the filter instance. The third
parameter is the priority.
### 3. Adding Filters to the Chain by Name

```php
// Create a filter chain and add filters to the chain
$filterChain = new Laminas\Filter\FilterChain();
$filterChain
->attachByName('alpha')
->attachByName('stringtolower', ['encoding' => 'utf-8'], 500);
use Laminas\Filter\FilterChain;
use Laminas\Filter\StringToLower;
use Laminas\Filter\StringTrim;

$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);

$chain = $pluginManager->get(FilterChain::class);

// `attachByName` retrieves the filter from the composed plugin manager:
$chain->attachByName(StringTrim::class);
$chain->attachByName(StringToLower::class);
// We must still use `attach` to add the closure:
$chain->attach(static fn (string $value): string => strrev($value));

print $chain->filter(' OOF '); // 'foo'
```

The following example shows how to add a custom filter to the `FilterPluginManager` and the
`FilterChain`:
By default, filters execute in the order they are added to the filter chain.
In the above examples, the input is trimmed first, then converted to lowercase and finally reversed.

## Types of Attachable Filters

Any object that implements `Laminas\Filter\FilterInterface` may be used in a filter chain.
Additionally, you can provide any type of `callable`, however it should match the signature `fn (mixed $value): mixed`. You are free to narrow the types from mixed, but given the simple contract of `FilterInterface` it is often better practice to [write a custom filter](writing-filters.md) and register it with the plugin manager.

## Setting Filter Chain Order

For each filter added to the `FilterChain`, you can set a priority to define the chain order.
Higher values indicate higher priority (execute first), while lower and/or negative values indicate lower priority (execute last).
The default priority is `1000`.

In the following example, an uppercase prefix is applied after the input has been converted to lower case, even though the prefix filter is added to the chain first:

```php
$filterChain = new Laminas\Filter\FilterChain();
$filterChain
->getPluginManager()
->setInvokableClass('myNewFilter', 'MyCustom\Filter\MyNewFilter');
$filterChain
->attachByName('alpha')
->attachByName('myNewFilter');
// Create a filter chain and add filters to the chain
$filterChain = $pluginManager->get(Laminas\Filter\FilterChain::class);
$filterChain->attach(new Laminas\Filter\StringPrefix(['prefix' => 'FOO: ']));
$filterChain->attach(new Laminas\Filter\StringToLower(), 500);

print $filterChain->filter('BAR'); // 'FOO: bar'
```

You can also add your own `FilterPluginManager` implementation:
## Array Configuration

As previously noted, you can define filter chains using a configuration array.
The exact specification of this array is as follows:

```php
$filterChain = new Laminas\Filter\FilterChain();
$filterChain->setPluginManager(new MyFilterPluginManager());
$filterChain
->attach(new Laminas\I18n\Filter\Alpha())
->attach(new MyCustom\Filter\MyNewFilter());
$filterChainConfig = [
'filters' => [
[
'name' => SomeFilter::class, // Required. Must be an alias or a FQCN registered in the plugin manager
'options' => [ /* ... */ ], // Optional. Provide options specific to the required filter
'priority' => 500, // Optional. Set the execution priority of the filter (Default 1000)
],
],
'callbacks' => [
[
'callback' => static fn (string $in): string => strrev($in), // Required. Any type of PHP callable
'priority' => 500, // Optional priority, default 1000
],
[
'callback' => new Laminas\Filter\StringToLower(), // Any object implementing FilterInterface
],
],
];
```

NOTE: **Callbacks are Registered First**
It's important to note that internally, `callbacks` are registered _first_.
This means that if you do not specify priorities when using the array configuration format, the filter execution order may not be what you want.

## Using the Plugin Manager

As with other plugin managers in the laminas ecosystem, you can retrieve filters either by fully qualified class name, or by any configured alias of that class.
For example, the `Laminas\Filter\StringToLower` filter is aliased to `stringToLower`, therefore, calling `$pluginManager->get('stringToLower')` will yield an instance of this filter.

When using filter chain, you must remember to [register custom filters](writing-filters.md#registering-custom-filters-with-the-plugin-manager) with the plugin manager correctly if you wish to reference your filters by FQCN or alias.
40 changes: 28 additions & 12 deletions docs/book/v3/writing-filters.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Writing Filters

`Laminas\Filter` supplies a set of commonly needed filters, but developers will
often need to write custom filters for their particular use cases. You can do
so by writing classes that implement `Laminas\Filter\FilterInterface`, which
defines a single method, `filter()`.
often need to write custom filters for their particular use cases.
You can do so by writing classes that implement `Laminas\Filter\FilterInterface`, which defines two methods, `filter()` and `__invoke()`.

## Example

Expand All @@ -14,28 +13,45 @@ use Laminas\Filter\FilterInterface;

class MyFilter implements FilterInterface
{
public function filter($value)
public function filter(mixed $value): mixed
{
// perform some transformation upon $value to arrive on $valueFiltered
// perform some transformation upon $value to arrive at $valueFiltered

return $valueFiltered;
}

public function __invoke(mixed $value): mixed {
return $this->filter($value);
}
}
```

To attach an instance of the filter defined above to a filter chain:

```php
$filterChain = new Laminas\Filter\FilterChain();
$filterChain = new Laminas\Filter\FilterChain($pluginManager);
$filterChain->attach(new Application\Filter\MyFilter());
```

Alternately, add it to the `FilterPluginManager`:
## Registering Custom Filters with the Plugin Manager

In both Laminas MVC and Mezzio applications, the top-level `filters` configuration key can be used to register filters with the plugin manager in standard Service Manager format:

```php
$filterChain = new Laminas\Filter\FilterChain();
$filterChain
->getPluginManager()
->setInvokableClass('myfilter', Application\Filter\MyFilter::class)
$filterChain->attachByName('myfilter');
use Laminas\ServiceManager\Factory\InvokableFactory;

return [
'filters' => [
'factories' => [
My\Filter\FilterOne::class => InvokableFactory::class,
My\Filter\FilterTwo::class => My\Filter\SomeCustomFactory::class,
],
'aliases' => [
'filterOne' => My\Filter\FilterOne::class,
'filterTwo' => My\Filter\FilterTwo::class,
],
],
];
```

Assuming the configuration above is merged into your application configuration, either by way of a dedicated configuration file, or via an MVC Module class or Mezzio Config Provider, you would be able to retrieve filter instances from the plugin manager by FQCN or alias.
14 changes: 0 additions & 14 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -185,20 +185,6 @@
<code><![CDATA[(bool) $flag]]></code>
</RedundantCastGivenDocblockType>
</file>
<file src="src/FilterChain.php">
<DeprecatedClass>
<code><![CDATA[AbstractFilter]]></code>
</DeprecatedClass>
<MixedPropertyTypeCoercion>
<code><![CDATA[new PriorityQueue()]]></code>
</MixedPropertyTypeCoercion>
<MoreSpecificImplementedParamType>
<code><![CDATA[$options]]></code>
</MoreSpecificImplementedParamType>
<RedundantFunctionCall>
<code><![CDATA[strtolower]]></code>
</RedundantFunctionCall>
</file>
<file src="src/FilterProviderInterface.php">
<UnusedClass>
<code><![CDATA[FilterProviderInterface]]></code>
Expand Down
Loading

0 comments on commit 8f75078

Please sign in to comment.