diff --git a/docs/book/v3/inflector.md b/docs/book/v3/inflector.md
index 22e2c92f..063c25a9 100644
--- a/docs/book/v3/inflector.md
+++ b/docs/book/v3/inflector.md
@@ -1,312 +1,233 @@
# String Inflection
-`Laminas\Filter\Inflector` is a general purpose tool for rules-based inflection of
-strings to a given target.
+`Laminas\Filter\Inflector` is a general purpose tool for rule-based inflection of strings to a given target.
-As an example, you may find you need to transform MixedCase or camelCasedWords
-into a path; for readability, OS policies, or other reasons, you also need to
-lower case this; and finally, you want to separate the words using a dash
-(`-`). An inflector can do this for you.
+As an example, you may find you need to transform MixedCase or camelCasedWords into a path;
+for readability, OS policies, or other reasons, you also need to lower case this;
+and finally, you want to separate the words using a dash (`-`).
+An inflector can do this for you.
`Laminas\Filter\Inflector` implements `Laminas\Filter\FilterInterface`; you perform
inflection by calling `filter()` on the object instance.
-## Transforming MixedCase and CamelCaseText to another Format
+## Options Reference
+
+- `target` **(required)** The target string containing the placeholders to replace
+- `rules` **(required)** An array of inflection rules
+- `targetReplacementIdentifier` Can be used to override the default placeholder delimiter of `':'`
+- `throwTargetExceptionsOn` A boolean to indicate whether an exception should be thrown for un-processed placeholders *(`true` by default)*
+
+## Example: Transform Mixed Case and Camel Cased Text to Another Format
```php
-$inflector = new Laminas\Filter\Inflector('pages/:page.:suffix');
-$inflector->setRules([
- ':page' => ['Word\CamelCaseToDash', 'StringToLower'],
- 'suffix' => 'html',
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'target' => 'pages/:page.:suffix',
+ 'rules' => [
+ ':page' => [
+ Laminas\Filter\Word\CamelCaseToDash::class,
+ Laminas\Filter\StringToLower::class,
+ ],
+ 'suffix' => 'html',
+ ],
]);
-$string = 'camelCasedWords';
-$filtered = $inflector->filter(['page' => $string]);
+$filtered = $inflector->filter(['page' => 'camelCasedWords']);
// pages/camel-cased-words.html
-$string = 'this_is_not_camel_cased';
-$filtered = $inflector->filter(['page' => $string]);
+$filtered = $inflector->filter(['page' => 'this_is_not_camel_cased']);
// pages/this_is_not_camel_cased.html
```
## Operation
-An inflector requires a **target** and one or more **rules**. A target is
-basically a string that defines placeholders for variables you wish to
-substitute. These are specified by prefixing with a `:`: `:script`.
+An inflector requires a **target** and one or more **rules**.
-When calling `filter()`, you then pass in an array of key and value pairs
-corresponding to the variables in the target.
+A `target` is basically a string that defines placeholders for variables you wish to substitute.
+These are specified by prefixing the placeholder name with a `:`, for example `:script`.
-Each variable in the target can have zero or more rules associated with them.
-Rules may be either **static** or refer to a laminas-filter class. Static rules
-will replace with the text provided. Otherwise, a class matching the rule
-provided will be used to inflect the text. Classes are typically specified
-using a short name indicating the filter name stripped of any common prefix.
+When calling `filter()`, you then pass in an array of key and value pairs corresponding to the placeholder names in the target.
-As an example, you can use any laminas-filter concrete implementations; however,
-instead of referring to them as `Laminas\Filter\Boolean` or
-`Laminas\Filter\StringToLower`, you'd specify only `Boolean` or `StringToLower`.
+Each variable in the target can have zero or more rules associated with them.
+Rules may be either **static** or refer to a filter type, or callable.
+**Static Rules** define straight-forward string replacement.
+**Filter Rules** define one or more filters that operate on the value.
-### Using Custom Filters
+Filters can be specified using:
-`Laminas\Filter\Inflector` uses `Laminas\Filter\FilterPluginManager` to manage
-loading filters to use with inflection. By default, filters registered with
-`Laminas\Filter\FilterPluginManager` are available. To access filters with that
-prefix but which occur deeper in the hierarchy, such as the various `Word`
-filters, simply strip off the `Laminas\Filter` prefix:
+- The FQCN of a filter such as `Laminas\Filter\StringToLower::class`
+- An alias of a known filter such as `stringtolower`
+- A concrete filter instance, i.e. `new Laminas\Filter\StringToLower()`
+- Any callable, for example a closure such as `static fn (string $input): string => strtolower($input)`
-```php
-// use Laminas\Filter\Word\CamelCaseToDash as a rule
-$inflector->addRules(['script' => 'Word\CamelCaseToDash']);
-```
+### Using Custom Filters
-To use custom filters, you have two choices: reference them by fully qualified
-class name (e.g., `My\Custom\Filter\Mungify`), or manipulate the composed
-`FilterPluginManager` instance.
+`Laminas\Filter\Inflector` uses `Laminas\Filter\FilterPluginManager` to manage loading filters to use with inflection.
+By default, all [standard filters](standard-filters.md) are available by referencing any FQCN, or known alias of the filter type.
+If you have configured your application with [custom filters](writing-filters.md), these will also be available in any rules you define.
-```php
-$filters = $inflector->getPluginManager();
-$filters->addInvokableClass('mungify', 'My\Custom\Filter\Mungify');
-```
+TIP: Try to prefer fully qualified class names *(FQCNs)* rather than aliases or 'short names'.
+It will be easier for your IDE or text editor to identify specific filter usage when you use FQCNs.
### Setting the Inflector Target
-The inflector target is a string with some placeholders for variables.
-Placeholders take the form of an identifier, a colon (`:`) by default, followed
-by a variable name: `:script`, `:path`, etc. The `filter()` method looks for
-the identifier followed by the variable name being replaced.
-
-You can change the identifier using the `setTargetReplacementIdentifier()`
-method, or passing it as the fourth argument to the constructor:
+As previously mentioned, the *required* option `target` is a string with some placeholders for variables.
+Placeholders take the form of an identifier, a colon (`:`) by default, followed by a variable name: `:script`, `:path`, etc.
+The `filter()` method looks for the identifier followed by the variable name being replaced.
```php
-// Via constructor:
-$inflector = new Laminas\Filter\Inflector('#foo/#bar.#sfx', array(), null, '#');
-
-// Via accessor:
-$inflector->setTargetReplacementIdentifier('#');
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'target' => 'pages/:page.:suffix',
+ // ... Other Options
+]);
```
-Typically, you will set the target via the constructor. However, you may want
-to re-set the target later. `setTarget()` can be used for this purpose:
+### Changing the Target Placeholder Delimiter
-```php
-$inflector->setTarget('layouts/:script.phtml');
-```
-
-Additionally, you may wish to have a class member for your class that you can
-use to keep the inflector target updated — without needing to directly update
-the target each time (thus saving on method calls). `setTargetReference()`
-allows you to do this:
+By setting the `targetReplacementIdentifier` option to the delimiter of your choice, you can prevent issues where the delimiter might need to be part of the `target` option:
```php
-class Foo
-{
- /**
- * @var string Inflector target
- */
- protected $target = 'foo/:bar/:baz.:suffix';
-
- /**
- * Constructor
- * @return void
- */
- public function __construct()
- {
- $this->inflector = new Laminas\Filter\Inflector();
- $this->inflector->setTargetReference($this->target);
- }
-
- /**
- * Set target; updates target in inflector
- *
- * @param string $target
- * @return Foo
- */
- public function setTarget($target)
- {
- $this->target = $target;
- return $this;
- }
-}
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'target' => '?host:?port/?page.?suffix',
+ 'targetReplacementIdentifier' => '?',
+ 'rules' => [
+ 'host' => 'example.com',
+ 'port' => '80',
+ 'page' => 'anything',
+ 'suffix' => 'html',
+ ],
+]);
+$inflector->filter(['page' => 'index']); // example.com:80/index.html
```
## Inflection Rules
-As mentioned in the introduction, there are two types of rules: static and filter-based.
+As mentioned in the introduction, there are two types of rules: **static** and **filter-based**.
-NOTE: **Order is important**
-It is important to note that regardless of the method in which you add rules to the inflector, either one-by-one, or all-at-once; the order is very important.
-More specific names, or names that might contain other rule names, must be added before least specific names.
-For example, assuming the two rule names `moduleDir` and `module`, the `moduleDir` rule should appear before module since `module` is contained within `moduleDir`.
-If `module` were added before `moduleDir`, `module` will match part of `moduleDir` and process it leaving `Dir` inside of the target uninflected.
+### Specifying Static Rules
-### Static Rules
+Static rules are key-value items, where `['placeholder' => 'replacement']`.
+Static rule names lack a leading `':'`.
-Static rules do simple string substitution; use them when you have a segment in
-the target that will typically be static, but which you want to allow the
-developer to modify. Use the `setStaticRule()` method to set or modify the
-rule:
+Filter input can be used to override the replacement value for a placeholder:
```php
-$inflector = new Laminas\Filter\Inflector(':script.:suffix');
-$inflector->setStaticRule('suffix', 'phtml');
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'target' => ':a,:b,:c',
+ 'rules' => [
+ 'a' => 'one',
+ 'b' => 'two',
+ 'c' => 'three',
+ ],
+]);
-// change it later:
-$inflector->setStaticRule('suffix', 'php');
+$inflector->filter(['c' => 'foo']); // 'one,two,foo'
```
-Much like the target itself, you can also bind a static rule to a reference,
-allowing you to update a single variable instead of require a method call; this
-is often useful when your class uses an inflector internally, and you don't
-want your users to need to fetch the inflector in order to update it. The
-`setStaticRuleReference()` method is used to accomplish this:
-
-```php
-class Foo
-{
- /**
- * @var string Suffix
- */
- private $suffix = 'phtml';
-
- /**
- * Constructor
- * @return void
- */
- public function construct()
- {
- $this->inflector = new Laminas\Filter\Inflector(':script.:suffix');
- $this->inflector->setStaticRuleReference('suffix', $this->suffix);
- }
-
- /**
- * Set suffix; updates suffix static rule in inflector
- *
- * @param string $suffix
- * @return Foo
- */
- public function setSuffix($suffix)
- {
- $this->suffix = $suffix;
- return $this;
- }
-}
-```
+NOTE: **Order is important**
+It is important to note that regardless of the rule type, either static of filter based; the order is very important.
+More specific names, or names that might contain other rule names, must be added before the least specific names.
+For example, assuming two rule names `moduleDir` and `module`, the `moduleDir` rule should appear before module since the word `module` is contained within `moduleDir`.
+If `module` were added before `moduleDir`, `module` will match part of `moduleDir` and process it leaving `Dir` inside of the target un-inflected.
### Filter-Based Inflector Rules
-`Laminas\Filter` filters may be used as inflector rules as well. Just like static
-rules, these are bound to a target variable; unlike static rules, you may
-define multiple filters to use when inflecting. These filters are processed in
-order, so be careful to register them in an order that makes sense for the data
-you receive.
+Filters may be used as inflector rules as well. Just like static rules, these are bound to a target variable; unlike static rules, you may define multiple filters to use when inflecting.
+These filters are processed in order, so be careful to register them in an order that makes sense for the data you receive.
-Rules may be added using `setFilterRule()` (which overwrites any previous rules
-for that variable) or `addFilterRule()` (which appends the new rule to any
-existing rule for that variable). Filters are specified in one of the following
-ways:
+```php
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'target' => ':script.:suffix',
+ 'rules' => [
+ ':script' => [
+ Laminas\Filter\Word\CamelCaseToDash::class,
+ Laminas\Filter\StringToLower::class,
+ ],
+ 'suffix' => 'php',
+ ],
+]);
-- **String**. The string may be a filter class name, or a class name segment
- minus any prefix set in the inflector's plugin loader (by default, minus the
- '`Laminas\Filter`' prefix).
-- **Filter object**. Any object instance implementing
- `Laminas\Filter\FilterInterface` may be passed as a filter.
-- **Array**. An array of one or more strings or filter objects as defined above.
+$inflector->filter(['script' => 'MyScript']); // "my-script.php"
+```
-```php
-$inflector = new Laminas\Filter\Inflector(':script.:suffix');
+### Avoiding Exceptions
-// Set rule to use Laminas\Filter\Word\CamelCaseToDash filter
-$inflector->setFilterRule('script', 'Word\CamelCaseToDash');
+Finally, the option `throwTargetExceptionsOn` defines whether an exception is thrown for un-processed placeholders.
-// Add rule to lowercase string
-$inflector->addFilterRule('script', new Laminas\Filter\StringToLower());
+By default, the following example will cause an exception:
-// Set rules en-masse
-$inflector->setFilterRule('script', [
- 'Word\CamelCaseToDash',
- new Laminas\Filter\StringToLower()
+```php
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'target' => ':script.:suffix',
+ 'rules' => [
+ ':script' => [
+ Laminas\Filter\Word\CamelCaseToDash::class,
+ Laminas\Filter\StringToLower::class,
+ ],
+ 'suffix' => 'php',
+ ],
]);
+
+$inflector->filter(['wrong-key' => 'SomeValue']); // Laminas\Filter\Exception\RuntimeException
```
-## Setting many Rules at once
+By setting `throwTargetExceptionsOn` to `false`, no exception will be thrown, but the filter output will contain un-processed placeholder values:
-Typically, it's easier to set many rules at once than to configure a single
-variable and its inflection rules one at a time. `Laminas\Filter\Inflector`'s
-`addRules()` and `setRules()` methods allow this.
+```php
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = new Laminas\Filter\Inflector($pluginManager, [
+ 'throwTargetExceptionsOn' => false,
+ 'target' => ':script.:suffix',
+ 'rules' => [
+ ':script' => [
+ Laminas\Filter\Word\CamelCaseToDash::class,
+ Laminas\Filter\StringToLower::class,
+ ],
+ 'suffix' => 'php',
+ ],
+]);
-Each method takes an array of variable and rule pairs, where the rule may be
-whatever the type of rule accepts (string, filter object, or array). Variable
-names accept a special notation to allow setting static rules and filter rules,
-according to the following notation:
+$inflector->filter(['wrong-key' => 'SomeValue']); // ':script.php'
+```
-- **`:` prefix**: filter rules.
-- **No prefix**: static rule.
+## Filterable Values
-As an example:
+The inflector can only process arrays or objects. Other types will be returned as-is, for example:
```php
-// Could also use setRules() with this notation:
-$inflector->addRules([
- // Filter rules:
- ':controller' => ['Word\CamelCaseToUnderscore','StringToLower'],
- ':action' => ['Word\CamelCaseToUnderscore','StringToLower'],
-
- // Static rule:
- 'suffix' => 'phtml',
-]);
+$inflector->filter('string'); // 'string
```
-## Utility Methods
-
-`Laminas\Filter\Inflector` has a number of utility methods for retrieving and
-setting the plugin loader, manipulating and retrieving rules, and controlling
-if and when exceptions are thrown.
-
-- `setPluginManager()` can be used when you have configured your own
- `Laminas\Filter\FilterPluginManager` instance and wish to use it with
- `Laminas\Filter\Inflector`; `getPluginManager()` retrieves the currently set
- one.
-- `setThrowTargetExceptionsOn()` can be used to control whether or not
- `filter()` throws an exception when a given replacement identifier passed to
- it is not found in the target. By default, no exceptions are thrown.
- `isThrowTargetExceptionsOn()` will tell you what the current value is.
-- `getRules($spec = null)` can be used to retrieve all registered rules for all
- variables, or just the rules for a single variable.
-- `getRule($spec, $index)` fetches a single rule for a given variable; this can
- be useful for fetching a specific filter rule for a variable that has a
- filter chain. `$index` must be passed.
-- `clearRules()` will clear all currently registered rules.
-
-## Using a Traversable or an Array
-
-You can use a `Traversable` or an array to set rules and other object state in
-your inflectors, by passing either type to either the constructor or the
-`setOptions()` method. The following settings may be specified:
-
-- `target` specifies the inflection target.
-- `pluginManager` specifies the `Laminas\Filter\FilterPluginManager` instance or
- extension to use for obtaining plugins; alternately, you may specify a class
- name of a class that extends the `FilterPluginManager`.
-- `throwTargetExceptionsOn` should be a boolean indicating whether or not to
- throw exceptions when a replacement identifier is still present after
- inflection.
-- `targetReplacementIdentifier` specifies the character to use when identifying
- replacement variables in the target string.
-- `rules` specifies an array of inflection rules; it should consist of keys
- that specify either values or arrays of values, consistent with `addRules()`.
-
-As examples:
+When an object is passed, it's public properties are extracted into an array using [`get_object_vars`](https://www.php.net/get_object_vars).
-```php
-// $options implements Traversable:
+This can be useful when you wish to use a readonly value object to define a specific, predictable set of data to be used for inflection with consistent type guarantees.
-// With the constructor:
-$inflector = new Laminas\Filter\Inflector($options);
+## Filter Plugin Manager Requirement
-// Or with setOptions():
-$inflector = new Laminas\Filter\Inflector();
-$inflector->setOptions($options);
+In all the examples so far, you will notice that the first constructor argument is an instance of `Laminas\Filter\FilterPluginManager`. The plugin manager is used to create filter instances for use with inflection and cannot be omitted.
+
+During general usage of filters, you will typically be configuring filters as part of an [input filter specification with laminas-inputfilter](https://docs.laminas.dev/laminas-inputfilter/) or `laminas-form`. In these cases, the constructor dependencies are resolved automatically and you only need to concern yourself with setting the correct options.
+
+If you find the need to use the inflector stand-alone, you can use `Laminas\Filter\FilterPluginManager::build()` to create an instance by only specifying options:
+
+```php
+$pluginManager = $container->get(Laminas\Filter\FilterPluginManager::class);
+$inflector = $pluginManager->build(
+ Laminas\Filter\Inflector::class,
+ [
+ 'target' => ':script.:suffix',
+ 'rules' => [
+ ':script' => [
+ Laminas\Filter\Word\CamelCaseToDash::class,
+ Laminas\Filter\StringToLower::class,
+ ],
+ 'suffix' => 'php',
+ ],
+ ],
+);
```
diff --git a/docs/book/v3/migration/v2-to-v3.md b/docs/book/v3/migration/v2-to-v3.md
index 2e91465d..3a1a5cfa 100644
--- a/docs/book/v3/migration/v2-to-v3.md
+++ b/docs/book/v3/migration/v2-to-v3.md
@@ -152,6 +152,36 @@ The following methods have been removed:
The constructor now only accepts an associative array of [documented options](../standard-filters.md#denylist).
+#### `Inflector`
+
+The following methods have been removed:
+
+- `getPluginManager`
+- `setPluginManager`
+- `setOptions`
+- `setThrowTargetExceptionsOn`
+- `isThrowTargetExceptionsOn`
+- `setTargetReplacementIdentifier`
+- `getTargetReplacementIdentifier`
+- `setTarget`
+- `getTarget`
+- `setTargetReference`
+- `setRules`
+- `addRules`
+- `getRules`
+- `getRule`
+- `clearRules`
+- `setFilterRule`
+- `addFilterRule`
+- `setStaticRule`
+- `setStaticRuleReference`
+
+The constructor now only accepts an associative array of [documented options](../inflector.md) and requires a `FilterPluginManager` instance as its first argument.
+
+The ability to pass in references to be used as the inflection target, or as a static rule is no longer possible.
+
+It is now possible to use an object as the filter input.
+
#### `MonthSelect`
The following methods have been removed:
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index e23a8698..1fe630e8 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -218,99 +218,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rules[$spec][]]]>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- getPluginManager()->get($rule)]]>
- getPluginManager()->get($rule)]]>
- rules[$spec]]]>
- rules[$spec][$index]]]>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -393,55 +300,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/FilterPluginManager.php b/src/FilterPluginManager.php
index 402c6bb0..0d808445 100644
--- a/src/FilterPluginManager.php
+++ b/src/FilterPluginManager.php
@@ -51,7 +51,7 @@ final class FilterPluginManager extends AbstractPluginManager
ForceUriScheme::class => InvokableFactory::class,
HtmlEntities::class => InvokableFactory::class,
ImmutableFilterChain::class => ImmutableFilterChainFactory::class,
- Inflector::class => InvokableFactory::class,
+ Inflector::class => InflectorFactory::class,
ToFloat::class => InvokableFactory::class,
MonthSelect::class => InvokableFactory::class,
UpperCaseWords::class => InvokableFactory::class,
diff --git a/src/Inflector.php b/src/Inflector.php
index 4516fc09..a41abc1c 100644
--- a/src/Inflector.php
+++ b/src/Inflector.php
@@ -4,18 +4,15 @@
namespace Laminas\Filter;
-use Laminas\Filter\FilterInterface;
-use Laminas\ServiceManager\ServiceManager;
-use Laminas\Stdlib\ArrayUtils;
-use Traversable;
+use Laminas\Filter\Exception\InvalidArgumentException;
-use function array_key_exists;
use function array_keys;
-use function array_shift;
+use function array_map;
use function array_values;
-use function class_exists;
-use function func_get_args;
+use function assert;
+use function get_object_vars;
use function is_array;
+use function is_object;
use function is_scalar;
use function is_string;
use function ltrim;
@@ -23,426 +20,132 @@
use function preg_quote;
use function preg_replace;
use function str_replace;
+use function str_starts_with;
/**
* Filter chain for string inflection
*
+ * @psalm-import-type InstanceType from FilterPluginManager
+ * @psalm-type RulesArray = array>
* @psalm-type Options = array{
- * target?: string,
- * rules?: array,
+ * target: string,
+ * rules?: RulesArray,
* throwTargetExceptionsOn?: bool,
- * targetReplacementIdentifier?: string,
- * pluginManager?: FilterPluginManager,
+ * targetReplacementIdentifier?: non-empty-string,
* }
- * @extends AbstractFilter
+ * @implements FilterInterface
*/
-final class Inflector extends AbstractFilter
+final class Inflector implements FilterInterface
{
- /** @var FilterPluginManager */
- protected $pluginManager;
-
- /** @var string */
- protected $target;
-
- /** @var bool */
- protected $throwTargetExceptionsOn = true;
-
- /** @var string */
- protected $targetReplacementIdentifier = ':';
-
- /** @var array */
- protected $rules = [];
-
- /**
- * @param string|array|Traversable $options Options to set
- */
- public function __construct($options = null)
- {
- if ($options instanceof Traversable) {
- $options = ArrayUtils::iteratorToArray($options);
- }
- if (! is_array($options)) {
- $options = func_get_args();
- $temp = [];
-
- if (! empty($options)) {
- $temp['target'] = array_shift($options);
- }
-
- if (! empty($options)) {
- $temp['rules'] = array_shift($options);
- }
-
- if (! empty($options)) {
- $temp['throwTargetExceptionsOn'] = array_shift($options);
- }
-
- if (! empty($options)) {
- $temp['targetReplacementIdentifier'] = array_shift($options);
- }
-
- $options = $temp;
- }
-
- $this->setOptions($options);
- }
-
- /**
- * Retrieve plugin manager
- *
- * @return FilterPluginManager
- */
- public function getPluginManager()
- {
- if (! $this->pluginManager instanceof FilterPluginManager) {
- $this->setPluginManager(new FilterPluginManager(new ServiceManager()));
- }
-
- return $this->pluginManager;
- }
-
- /**
- * Set plugin manager
- *
- * @return self
- */
- public function setPluginManager(FilterPluginManager $manager)
- {
- $this->pluginManager = $manager;
- return $this;
- }
-
- /**
- * Set options
- *
- * @param array|Options|iterable $options
- * @return self
- */
- public function setOptions($options)
- {
- if ($options instanceof Traversable) {
- $options = ArrayUtils::iteratorToArray($options);
- }
-
- // Set plugin manager
- if (array_key_exists('pluginManager', $options)) {
- if (is_scalar($options['pluginManager']) && class_exists($options['pluginManager'])) {
- $options['pluginManager'] = new $options['pluginManager']();
- }
- $this->setPluginManager($options['pluginManager']);
- }
-
- if (array_key_exists('throwTargetExceptionsOn', $options)) {
- $this->setThrowTargetExceptionsOn($options['throwTargetExceptionsOn']);
- }
-
- if (array_key_exists('targetReplacementIdentifier', $options)) {
- $this->setTargetReplacementIdentifier($options['targetReplacementIdentifier']);
- }
-
- if (array_key_exists('target', $options)) {
- $this->setTarget($options['target']);
- }
-
- if (array_key_exists('rules', $options)) {
- $this->addRules($options['rules']);
- }
-
- return $this;
- }
-
- /**
- * Set Whether or not the inflector should throw an exception when a replacement
- * identifier is still found within an inflected target.
- *
- * @param bool $throwTargetExceptionsOn
- * @return self
- */
- public function setThrowTargetExceptionsOn($throwTargetExceptionsOn)
- {
- $this->throwTargetExceptionsOn = (bool) $throwTargetExceptionsOn;
- return $this;
- }
-
- /**
- * Will exceptions be thrown?
- *
- * @return bool
- */
- public function isThrowTargetExceptionsOn()
- {
- return $this->throwTargetExceptionsOn;
- }
-
- /**
- * Set the Target Replacement Identifier, by default ':'
- *
- * @param string $targetReplacementIdentifier
- * @return self
- */
- public function setTargetReplacementIdentifier($targetReplacementIdentifier)
- {
- if ($targetReplacementIdentifier) {
- $this->targetReplacementIdentifier = (string) $targetReplacementIdentifier;
- }
-
- return $this;
- }
-
- /**
- * Get Target Replacement Identifier
- *
- * @return string
- */
- public function getTargetReplacementIdentifier()
- {
- return $this->targetReplacementIdentifier;
- }
-
- /**
- * Set a Target
- * ex: 'scripts/:controller/:action.:suffix'
- *
- * @param string $target
- * @return self
- */
- public function setTarget($target)
- {
- $this->target = (string) $target;
- return $this;
- }
-
- /**
- * Retrieve target
- *
- * @return string
- */
- public function getTarget()
- {
- return $this->target;
- }
-
- /**
- * Set Target Reference
- *
- * @param string $target
- * @return self
- */
- public function setTargetReference(&$target)
- {
- $this->target = &$target;
- return $this;
- }
-
- /**
- * Is the same as calling addRules() with the exception that it
- * clears the rules before adding them.
- *
- * @return self
- */
- public function setRules(array $rules)
- {
- $this->clearRules();
- $this->addRules($rules);
- return $this;
- }
-
- /**
- * Multi-call to setting filter rules.
- *
- * If prefixed with a ":" (colon), a filter rule will be added. If not
- * prefixed, a static replacement will be added.
- *
- * ex:
- * array(
- * ':controller' => array('CamelCaseToUnderscore', 'StringToLower'),
- * ':action' => array('CamelCaseToUnderscore', 'StringToLower'),
- * 'suffix' => 'phtml'
- * );
- *
- * @return self
- */
- public function addRules(array $rules)
- {
- $keys = array_keys($rules);
- foreach ($keys as $spec) {
- if ($spec[0] === ':') {
- $this->addFilterRule($spec, $rules[$spec]);
+ /** @var non-empty-string */
+ private readonly string $target;
+ private readonly bool $throwTargetExceptionsOn;
+ /** @var non-empty-string */
+ private readonly string $targetReplacementIdentifier;
+ /** @var array> */
+ private readonly array $rules;
+
+ /** @param Options $options */
+ public function __construct(
+ private readonly FilterPluginManager $pluginManager,
+ array $options,
+ ) {
+ $target = $options['target'] ?? null;
+ if (! is_string($target) || $target === '') {
+ throw new InvalidArgumentException('Inflector requires the target option to be a non-empty string');
+ }
+
+ $this->target = $target;
+ $this->throwTargetExceptionsOn = $options['throwTargetExceptionsOn'] ?? true;
+ $this->targetReplacementIdentifier = $options['targetReplacementIdentifier'] ?? ':';
+ $this->rules = $this->resolveRules($options['rules'] ?? []);
+ }
+
+ /**
+ * Resolve rules argument
+ *
+ * If prefixed with a ":" (colon), a filter rule will be added.
+ * If not prefixed, a static string replacement will be added.
+ *
+ * example:
+ * [
+ * ':controller' => [CamelCaseToUnderscore::class, StringToLower::class],
+ * ':action' => [CamelCaseToUnderscore::class, StringToLower::class],
+ * 'suffix' => 'phtml',
+ * ]
+ *
+ * @param RulesArray $rules
+ * @return array>
+ */
+ private function resolveRules(array $rules): array
+ {
+ $resolved = [];
+ foreach ($rules as $spec => $ruleSet) {
+ $name = ltrim($spec, ':');
+ if (str_starts_with($spec, ':')) {
+ $resolved[$name] = array_map(
+ function (string|FilterInterface|callable $filter): FilterInterface|callable {
+ return $this->loadFilter($filter);
+ },
+ is_string($ruleSet) ? [$ruleSet] : $ruleSet,
+ );
} else {
- $this->setStaticRule($spec, $rules[$spec]);
- }
- }
-
- return $this;
- }
-
- /**
- * Get rules
- *
- * By default, returns all rules. If a $spec is provided, will return those
- * rules if found, false otherwise.
- *
- * @param string $spec
- * @return array|false
- */
- public function getRules($spec = null)
- {
- if (null !== $spec) {
- $spec = $this->_normalizeSpec($spec);
- if (isset($this->rules[$spec])) {
- return $this->rules[$spec];
+ assert(is_string($ruleSet));
+ $resolved[$name] = $ruleSet;
}
- return false;
}
- return $this->rules;
+ return $resolved;
}
- /**
- * Returns a rule set by setFilterRule(), a numeric index must be provided
- *
- * @param string $spec
- * @param int $index
- * @return FilterInterface|false
- */
- public function getRule($spec, $index)
- {
- $spec = $this->_normalizeSpec($spec);
- if (isset($this->rules[$spec]) && is_array($this->rules[$spec])) {
- if (isset($this->rules[$spec][$index])) {
- return $this->rules[$spec][$index];
- }
- }
- return false;
- }
-
- /**
- * Clears the rules currently in the inflector
- *
- * @return self
- */
- public function clearRules()
- {
- $this->rules = [];
- return $this;
- }
-
- /**
- * Set a filtering rule for a spec. $ruleSet can be a string, Filter object
- * or an array of strings or filter objects.
- *
- * @param string $spec
- * @param array|string|FilterInterface $ruleSet
- * @return self
- */
- public function setFilterRule($spec, $ruleSet)
- {
- $spec = $this->_normalizeSpec($spec);
- $this->rules[$spec] = [];
- return $this->addFilterRule($spec, $ruleSet);
- }
-
- /**
- * Add a filter rule for a spec
- *
- * @return self
- */
- public function addFilterRule(mixed $spec, mixed $ruleSet)
+ public function filter(mixed $value): mixed
{
- $spec = $this->_normalizeSpec($spec);
- if (! isset($this->rules[$spec])) {
- $this->rules[$spec] = [];
+ if (is_object($value)) {
+ $value = get_object_vars($value);
}
- if (! is_array($ruleSet)) {
- $ruleSet = [$ruleSet];
+ if (! is_array($value)) {
+ return $value;
}
- if (is_string($this->rules[$spec])) {
- $temp = $this->rules[$spec];
- $this->rules[$spec] = [];
- $this->rules[$spec][] = $temp;
- }
-
- foreach ($ruleSet as $rule) {
- $this->rules[$spec][] = $this->_getRule($rule);
- }
-
- return $this;
- }
-
- /**
- * Set a static rule for a spec. This is a single string value
- *
- * @param string $name
- * @param string $value
- * @return self
- */
- public function setStaticRule($name, $value)
- {
- $name = $this->_normalizeSpec($name);
- $this->rules[$name] = (string) $value;
- return $this;
- }
-
- /**
- * Set Static Rule Reference.
- *
- * This allows a consuming class to pass a property or variable
- * in to be referenced when its time to build the output string from the
- * target.
- *
- * @param string $name
- * @return self
- */
- public function setStaticRuleReference($name, mixed &$reference)
- {
- $name = $this->_normalizeSpec($name);
- $this->rules[$name] = &$reference;
- return $this;
- }
-
- /**
- * Inflect
- *
- * @param string|array $value
- * @throws Exception\RuntimeException
- */
- public function filter(mixed $value): mixed
- {
// clean source
- foreach ((array) $value as $sourceName => $sourceValue) {
- $value[ltrim($sourceName, ':')] = $sourceValue;
+ $subject = [];
+ foreach ($value as $sourceName => $sourceValue) {
+ if (! is_string($sourceName) || ! is_scalar($sourceValue)) {
+ continue;
+ }
+
+ $sourceName = ltrim($sourceName, ':');
+ $subject[$sourceName] = (string) $sourceValue;
}
$pregQuotedTargetReplacementIdentifier = preg_quote($this->targetReplacementIdentifier, '#');
$processedParts = [];
foreach ($this->rules as $ruleName => $ruleValue) {
- if (isset($value[$ruleName])) {
+ if (isset($subject[$ruleName])) {
if (is_string($ruleValue)) {
- // overriding the set rule
$processedParts['#' . $pregQuotedTargetReplacementIdentifier . $ruleName . '#'] = str_replace(
'\\',
'\\\\',
- $value[$ruleName]
+ $subject[$ruleName],
);
- } elseif (is_array($ruleValue)) {
- $processedPart = $value[$ruleName];
+ } else {
+ $processedPart = $subject[$ruleName];
foreach ($ruleValue as $ruleFilter) {
- $processedPart = $ruleFilter($processedPart);
+ $processedPart = (string) $ruleFilter($processedPart);
}
$processedParts['#' . $pregQuotedTargetReplacementIdentifier . $ruleName . '#'] = str_replace(
'\\',
'\\\\',
- $processedPart
+ $processedPart,
);
}
} elseif (is_string($ruleValue)) {
$processedParts['#' . $pregQuotedTargetReplacementIdentifier . $ruleName . '#'] = str_replace(
'\\',
'\\\\',
- $ruleValue
+ $ruleValue,
);
}
}
@@ -458,41 +161,29 @@ public function filter(mixed $value): mixed
throw new Exception\RuntimeException(
'A replacement identifier ' . $this->targetReplacementIdentifier
. ' was found inside the inflected target, perhaps a rule was not satisfied with a target source? '
- . 'Unsatisfied inflected target: ' . $inflectedTarget
+ . 'Unsatisfied inflected target: ' . $inflectedTarget,
);
}
return $inflectedTarget;
}
- /**
- * Normalize spec string
- *
- * @param string $spec
- * @return string
- */
- // @codingStandardsIgnoreStart
- protected function _normalizeSpec($spec)
+ public function __invoke(mixed $value): mixed
{
- // @codingStandardsIgnoreEnd
- return ltrim((string) $spec, ':&');
+ return $this->filter($value);
}
/**
* Resolve named filters and convert them to filter objects.
*
- * @param string $rule
- * @return FilterInterface|callable(mixed): mixed
+ * @return InstanceType
*/
- // @codingStandardsIgnoreStart
- protected function _getRule($rule)
+ private function loadFilter(string|FilterInterface|callable $rule): FilterInterface|callable
{
- // @codingStandardsIgnoreEnd
- if ($rule instanceof FilterInterface) {
+ if (! is_string($rule)) {
return $rule;
}
- $rule = (string) $rule;
- return $this->getPluginManager()->get($rule);
+ return $this->pluginManager->get($rule);
}
}
diff --git a/src/InflectorFactory.php b/src/InflectorFactory.php
new file mode 100644
index 00000000..b5b598c9
--- /dev/null
+++ b/src/InflectorFactory.php
@@ -0,0 +1,28 @@
+ $options */
+ public function __invoke(
+ ContainerInterface $container,
+ string $requestedName,
+ ?array $options = null,
+ ): Inflector {
+ /** @psalm-var Options $options - Forcing this type to avoid unnecessary runtime validation */
+ $options = $options ?? [];
+ $pluginManager = $container->get(FilterPluginManager::class);
+ assert($pluginManager instanceof FilterPluginManager);
+
+ return new Inflector($pluginManager, $options);
+ }
+}
diff --git a/test/FilterPluginManagerCompatibilityTest.php b/test/FilterPluginManagerCompatibilityTest.php
index e4a1eabf..1eec67a2 100644
--- a/test/FilterPluginManagerCompatibilityTest.php
+++ b/test/FilterPluginManagerCompatibilityTest.php
@@ -6,9 +6,12 @@
use Generator;
use Laminas\Filter\Callback;
+use Laminas\Filter\DataUnitFormatter;
use Laminas\Filter\FilterPluginManager;
+use Laminas\Filter\Inflector;
use Laminas\Filter\PregReplace;
use Laminas\ServiceManager\Exception\InvalidServiceException;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use stdClass;
@@ -17,12 +20,13 @@
use function assert;
use function class_exists;
use function in_array;
-use function str_contains;
class FilterPluginManagerCompatibilityTest extends TestCase
{
private const FILTERS_WITH_REQUIRED_OPTIONS = [
Callback::class,
+ DataUnitFormatter::class,
+ Inflector::class,
PregReplace::class,
];
@@ -44,11 +48,6 @@ public static function aliasProvider(): Generator
self::assertIsString($alias);
self::assertIsString($target);
- // Skipping as it has required options
- if (str_contains($target, 'DataUnitFormatter')) {
- continue;
- }
-
if (in_array($target, self::FILTERS_WITH_REQUIRED_OPTIONS, true)) {
continue;
}
@@ -61,8 +60,8 @@ public static function aliasProvider(): Generator
/**
* @param class-string $expected
- * @dataProvider aliasProvider
*/
+ #[DataProvider('aliasProvider')]
public function testPluginAliasesResolve(string $alias, string $expected): void
{
self::assertInstanceOf(
diff --git a/test/InflectorFactoryTest.php b/test/InflectorFactoryTest.php
new file mode 100644
index 00000000..80ac825b
--- /dev/null
+++ b/test/InflectorFactoryTest.php
@@ -0,0 +1,77 @@
+set(FilterPluginManager::class, $plugins);
+
+ $filter = (new InflectorFactory())->__invoke(
+ $container,
+ 'whatever',
+ [
+ 'target' => '/:controller/:action.:suffix',
+ 'rules' => [
+ ':controller' => [CamelCaseToDash::class, StringToLower::class],
+ ':action' => [CamelCaseToDash::class, StringToLower::class],
+ 'suffix' => 'phtml',
+ ],
+ ],
+ );
+
+ self::assertSame(
+ '/my-controller/some-action.php',
+ $filter->filter([
+ 'controller' => 'MyController',
+ 'action' => 'SomeAction',
+ 'suffix' => 'php',
+ ]),
+ );
+ }
+
+ public function testFilterProductionViaPluginManager(): void
+ {
+ $container = new ServiceManager([
+ 'factories' => [
+ FilterPluginManager::class => FilterPluginManagerFactory::class,
+ ],
+ ]);
+ $plugins = $container->get(FilterPluginManager::class);
+ $filter = $plugins->build(
+ Inflector::class,
+ [
+ 'target' => '/:controller/:action.:suffix',
+ 'rules' => [
+ ':controller' => [CamelCaseToDash::class, StringToLower::class],
+ ':action' => [CamelCaseToDash::class, StringToLower::class],
+ 'suffix' => 'phtml',
+ ],
+ ],
+ );
+
+ self::assertSame(
+ '/my-controller/some-action.php',
+ $filter->filter([
+ 'controller' => 'MyController',
+ 'action' => 'SomeAction',
+ 'suffix' => 'php',
+ ]),
+ );
+ }
+}
diff --git a/test/InflectorTest.php b/test/InflectorTest.php
index 9feacccc..1a5b609f 100644
--- a/test/InflectorTest.php
+++ b/test/InflectorTest.php
@@ -4,210 +4,152 @@
namespace LaminasTest\Filter;
-use ArrayObject;
-use Laminas\Filter\Exception;
+use Laminas\Filter\Exception\InvalidArgumentException;
+use Laminas\Filter\Exception\RuntimeException;
use Laminas\Filter\FilterInterface;
use Laminas\Filter\FilterPluginManager;
-use Laminas\Filter\Inflector as InflectorFilter;
+use Laminas\Filter\Inflector;
use Laminas\Filter\StringToLower;
use Laminas\Filter\StringToUpper;
use Laminas\Filter\Word\CamelCaseToDash;
-use Laminas\Filter\Word\CamelCaseToUnderscore;
use Laminas\ServiceManager\ServiceManager;
-use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
-use function array_values;
-use function count;
+use function strtoupper;
use const DIRECTORY_SEPARATOR;
+/** @psalm-import-type Options from Inflector */
class InflectorTest extends TestCase
{
- private InflectorFilter $inflector;
-
- public function setUp(): void
- {
- $this->inflector = new InflectorFilter();
- }
-
- public function testGetPluginManagerReturnsFilterManagerByDefault(): void
- {
- $broker = $this->inflector->getPluginManager();
- self::assertInstanceOf(FilterPluginManager::class, $broker);
- }
-
- public function testSetPluginManagerAllowsSettingAlternatePluginManager(): void
- {
- $defaultManager = $this->inflector->getPluginManager();
- $manager = new FilterPluginManager(new ServiceManager());
- $this->inflector->setPluginManager($manager);
- $receivedManager = $this->inflector->getPluginManager();
- self::assertNotSame($defaultManager, $receivedManager);
- self::assertSame($manager, $receivedManager);
- }
-
- public function testTargetAccessorsWork(): void
- {
- $this->inflector->setTarget('foo/:bar/:baz');
- self::assertSame('foo/:bar/:baz', $this->inflector->getTarget());
- }
-
- public function testTargetInitiallyNull(): void
- {
- self::assertNull($this->inflector->getTarget());
- }
-
- public function testPassingTargetToConstructorSetsTarget(): void
+ /** @param Options $options */
+ private static function withOptions(array $options): Inflector
{
- $inflector = new InflectorFilter('foo/:bar/:baz');
- self::assertSame('foo/:bar/:baz', $inflector->getTarget());
+ return new Inflector(new FilterPluginManager(new ServiceManager()), $options);
}
- public function testSetTargetByReferenceWorks(): void
+ /** @return array */
+ public static function invalidTargets(): array
{
- $target = 'foo/:bar/:baz';
- $this->inflector->setTargetReference($target);
- self::assertSame('foo/:bar/:baz', $this->inflector->getTarget());
- /* this variable is used by-ref through `setTargetReference` above */
- $target .= '/:bat';
- self::assertSame('foo/:bar/:baz/:bat', $this->inflector->getTarget());
+ return [
+ 'Empty String' => [''],
+ 'Null' => [null],
+ 'Array' => [['foo' => 'bar']],
+ ];
}
- public function testSetFilterRuleWithStringRuleCreatesRuleEntryAndFilterObject(): void
+ #[DataProvider('invalidTargets')]
+ public function testTargetOptionMustBeValid(mixed $option): void
{
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $this->inflector->setFilterRule('controller', StringToLower::class);
- $rules = $this->inflector->getRules('controller');
- self::assertIsArray($rules);
- self::assertSame(1, count($rules));
- $filter = $rules[0];
- self::assertInstanceOf(FilterInterface::class, $filter);
+ $this->expectException(InvalidArgumentException::class);
+ /** @psalm-suppress MixedArgumentTypeCoercion - Intentionally invalid argument */
+ self::withOptions([
+ 'target' => $option,
+ ]);
}
- public function testSetFilterRuleWithFilterObjectCreatesRuleEntryWithFilterObject(): void
+ public function testExpectedResultWithValidTargetOption(): void
{
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $filter = new StringToLower();
- $this->inflector->setFilterRule('controller', $filter);
- $rules = $this->inflector->getRules('controller');
- self::assertIsArray($rules);
- self::assertSame(1, count($rules));
- $received = $rules[0];
- self::assertInstanceOf(FilterInterface::class, $received);
- self::assertSame($filter, $received);
- }
+ $filter = self::withOptions([
+ 'target' => 'foo/:bar/:baz.:bat',
+ 'rules' => [
+ ':bar' => [StringToUpper::class],
+ ':baz' => [StringToUpper::class],
+ 'bat' => 'z',
+ ],
+ ]);
- public function testAddFilterRuleAppendsRuleEntries(): void
- {
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $this->inflector->setFilterRule('controller', [StringToLower::class, TestAsset\Alpha::class]);
- $rules = $this->inflector->getRules('controller');
- self::assertIsArray($rules);
- self::assertSame(2, count($rules));
- self::assertInstanceOf(FilterInterface::class, $rules[0]);
- self::assertInstanceOf(FilterInterface::class, $rules[1]);
+ self::assertSame('foo/A/B.z', $filter->__invoke([
+ 'bar' => 'a',
+ 'baz' => 'b',
+ ]));
}
- public function testSetStaticRuleCreatesScalarRuleEntry(): void
+ /** @return array */
+ public static function filterTypesProvider(): array
{
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $this->inflector->setStaticRule('controller', 'foobar');
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress DocblockTypeContradiction */
- self::assertSame('foobar', $rules);
+ return [
+ 'Closure' => [
+ static fn (string $input): string => strtoupper($input),
+ 'foo',
+ 'FOO',
+ ],
+ 'Filter FQCN' => [
+ StringToUpper::class,
+ 'foo',
+ 'FOO',
+ ],
+ 'Filter Instance' => [
+ new StringToUpper(),
+ 'foo',
+ 'FOO',
+ ],
+ 'Filter Alias' => [
+ 'stringtoupper',
+ 'foo',
+ 'FOO',
+ ],
+ ];
}
- public function testSetStaticRuleMultipleTimesOverwritesEntry(): void
+ /** @param string|FilterInterface|callable(mixed):mixed $ruleFilter */
+ #[DataProvider('filterTypesProvider')]
+ public function testFilterRuleExecutesExpectedFilter(mixed $ruleFilter, string $input, string $expect): void
{
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $this->inflector->setStaticRule('controller', 'foobar');
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress DocblockTypeContradiction */
- self::assertSame('foobar', $rules);
- $this->inflector->setStaticRule('controller', 'bazbat');
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress DocblockTypeContradiction */
- self::assertSame('bazbat', $rules);
- }
+ $filter = self::withOptions([
+ 'target' => ':target',
+ 'rules' => [
+ ':target' => [$ruleFilter],
+ ],
+ ]);
- public function testSetStaticRuleReferenceAllowsUpdatingRuleByReference(): void
- {
- $rule = 'foobar';
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $this->inflector->setStaticRuleReference('controller', $rule);
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress DocblockTypeContradiction */
- self::assertSame('foobar', $rules);
- $rule .= '/baz';
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress DocblockTypeContradiction */
- self::assertSame('foobar/baz', $rules);
+ self::assertSame($expect, $filter->filter(['target' => $input]));
}
- public function testAddRulesCreatesAppropriateRuleEntries(): void
+ public function testStaticRulesBehaveLikeStringReplace(): void
{
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(0, count($rules));
- $this->inflector->addRules([
- ':controller' => [StringToLower::class, TestAsset\Alpha::class],
- 'suffix' => 'phtml',
+ $filter = self::withOptions([
+ 'target' => '/:c/:b/:a',
+ 'rules' => [
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ ],
]);
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(2, count($rules));
- self::assertSame(2, count($rules['controller']));
- self::assertSame('phtml', $rules['suffix']);
+
+ self::assertSame('/C/B/A', $filter->filter(['foo' => 'bar']));
}
- public function testSetRulesCreatesAppropriateRuleEntries(): void
+ public function testStaticRuleReplacementsCanBeOverriddenInFilterValue(): void
{
- $this->inflector->setStaticRule('some-rules', 'some-value');
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(1, count($rules));
- $this->inflector->setRules([
- ':controller' => [StringToLower::class, TestAsset\Alpha::class],
- 'suffix' => 'phtml',
+ $filter = self::withOptions([
+ 'target' => '/:c/:b/:a',
+ 'rules' => [
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ ],
]);
- $rules = $this->inflector->getRules();
- self::assertIsArray($rules);
- self::assertSame(2, count($rules));
- self::assertSame(2, count($rules['controller']));
- self::assertSame('phtml', $rules['suffix']);
- }
- public function testGetRule(): void
- {
- $this->inflector->setFilterRule(':controller', [TestAsset\Alpha::class, StringToLower::class]);
- self::assertInstanceOf(StringToLower::class, $this->inflector->getRule('controller', 1));
- self::assertFalse($this->inflector->getRule('controller', 2));
+ self::assertSame('/z/y/x', $filter->filter([
+ 'a' => 'x',
+ 'b' => 'y',
+ 'c' => 'z',
+ ]));
}
public function testFilterTransformsStringAccordingToRules(): void
{
- $this->inflector
- ->setTarget(':controller/:action.:suffix')
- ->addRules([
+ $filter = self::withOptions([
+ 'target' => ':controller/:action.:suffix',
+ 'rules' => [
':controller' => [CamelCaseToDash::class],
':action' => [CamelCaseToDash::class],
'suffix' => 'phtml',
- ]);
+ ],
+ ]);
- $filter = $this->inflector;
$filtered = $filter([
'controller' => 'FooBar',
'action' => 'bazBat',
@@ -215,27 +157,38 @@ public function testFilterTransformsStringAccordingToRules(): void
self::assertSame('Foo-Bar/baz-Bat.phtml', $filtered);
}
- public function testTargetReplacementIdentifierAccessorsWork(): void
+ public function testInputWithNonStringKeysIsIgnored(): void
{
- self::assertSame(':', $this->inflector->getTargetReplacementIdentifier());
- $this->inflector->setTargetReplacementIdentifier('?=');
- self::assertSame('?=', $this->inflector->getTargetReplacementIdentifier());
+ $filter = self::withOptions([
+ 'target' => ':controller/:action.:suffix',
+ 'rules' => [
+ ':controller' => [CamelCaseToDash::class],
+ ':action' => [CamelCaseToDash::class],
+ 'suffix' => 'phtml',
+ ],
+ ]);
+
+ $filtered = $filter([
+ 'controller' => 'FooBar',
+ 0 => 'bing',
+ 'action' => 99,
+ ]);
+ self::assertSame('Foo-Bar/99.phtml', $filtered);
}
public function testTargetReplacementIdentifierWorksWhenInflected(): void
{
- $inflector = new InflectorFilter(
- '?=##controller/?=##action.?=##suffix',
- [
+ $filter = self::withOptions([
+ 'target' => '?=##controller/?=##action.?=##suffix',
+ 'rules' => [
':controller' => [CamelCaseToDash::class],
':action' => [CamelCaseToDash::class],
'suffix' => 'phtml',
],
- null,
- '?=##'
- );
+ 'targetReplacementIdentifier' => '?=##',
+ ]);
- $filtered = $inflector([
+ $filtered = $filter->__invoke([
'controller' => 'FooBar',
'action' => 'bazBat',
]);
@@ -243,123 +196,57 @@ public function testTargetReplacementIdentifierWorksWhenInflected(): void
self::assertSame('Foo-Bar/baz-Bat.phtml', $filtered);
}
- public function testThrowTargetExceptionsAccessorsWork(): void
- {
- self::assertSame(':', $this->inflector->getTargetReplacementIdentifier());
- $this->inflector->setTargetReplacementIdentifier('?=');
- self::assertSame('?=', $this->inflector->getTargetReplacementIdentifier());
- }
-
- public function testThrowTargetExceptionsOnAccessorsWork(): void
- {
- self::assertTrue($this->inflector->isThrowTargetExceptionsOn());
- $this->inflector->setThrowTargetExceptionsOn(false);
- self::assertFalse($this->inflector->isThrowTargetExceptionsOn());
- }
-
public function testTargetExceptionThrownWhenTargetSourceNotSatisfied(): void
{
- $inflector = new InflectorFilter(
- '?=##controller/?=##action.?=##suffix',
- [
+ $filter = self::withOptions([
+ 'target' => '?=##controller/?=##action.?=##suffix',
+ 'rules' => [
':controller' => [CamelCaseToDash::class],
':action' => [CamelCaseToDash::class],
'suffix' => 'phtml',
],
- true,
- '?=##'
- );
+ 'targetReplacementIdentifier' => '?=##',
+ ]);
- $this->expectException(Exception\RuntimeException::class);
+ $this->expectException(RuntimeException::class);
$this->expectExceptionMessage('perhaps a rule was not satisfied');
- $filtered = $inflector(['controller' => 'FooBar']);
+ $filter->filter(['controller' => 'FooBar']);
}
- public function testTargetExceptionNotThrownOnIdentifierNotFollowedByCharacter(): void
+ public function testTargetExceptionsCanBeDisabled(): void
{
- $inflector = new InflectorFilter(
- 'e:\path\to\:controller\:action.:suffix',
- [
- ':controller' => [CamelCaseToDash::class, StringToLower::class],
+ $filter = self::withOptions([
+ 'target' => ':controller/:action.:suffix',
+ 'rules' => [
+ ':controller' => [CamelCaseToDash::class],
':action' => [CamelCaseToDash::class],
'suffix' => 'phtml',
],
- true,
- ':'
- );
+ 'throwTargetExceptionsOn' => false,
+ ]);
- $filtered = $inflector(['controller' => 'FooBar', 'action' => 'MooToo']);
- self::assertSame($filtered, 'e:\path\to\foo-bar\Moo-Too.phtml');
+ self::assertSame(
+ 'Foo-Bar/:action.phtml',
+ $filter->filter(['controller' => 'FooBar']),
+ );
}
- /**
- * @return array
- */
- public function getOptions(): array
+ public function testTargetExceptionNotThrownOnIdentifierNotFollowedByCharacter(): void
{
- return [
- 'target' => '$controller/$action.$suffix',
- 'throwTargetExceptionsOn' => true,
- 'targetReplacementIdentifier' => '$',
- 'rules' => [
- ':controller' => [
- 'rule1' => CamelCaseToUnderscore::class,
- 'rule2' => StringToLower::class,
- ],
- ':action' => [
- 'rule1' => CamelCaseToDash::class,
- 'rule2' => StringToUpper::class,
- ],
- 'suffix' => 'php',
+ $filter = self::withOptions([
+ 'target' => 'e:\path\to\:controller\:action.:suffix',
+ 'rules' => [
+ ':controller' => [CamelCaseToDash::class, StringToLower::class],
+ ':action' => [CamelCaseToDash::class],
+ 'suffix' => 'phtml',
],
- ];
- }
-
- /**
- * This method returns an ArrayObject instance in place of a
- * Laminas\Config\Config instance; the two are interchangeable, as inflectors
- * consume the more general array or Traversable types.
- */
- public function getConfig(): ArrayObject
- {
- $options = $this->getOptions();
-
- return new ArrayObject($options);
- }
-
- // @codingStandardsIgnoreStart
- protected function _testOptions($inflector)
- {
- // @codingStandardsIgnoreEnd
- $options = $this->getOptions();
- $broker = $inflector->getPluginManager();
- self::assertSame($options['target'], $inflector->getTarget());
-
- self::assertInstanceOf(FilterPluginManager::class, $broker);
- self::assertTrue($inflector->isThrowTargetExceptionsOn());
- self::assertSame($options['targetReplacementIdentifier'], $inflector->getTargetReplacementIdentifier());
-
- $rules = $inflector->getRules();
- /** @psalm-suppress MixedArrayAccess */
- foreach (array_values($options['rules'][':controller']) as $key => $rule) {
- $class = $rules['controller'][$key]::class;
- self::assertStringContainsString($rule, $class);
- }
- /** @psalm-suppress MixedArrayAccess */
- foreach (array_values($options['rules'][':action']) as $key => $rule) {
- $class = $rules['action'][$key]::class;
- self::assertStringContainsString($rule, $class);
- }
- /** @psalm-suppress MixedArrayAccess */
- self::assertSame($options['rules']['suffix'], $rules['suffix']);
- }
+ 'throwTargetExceptionsOn' => true,
+ ]);
- public function testSetConfigSetsStateAndRules(): void
- {
- $config = $this->getConfig();
- $inflector = new InflectorFilter();
- $inflector->setOptions($config);
- $this->_testOptions($inflector);
+ self::assertSame(
+ 'e:\path\to\foo-bar\Moo-Too.phtml',
+ $filter->filter(['controller' => 'FooBar', 'action' => 'MooToo']),
+ );
}
/**
@@ -369,108 +256,112 @@ public function testSetConfigSetsStateAndRules(): void
*/
public function testCheckInflectorWithPregBackreferenceLikeParts(): void
{
- $inflector = new InflectorFilter(
- ':moduleDir' . DIRECTORY_SEPARATOR . ':controller' . DIRECTORY_SEPARATOR . ':action.:suffix',
- [
+ $filter = self::withOptions([
+ 'target' => ':moduleDir' . DIRECTORY_SEPARATOR . ':controller' . DIRECTORY_SEPARATOR . ':action.:suffix',
+ 'rules' => [
+ 'moduleDir' => 'C:\htdocs\public\cache\00\01\42\app\modules',
':controller' => [CamelCaseToDash::class, StringToLower::class],
':action' => [CamelCaseToDash::class],
'suffix' => 'phtml',
],
- true,
- ':'
- );
-
- $inflector->setStaticRule('moduleDir', 'C:\htdocs\public\cache\00\01\42\app\modules');
-
- $filtered = $inflector([
- 'controller' => 'FooBar',
- 'action' => 'MooToo',
]);
+
self::assertSame(
- $filtered,
'C:\htdocs\public\cache\00\01\42\app\modules'
. DIRECTORY_SEPARATOR
. 'foo-bar'
. DIRECTORY_SEPARATOR
- . 'Moo-Too.phtml'
+ . 'Moo-Too.phtml',
+ $filter->filter([
+ 'controller' => 'FooBar',
+ 'action' => 'MooToo',
+ ]),
);
}
- /**
- * @issue Laminas-2522
- */
- public function testTestForFalseInConstructorParams(): void
- {
- $inflector = new InflectorFilter('something', [], false, false);
- self::assertFalse($inflector->isThrowTargetExceptionsOn());
- self::assertSame($inflector->getTargetReplacementIdentifier(), ':');
-
- new InflectorFilter('something', [], false, '#');
- }
-
/**
* @issue Laminas-2964
*/
public function testNoInflectableTarget(): void
{
- $inflector = new InflectorFilter('abc');
- $inflector->addRules([':foo' => []]);
- self::assertSame($inflector(['fo' => 'bar']), 'abc');
+ $inflector = self::withOptions([
+ 'target' => 'abc',
+ 'rules' => [':foo' => []],
+ ]);
+
+ self::assertSame($inflector(['any' => 'thing']), 'abc');
}
- /**
- * @issue Laminas-7544
- */
- public function testAddFilterRuleMultipleTimes(): void
+ public static function unFilterableInput(): array
{
- $rules = $this->inflector->getRules();
- self::assertSame(0, count($rules));
- $this->inflector->setFilterRule('controller', StringToLower::class);
- $rules = $this->inflector->getRules('controller');
- self::assertSame(1, count($rules));
- $this->inflector->addFilterRule('controller', [TestAsset\Alpha::class, StringToLower::class]);
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress PossiblyFalseArgument */
- self::assertSame(3, count($rules));
- $context = StringToLower::class;
- $this->inflector->setStaticRuleReference('context', $context);
- $this->inflector->addFilterRule('controller', [TestAsset\Alpha::class, StringToLower::class]);
- $rules = $this->inflector->getRules('controller');
- /** @psalm-suppress PossiblyFalseArgument */
- self::assertSame(5, count($rules));
+ return [
+ ['Foo'],
+ [1],
+ [1.23],
+ [true],
+ [null],
+ ];
}
- #[Group('Laminas-8997')]
- public function testPassingArrayToConstructorSetsStateAndRules(): void
+ #[DataProvider('unFilterableInput')]
+ public function testOnlyArraysCanBeFiltered(mixed $input): void
{
- $options = $this->getOptions();
- $inflector = new InflectorFilter($options);
- $this->_testOptions($inflector);
+ $filter = self::withOptions([
+ 'target' => 'abc',
+ ]);
+
+ self::assertSame($input, $filter->filter($input));
}
- #[Group('Laminas-8997')]
- public function testPassingArrayToSetConfigSetsStateAndRules(): void
+ public function testObjectPropertiesAreExtractedAsFilterSubject(): void
{
- $options = $this->getOptions();
- $inflector = new InflectorFilter();
- $inflector->setOptions($options);
- $this->_testOptions($inflector);
+ $filter = self::withOptions([
+ 'target' => '/:controller/:action.:suffix',
+ 'rules' => [
+ ':controller' => [CamelCaseToDash::class, StringToLower::class],
+ ':action' => [CamelCaseToDash::class, StringToLower::class],
+ 'suffix' => 'phtml',
+ ],
+ ]);
+
+ $value = new class () {
+ public string $controller = 'MyController';
+ public string $action = 'SomeAction';
+ public string $suffix = 'php';
+ };
+
+ self::assertSame(
+ '/my-controller/some-action.php',
+ $filter->filter($value),
+ );
}
- #[Group('Laminas-8997')]
- public function testPassingConfigObjectToConstructorSetsStateAndRules(): void
+ public function testThatYouCannotUseAColonInTheTargetByDefault(): void
{
- $config = $this->getConfig();
- $inflector = new InflectorFilter($config);
- $this->_testOptions($inflector);
+ $filter = self::withOptions([
+ 'target' => '::something',
+ 'rules' => [
+ 'something' => 'foo',
+ ],
+ ]);
+
+ $this->expectException(RuntimeException::class);
+ $filter->filter([]);
}
- #[Group('Laminas-8997')]
- public function testPassingConfigObjectToSetConfigSetsStateAndRules(): void
+ public function testThatYouCanUseAColonInTheTargetWhenTheDelimiterIsSet(): void
{
- $config = $this->getConfig();
- $inflector = new InflectorFilter();
- $inflector->setOptions($config);
- $this->_testOptions($inflector);
+ $filter = self::withOptions([
+ 'target' => ':?something',
+ 'targetReplacementIdentifier' => '?',
+ 'rules' => [
+ 'something' => 'foo',
+ ],
+ ]);
+
+ self::assertSame(
+ ':foo',
+ $filter->filter([]),
+ );
}
}
diff --git a/test/TestAsset/Alpha.php b/test/TestAsset/Alpha.php
deleted file mode 100644
index 21f51cac..00000000
--- a/test/TestAsset/Alpha.php
+++ /dev/null
@@ -1,30 +0,0 @@
- */
-class Alpha implements FilterInterface
-{
- /** @inheritDoc */
- public function filter(mixed $value): mixed
- {
- if (! is_string($value)) {
- return $value;
- }
-
- return preg_replace('/[^a-zA-Z\s]/', '', $value);
- }
-
- /** @inheritDoc */
- public function __invoke(mixed $value): mixed
- {
- return $this->filter($value);
- }
-}
diff --git a/test/TestAsset/InMemoryContainer.php b/test/TestAsset/InMemoryContainer.php
new file mode 100644
index 00000000..ec69bc7e
--- /dev/null
+++ b/test/TestAsset/InMemoryContainer.php
@@ -0,0 +1,39 @@
+ */
+ private array $services = [];
+
+ /** @inheritDoc */
+ public function get(string $id): mixed
+ {
+ if (! array_key_exists($id, $this->services)) {
+ throw new class ($id . ' was not found') extends RuntimeException implements NotFoundExceptionInterface {
+ };
+ }
+
+ return $this->services[$id];
+ }
+
+ /** @inheritDoc */
+ public function has(string $id): bool
+ {
+ return array_key_exists($id, $this->services);
+ }
+
+ public function set(string $id, mixed $item): void
+ {
+ $this->services[$id] = $item;
+ }
+}