diff --git a/composer.json b/composer.json index 0f6337f66..de3278363 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,6 @@ "test": "phpunit --exclude-group ''" }, "extra": { - "version": "2.16.1-dev" + "version": "2.17.0-dev" } } diff --git a/docs/testdox.txt b/docs/testdox.txt index 026e50109..bb9b3e893 100644 --- a/docs/testdox.txt +++ b/docs/testdox.txt @@ -198,13 +198,14 @@ Attribute Collection (s9e\TextFormatter\Tests\Configurator\Collections\Attribute [x] Throws an exception when accessing an Attribute that does not exist Attribute Filter Chain (s9e\TextFormatter\Tests\Configurator\Collections\AttributeFilterChain) - [x] append() throws an InvalidArgumentException on invalid callbacks - [x] prepend() throws an InvalidArgumentException on invalid callbacks + [x] append() throws a RuntimeException on invalid callbacks + [x] prepend() throws a RuntimeException on invalid callbacks [x] append() throws an InvalidArgumentException on uncallable callbacks [x] prepend() throws an InvalidArgumentException on uncallable callbacks [x] PHP string callbacks are normalized to an instance of AttributeFilter [x] PHP array callbacks are normalized to an instance of AttributeFilter [x] Default filters such as "#int" are normalized to an instance of the corresponding AttributeFilter + [x] Default filters accept positional constructor arguments [x] Instances of AttributeFilter are added as-is [x] Automatically parses callback parameters @@ -458,8 +459,8 @@ Tag Collection (s9e\TextFormatter\Tests\Configurator\Collections\TagCollection) [x] Throws an exception when accessing a Tag that does not exist Tag Filter Chain (s9e\TextFormatter\Tests\Configurator\Collections\TagFilterChain) - [x] append() throws an InvalidArgumentException on invalid callbacks - [x] prepend() throws an InvalidArgumentException on invalid callbacks + [x] append() throws a RuntimeException on invalid callbacks + [x] prepend() throws a RuntimeException on invalid callbacks [x] append() throws an InvalidArgumentException on uncallable callbacks [x] prepend() throws an InvalidArgumentException on uncallable callbacks [x] PHP string callbacks are normalized to an instance of TagFilter diff --git a/src/Configurator/Collections/AttributeFilterChain.php b/src/Configurator/Collections/AttributeFilterChain.php index 94c4aeeb8..5d222e02c 100644 --- a/src/Configurator/Collections/AttributeFilterChain.php +++ b/src/Configurator/Collections/AttributeFilterChain.php @@ -7,29 +7,20 @@ */ namespace s9e\TextFormatter\Configurator\Collections; +use Override; +use s9e\TextFormatter\Configurator\Items\Filter; + class AttributeFilterChain extends FilterChain { - /** - * {@inheritdoc} - */ - public function getFilterClassName() + #[Override] + protected function getDefaultFilter(string $filterName, array $constructorArgs = []): Filter { - return 's9e\\TextFormatter\\Configurator\\Items\\AttributeFilter'; + return AttributeFilterCollection::getDefaultFilter($filterName, $constructorArgs); } - /** - * Normalize a value into an AttributeFilter instance - * - * @param mixed $value Either a valid callback or an instance of AttributeFilter - * @return \s9e\TextFormatter\Configurator\Items\AttributeFilter Normalized filter - */ - public function normalizeValue($value) + #[Override] + public function getFilterClassName() { - if (is_string($value) && preg_match('(^#\\w+$)', $value)) - { - $value = AttributeFilterCollection::getDefaultFilter(substr($value, 1)); - } - - return parent::normalizeValue($value); + return 's9e\\TextFormatter\\Configurator\\Items\\AttributeFilter'; } } \ No newline at end of file diff --git a/src/Configurator/Collections/FilterChain.php b/src/Configurator/Collections/FilterChain.php index 81227c97b..ad48fb1ee 100644 --- a/src/Configurator/Collections/FilterChain.php +++ b/src/Configurator/Collections/FilterChain.php @@ -8,6 +8,7 @@ namespace s9e\TextFormatter\Configurator\Collections; use InvalidArgumentException; +use RuntimeException; use s9e\TextFormatter\Configurator\Helpers\FilterHelper; use s9e\TextFormatter\Configurator\Items\Filter; use s9e\TextFormatter\Configurator\Items\ProgrammableCallback; @@ -51,23 +52,33 @@ public function containsCallback(callable $callback) */ public function normalizeValue($value) { - if (is_string($value) && strpos($value, '(') !== false) - { - return $this->createFilter($value); - } - $className = $this->getFilterClassName(); if ($value instanceof $className) { return $value; } - if (!is_callable($value)) + if (is_callable($value)) { - throw new InvalidArgumentException('Filter ' . var_export($value, true) . ' is neither callable nor an instance of ' . $className); + return new $className($value); } - return new $className($value); + if (is_string($value)) + { + return $this->createFilter($value); + } + + throw new InvalidArgumentException('Filter ' . var_export($value, true) . ' is neither callable nor an instance of ' . $className); + } + + protected function createDefaultFilter(string $filterString) + { + $config = FilterHelper::parse($filterString); + + $constructorArgs = $this->getConstructorArgsFromFilterParams($config['params'] ?? []); + $filterName = substr($config['filter'], 1); + + return $this->getDefaultFilter($filterName, $constructorArgs); } /** @@ -78,6 +89,11 @@ public function normalizeValue($value) */ protected function createFilter($filterString) { + if ($filterString[0] === '#') + { + return $this->createDefaultFilter($filterString); + } + $config = FilterHelper::parse($filterString); $filter = $this->normalizeValue($config['filter']); if (isset($config['params'])) @@ -92,4 +108,38 @@ protected function createFilter($filterString) return $filter; } + + protected function getConstructorArgsFromFilterParams(array $params): array + { + $constructorArgs = []; + foreach ($params as $i => $param) + { + [$type, $value] = $param; + if ($type !== 'Value') + { + throw new RuntimeException('Cannot use default filter with named parameters'); + } + + if (isset($param[2])) + { + // Named argument + $constructorArgs[$param[2]] = $value; + } + else + { + // Positional argument + $constructorArgs[] = $value; + } + } + + return $constructorArgs; + } + + /** + * Get an instance of a default filter + */ + protected function getDefaultFilter(string $filterName, array $constructorArgs = []): Filter + { + throw new RuntimeException(get_class($this) . ' has no default filters'); + } } \ No newline at end of file diff --git a/tests/Configurator/Collections/AttributeFilterChainTest.php b/tests/Configurator/Collections/AttributeFilterChainTest.php index 627eab56d..619f50c0d 100644 --- a/tests/Configurator/Collections/AttributeFilterChainTest.php +++ b/tests/Configurator/Collections/AttributeFilterChainTest.php @@ -17,24 +17,24 @@ private function privateMethod() {} public function doNothing() {} /** - * @testdox append() throws an InvalidArgumentException on invalid callbacks + * @testdox append() throws a RuntimeException on invalid callbacks */ public function testAppendInvalidCallback() { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage("Filter '*invalid*' is neither callable nor an instance of s9e\\TextFormatter\\Configurator\\Items\\AttributeFilter"); + $this->expectException('RuntimeException'); + $this->expectExceptionMessage("Cannot parse '*invalid*'"); $filterChain = new AttributeFilterChain; $filterChain->append('*invalid*'); } /** - * @testdox prepend() throws an InvalidArgumentException on invalid callbacks + * @testdox prepend() throws a RuntimeException on invalid callbacks */ public function testPrependInvalidCallback() { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage("Filter '*invalid*' is neither callable nor an instance of s9e\\TextFormatter\\Configurator\\Items\\AttributeFilter"); + $this->expectException('RuntimeException'); + $this->expectExceptionMessage("Cannot parse '*invalid*'"); $filterChain = new AttributeFilterChain; $filterChain->prepend('*invalid*'); @@ -103,6 +103,28 @@ public function testDefaultFilter() ); } + /** + * @testdox Default filters accept positional constructor arguments + */ + public function testDefaultFilterPositionalConstructorArguments() + { + $filterChain = new AttributeFilterChain; + $filter = $filterChain->append('#range(1, 12)'); + + $this->assertEquals(['min' => 1, 'max' => 12], $filter->getVars()); + } + +// /** +// * @testdox Default filters accept named constructor arguments +// */ +// public function testDefaultFilterNamedConstructorArguments() +// { +// $filterChain = new AttributeFilterChain; +// $filter = $filterChain->append('#range(max: 12, min: 3)'); +// +// $this->assertEquals(['min' => 3, 'max' => 12], $filter->getVars()); +// } + /** * @testdox Instances of AttributeFilter are added as-is */ diff --git a/tests/Configurator/Collections/TagFilterChainTest.php b/tests/Configurator/Collections/TagFilterChainTest.php index 3d7a11cb8..4596c40dc 100644 --- a/tests/Configurator/Collections/TagFilterChainTest.php +++ b/tests/Configurator/Collections/TagFilterChainTest.php @@ -17,24 +17,24 @@ private function privateMethod() {} public function doNothing() {} /** - * @testdox append() throws an InvalidArgumentException on invalid callbacks + * @testdox append() throws a RuntimeException on invalid callbacks */ public function testAppendInvalidCallback() { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage("Filter '*invalid*' is neither callable nor an instance of s9e\\TextFormatter\\Configurator\\Items\\TagFilter"); + $this->expectException('RuntimeException'); + $this->expectExceptionMessage("Cannot parse '*invalid*'"); $filterChain = new TagFilterChain; $filterChain->append('*invalid*'); } /** - * @testdox prepend() throws an InvalidArgumentException on invalid callbacks + * @testdox prepend() throws a RuntimeException on invalid callbacks */ public function testPrependInvalidCallback() { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage("Filter '*invalid*' is neither callable nor an instance of s9e\\TextFormatter\\Configurator\\Items\\TagFilter"); + $this->expectException('RuntimeException'); + $this->expectExceptionMessage("Cannot parse '*invalid*'"); $filterChain = new TagFilterChain; $filterChain->prepend('*invalid*');