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..e9ba52e9 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\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->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,83 @@ 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' => []],
+ ]);
- /**
- * @issue Laminas-7544
- */
- public function testAddFilterRuleMultipleTimes(): void
- {
- $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));
+ self::assertSame($inflector(['any' => 'thing']), 'abc');
}
- #[Group('Laminas-8997')]
- public function testPassingArrayToConstructorSetsStateAndRules(): void
+ public static function unFilterableInput(): array
{
- $options = $this->getOptions();
- $inflector = new InflectorFilter($options);
- $this->_testOptions($inflector);
+ return [
+ ['Foo'],
+ [1],
+ [1.23],
+ [true],
+ [null],
+ ];
}
- #[Group('Laminas-8997')]
- public function testPassingArrayToSetConfigSetsStateAndRules(): void
+ #[DataProvider('unFilterableInput')]
+ public function testOnlyArraysCanBeFiltered(mixed $input): void
{
- $options = $this->getOptions();
- $inflector = new InflectorFilter();
- $inflector->setOptions($options);
- $this->_testOptions($inflector);
- }
+ $filter = self::withOptions([
+ 'target' => 'abc',
+ ]);
- #[Group('Laminas-8997')]
- public function testPassingConfigObjectToConstructorSetsStateAndRules(): void
- {
- $config = $this->getConfig();
- $inflector = new InflectorFilter($config);
- $this->_testOptions($inflector);
+ self::assertSame($input, $filter->filter($input));
}
- #[Group('Laminas-8997')]
- public function testPassingConfigObjectToSetConfigSetsStateAndRules(): void
+ public function testObjectPropertiesAreExtractedAsFilterSubject(): void
{
- $config = $this->getConfig();
- $inflector = new InflectorFilter();
- $inflector->setOptions($config);
- $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),
+ );
}
}
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;
+ }
+}