From 138c940f401a664abb26c14b383d142141d5e93a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2017 10:14:46 -0600 Subject: [PATCH 1/6] Allow processing class constants This patch updates the `Constant` processor to allow processing class constants, including the `::class` pseudo-constant. To allow this to work, I've changed the visibility of `Token::doProcess()` from `private` to `protected`, to allow overriding the method within the `Constant` class. I've also added two new tests in the new test case `ZendTest\Config\Processor\Constant` to cover the behavior. --- src/Processor/Constant.php | 38 +++++++++++++++++++++++++++++ src/Processor/Token.php | 2 +- test/Processor/ConstantTest.php | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 test/Processor/ConstantTest.php diff --git a/src/Processor/Constant.php b/src/Processor/Constant.php index 0e6025e..81cb735 100644 --- a/src/Processor/Constant.php +++ b/src/Processor/Constant.php @@ -79,4 +79,42 @@ public function getTokens() { return $this->tokens; } + + /** + * Override processing of individual value. + * + * If the value is a string and evaluates to a class constant, returns + * the class constant value; otherwise, delegates to the parent. + * + * @param mixed $value + * @param array $replacements + * @return mixed + */ + protected function doProcess($value, array $replacements) + { + if (! is_string($value)) { + return parent::doProcess($value, $replacements); + } + + if (false === strpos($value, '::')) { + return parent::doProcess($value, $replacements); + } + + // Handle class constants + if (defined($value)) { + return constant($value); + } + + // Handle ::class notation + if (! preg_match('/::class$/', $value)) { + return parent::doProcess($value, $replacements); + } + + $class = substr($value, 0, strlen($value) - 7); + if (class_exists($class)) { + return $class; + } + + return parent::doProcess($value, $replacements); + } } diff --git a/src/Processor/Token.php b/src/Processor/Token.php index d679c6d..0dab200 100644 --- a/src/Processor/Token.php +++ b/src/Processor/Token.php @@ -231,7 +231,7 @@ public function processValue($value) * * @throws Exception\InvalidArgumentException if the provided value is a read-only {@see Config} */ - private function doProcess($value, array $replacements) + protected function doProcess($value, array $replacements) { if ($value instanceof Config) { if ($value->isReadOnly()) { diff --git a/test/Processor/ConstantTest.php b/test/Processor/ConstantTest.php new file mode 100644 index 0000000..fdd2dd2 --- /dev/null +++ b/test/Processor/ConstantTest.php @@ -0,0 +1,43 @@ + __CLASS__ . '::CONFIG_TEST', + ], true); + + $processor = new ConstantProcessor(); + $processor->process($config); + + $this->assertEquals(self::CONFIG_TEST, $config->get('test')); + } + + public function testCanResolveClassPseudoConstant() + { + $key = __CLASS__ . '::CONFIG_TEST'; + $config = new Config([ + 'test' => __CLASS__ . '::class', + ], true); + + $processor = new ConstantProcessor(); + $processor->process($config); + + $this->assertEquals(self::class, $config->get('test')); + } +} From 4a7c77dfa5de03a247b14eaec0c34a80778c7576 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2017 10:27:14 -0600 Subject: [PATCH 2/6] Allow constant and token value subsitutions in keys This patch updates the `Token` processor, and, by extension, the `Constant` processor, to allow substituting tokens/constants found in *key* values. This enables the following: ```json { "Acme\\Component::CONFIG_KEY": { "dependencies": { "Acme\\Middleware::class": "Acme\\MiddlewareFactory::class" } } } ``` --- src/Processor/Token.php | 1 + test/Processor/ConstantTest.php | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Processor/Token.php b/src/Processor/Token.php index 0dab200..da91482 100644 --- a/src/Processor/Token.php +++ b/src/Processor/Token.php @@ -239,6 +239,7 @@ protected function doProcess($value, array $replacements) } foreach ($value as $key => $val) { + $key = $this->doProcess($key, $replacements); $value->$key = $this->doProcess($val, $replacements); } diff --git a/test/Processor/ConstantTest.php b/test/Processor/ConstantTest.php index fdd2dd2..19e576e 100644 --- a/test/Processor/ConstantTest.php +++ b/test/Processor/ConstantTest.php @@ -40,4 +40,46 @@ public function testCanResolveClassPseudoConstant() $this->assertEquals(self::class, $config->get('test')); } + + public function testCanProcessConstantValuesInKeys() + { + if (! defined('ZEND_CONFIG_PROCESSOR_CONSTANT_TEST')) { + define('ZEND_CONFIG_PROCESSOR_CONSTANT_TEST', 'test-key'); + } + + $config = new Config([ + 'ZEND_CONFIG_PROCESSOR_CONSTANT_TEST' => 'value', + ], true); + + $processor = new ConstantProcessor(); + $processor->process($config); + + $this->assertEquals('value', $config->get(ZEND_CONFIG_PROCESSOR_CONSTANT_TEST)); + } + + public function testCanProcessClassConstantValuesInKeys() + { + $key = __CLASS__ . '::CONFIG_TEST'; + $config = new Config([ + $key => 'value', + ], true); + + $processor = new ConstantProcessor(); + $processor->process($config); + + $this->assertEquals('value', $config->get(self::CONFIG_TEST)); + } + + public function testCanProcessPseudoClassConstantValuesInKeys() + { + $key = __CLASS__ . '::class'; + $config = new Config([ + $key => 'value', + ], true); + + $processor = new ConstantProcessor(); + $processor->process($config); + + $this->assertEquals('value', $config->get(self::class)); + } } From 53e616b80c983d5b06fe5ef5c4ddb0e449d76f84 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2017 11:35:45 -0600 Subject: [PATCH 3/6] Make key processing opt-in Prevents BC break, as the original behavior, which does not process keys, is the default. Call `$processor->enableKeyProcessing()` to enable it. --- src/Processor/Constant.php | 2 +- src/Processor/Token.php | 26 +++++++++- test/Processor/ConstantTest.php | 88 ++++++++++++++++----------------- 3 files changed, 68 insertions(+), 48 deletions(-) diff --git a/src/Processor/Constant.php b/src/Processor/Constant.php index 81cb735..de6ebe7 100644 --- a/src/Processor/Constant.php +++ b/src/Processor/Constant.php @@ -106,7 +106,7 @@ protected function doProcess($value, array $replacements) } // Handle ::class notation - if (! preg_match('/::class$/', $value)) { + if (! preg_match('/::class$/i', $value)) { return parent::doProcess($value, $replacements); } diff --git a/src/Processor/Token.php b/src/Processor/Token.php index da91482..5972686 100644 --- a/src/Processor/Token.php +++ b/src/Processor/Token.php @@ -20,6 +20,13 @@ class Token implements ProcessorInterface */ protected $prefix = ''; + /** + * Whether or not to process keys as well as values. + * + * @var bool + */ + protected $processKeys = false; + /** * Token suffix. * @@ -170,6 +177,16 @@ public function setToken($token, $value) return $this->addToken($token, $value); } + /** + * Enable processing keys as well as values. + * + * @return void + */ + public function enableKeyProcessing() + { + $this->processKeys = true; + } + /** * Build replacement map * @@ -239,8 +256,13 @@ protected function doProcess($value, array $replacements) } foreach ($value as $key => $val) { - $key = $this->doProcess($key, $replacements); - $value->$key = $this->doProcess($val, $replacements); + $newKey = $this->processKeys ? $this->doProcess($key, $replacements) : $key; + $value->$newKey = $this->doProcess($val, $replacements); + + // If the processed key differs from the original, remove the original + if ($newKey !== $key) { + unset($value->$key); + } } return $value; diff --git a/test/Processor/ConstantTest.php b/test/Processor/ConstantTest.php index 19e576e..62cde5d 100644 --- a/test/Processor/ConstantTest.php +++ b/test/Processor/ConstantTest.php @@ -15,71 +15,69 @@ class ConstantTest extends TestCase { const CONFIG_TEST = 'config'; - public function testCanResolveClassConstants() - { - $key = __CLASS__ . '::CONFIG_TEST'; - $config = new Config([ - 'test' => __CLASS__ . '::CONFIG_TEST', - ], true); - - $processor = new ConstantProcessor(); - $processor->process($config); - - $this->assertEquals(self::CONFIG_TEST, $config->get('test')); - } - - public function testCanResolveClassPseudoConstant() - { - $key = __CLASS__ . '::CONFIG_TEST'; - $config = new Config([ - 'test' => __CLASS__ . '::class', - ], true); - - $processor = new ConstantProcessor(); - $processor->process($config); - - $this->assertEquals(self::class, $config->get('test')); - } - - public function testCanProcessConstantValuesInKeys() + public function constantProvider() { if (! defined('ZEND_CONFIG_PROCESSOR_CONSTANT_TEST')) { define('ZEND_CONFIG_PROCESSOR_CONSTANT_TEST', 'test-key'); } - $config = new Config([ - 'ZEND_CONFIG_PROCESSOR_CONSTANT_TEST' => 'value', - ], true); + // @codingStandardsIgnoreStart + // constantString, constantValue + return [ + 'constant' => ['ZEND_CONFIG_PROCESSOR_CONSTANT_TEST', ZEND_CONFIG_PROCESSOR_CONSTANT_TEST], + 'class-constant' => [__CLASS__ . '::CONFIG_TEST', self::CONFIG_TEST], + 'class-pseudo-constant' => [__CLASS__ . '::class', self::class], + 'class-pseudo-constant-upper' => [__CLASS__ . '::CLASS', self::class], + ]; + // @codingStandardsIgnoreEnd + } + + /** + * @dataProvider constantProvider + * + * @param string $constantString + * @param string $constantValue + */ + public function testCanResolveConstantValues($constantString, $constantValue) + { + $config = new Config(['test' => $constantString], true); $processor = new ConstantProcessor(); $processor->process($config); - $this->assertEquals('value', $config->get(ZEND_CONFIG_PROCESSOR_CONSTANT_TEST)); + $this->assertEquals($constantValue, $config->get('test')); } - public function testCanProcessClassConstantValuesInKeys() + /** + * @dataProvider constantProvider + * + * @param string $constantString + * @param string $constantValue + */ + public function testWillNotProcessConstantValuesInKeysByDefault($constantString, $constantValue) { - $key = __CLASS__ . '::CONFIG_TEST'; - $config = new Config([ - $key => 'value', - ], true); - + $config = new Config([$constantString => 'value'], true); $processor = new ConstantProcessor(); $processor->process($config); - $this->assertEquals('value', $config->get(self::CONFIG_TEST)); + $this->assertNotEquals('value', $config->get($constantValue)); + $this->assertEquals('value', $config->get($constantString)); } - public function testCanProcessPseudoClassConstantValuesInKeys() + /** + * @dataProvider constantProvider + * + * @param string $constantString + * @param string $constantValue + */ + public function testCanProcessConstantValuesInKeys($constantString, $constantValue) { - $key = __CLASS__ . '::class'; - $config = new Config([ - $key => 'value', - ], true); - + $config = new Config([$constantString => 'value'], true); $processor = new ConstantProcessor(); + $processor->enableKeyProcessing(); $processor->process($config); - $this->assertEquals('value', $config->get(self::class)); + $this->assertEquals('value', $config->get($constantValue)); + $this->assertNotEquals('value', $config->get($constantString)); } } From 0e450f9679aea8175c4b0d8ec25bff11d6ed989f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2017 11:37:33 -0600 Subject: [PATCH 4/6] Return the value verbatim if it does not resolve to a class --- src/Processor/Constant.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Processor/Constant.php b/src/Processor/Constant.php index de6ebe7..4fbcb4d 100644 --- a/src/Processor/Constant.php +++ b/src/Processor/Constant.php @@ -115,6 +115,10 @@ protected function doProcess($value, array $replacements) return $class; } - return parent::doProcess($value, $replacements); + // While we've matched ::class, the class does not exist, and PHP will + // raise an error if you try to define a constant using that notation. + // As such, we have something that cannot possibly be a constant, so we + // can safely return the value verbatim. + return $value; } } From 1955e338332a31fbc9a86094d90049d2ef8edf10 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2017 11:38:50 -0600 Subject: [PATCH 5/6] Document new constant features - `Constant` processor now also recognizes class and `::class` constants. - `Constant` and `Token` processors now allow optionally processing keys. --- doc/book/processor.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc/book/processor.md b/doc/book/processor.md index df9a9b6..ecdd5f4 100644 --- a/doc/book/processor.md +++ b/doc/book/processor.md @@ -12,6 +12,22 @@ zend-config provides the following concrete implementations: - `Zend\Config\Processor\Token`: find and replace specific tokens. - `Zend\Config\Processor\Translator`: translate configuration values in other languages using `Zend\I18n\Translator`. +> ### What gets processed? +> +> Typically, you will process configuration _values_. However, there are use +> cases for supplying constant and/or token _keys_; one common one is for +> using class-based constants as keys to avoid using magic "strings": +> +> ```json +> { +> "Acme\\Compoment::CONFIG_KEY": {} +> } +> ``` +> +> As such, as of version 3.1.0, the `Constant` and `Token` processors can +> optionally also process the keys of the `Config` instance provided to them, by +> calling `enableKeyProcessing()` on their instances. + ## Zend\\Config\\Processor\\Constant ### Using Zend\\Config\\Processor\\Constant @@ -32,6 +48,14 @@ echo $config->foo; This example returns the output: `TEST_CONST,bar`. +As of version 3.1.0, you can also tell the `Constant` processor to process keys: + +```php +$processor->enableKeyProcessing(); +``` + +When enabled, any constant values found in keys will also be replaced. + ## Zend\\Config\\Processor\\Filter ### Using Zend\\Config\\Processor\\Filter @@ -107,6 +131,14 @@ echo $config->foo; This example returns the output: `Value is TOKEN,Value is bar`. +As of version 3.1.0, you can also tell the `Constant` processor to process keys: + +```php +$processor->enableKeyProcessing(); +``` + +When enabled, any token values found in keys will also be replaced. + ## Zend\\Config\\Processor\\Translator ### Using Zend\\Config\\Processor\\Translator From 092183951b825b4fa3923df872e066864809f81a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 22 Feb 2017 08:19:16 -0600 Subject: [PATCH 6/6] Allow enabling key processing via constructor arguments Added a new boolean argument to each of the `Token` and `Constant` processors, `$enableKeyProcessing`. Defaults to `false`, but when set to `true`, will enable key processing in each. This patch also adds type casting for optional arguments that are expected to be of known types. --- src/Processor/Constant.php | 22 ++++++++++++++-------- src/Processor/Token.php | 11 ++++++++--- test/Processor/ConstantTest.php | 12 ++++++++++++ test/Processor/TokenTest.php | 30 ++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 test/Processor/TokenTest.php diff --git a/src/Processor/Constant.php b/src/Processor/Constant.php index 4fbcb4d..6b7fa36 100644 --- a/src/Processor/Constant.php +++ b/src/Processor/Constant.php @@ -20,17 +20,23 @@ class Constant extends Token implements ProcessorInterface * Constant Processor walks through a Config structure and replaces all * PHP constants with their respective values * - * @param bool $userOnly True to process only user-defined constants, - * false to process all PHP constants - * @param string $prefix Optional prefix - * @param string $suffix Optional suffix + * @param bool $userOnly True to process only user-defined constants, + * false to process all PHP constants; defaults to true. + * @param string $prefix Optional prefix + * @param string $suffix Optional suffix + * @param bool $enableKeyProcessing Whether or not to enable processing of + * constant values in configuration keys; defaults to false. * @return \Zend\Config\Processor\Constant */ - public function __construct($userOnly = true, $prefix = '', $suffix = '') + public function __construct($userOnly = true, $prefix = '', $suffix = '', $enableKeyProcessing = false) { - $this->setUserOnly($userOnly); - $this->setPrefix($prefix); - $this->setSuffix($suffix); + $this->setUserOnly((bool) $userOnly); + $this->setPrefix((string) $prefix); + $this->setSuffix((string) $suffix); + + if (true === $enableKeyProcessing) { + $this->enableKeyProcessing(); + } $this->loadConstants(); } diff --git a/src/Processor/Token.php b/src/Processor/Token.php index 5972686..2310aba 100644 --- a/src/Processor/Token.php +++ b/src/Processor/Token.php @@ -56,12 +56,17 @@ class Token implements ProcessorInterface * value to replace it with * @param string $prefix * @param string $suffix + * @param bool $enableKeyProcessing Whether or not to enable processing of + * token values in configuration keys; defaults to false. */ - public function __construct($tokens = [], $prefix = '', $suffix = '') + public function __construct($tokens = [], $prefix = '', $suffix = '', $enableKeyProcessing = false) { $this->setTokens($tokens); - $this->setPrefix($prefix); - $this->setSuffix($suffix); + $this->setPrefix((string) $prefix); + $this->setSuffix((string) $suffix); + if (true === $enableKeyProcessing) { + $this->enableKeyProcessing(); + } } /** diff --git a/test/Processor/ConstantTest.php b/test/Processor/ConstantTest.php index 62cde5d..5e1397b 100644 --- a/test/Processor/ConstantTest.php +++ b/test/Processor/ConstantTest.php @@ -80,4 +80,16 @@ public function testCanProcessConstantValuesInKeys($constantString, $constantVal $this->assertEquals('value', $config->get($constantValue)); $this->assertNotEquals('value', $config->get($constantString)); } + + public function testKeyProcessingDisabledByDefault() + { + $processor = new ConstantProcessor(); + $this->assertAttributeSame(false, 'processKeys', $processor); + } + + public function testCanEnableKeyProcessingViaConstructorArgument() + { + $processor = new ConstantProcessor(true, '', '', true); + $this->assertAttributeSame(true, 'processKeys', $processor); + } } diff --git a/test/Processor/TokenTest.php b/test/Processor/TokenTest.php new file mode 100644 index 0000000..b49c770 --- /dev/null +++ b/test/Processor/TokenTest.php @@ -0,0 +1,30 @@ +assertAttributeSame(false, 'processKeys', $processor); + } + + public function testCanEnableKeyProcessingViaConstructorArgument() + { + $processor = new TokenProcessor([], '', '', true); + $this->assertAttributeSame(true, 'processKeys', $processor); + } +}