From 4e5c5aed9cffffbf3456e155f1b6cb763153c700 Mon Sep 17 00:00:00 2001 From: Dom Morgan Date: Wed, 12 Aug 2015 01:15:29 +0100 Subject: [PATCH] Add support for handlers where constructor params are objects. This is achieved by making the ClassLoader recursive. As a side effect it is necessary to convert constructor params to camelCase to support non PSR-2 compliant constructors (e.g. Raven_Client for RavenHandler). A couple of examples have been added for Raven and Redis. --- examples/dependency_config.yml | 22 +++++++++ examples/dependency_logger.php | 13 +++++ src/Config/Loader/ClassLoader.php | 21 +++++++++ .../Resolver/ConstructorResolver.php | 7 ++- .../Resolver/ConstructorResolverTest.php | 32 ++++++++----- tests/Config/Loader/ClassLoaderTest.php | 26 ++++++++++ tests/Fixtures/DependentClass.php | 47 +++++++++++++++++++ tests/Fixtures/SampleClass.php | 3 +- 8 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 examples/dependency_config.yml create mode 100644 examples/dependency_logger.php create mode 100644 tests/Fixtures/DependentClass.php diff --git a/examples/dependency_config.yml b/examples/dependency_config.yml new file mode 100644 index 0000000..ae430de --- /dev/null +++ b/examples/dependency_config.yml @@ -0,0 +1,22 @@ +--- +version: 1 + +disable_existing_loggers: false + +handlers: + sentry: + class: Monolog\Handler\RavenHandler + level: DEBUG + raven_client: + class: Raven_Client + options_or_dsn: https://something:dummy@getsentry.com/1 + redis: + class: Monolog\Handler\RedisHandler + level: DEBUG + key: cascade + redis: + class: Redis + connect: ['127.0.0.1', 6379] +loggers: + dependency: + handlers: [sentry, redis] diff --git a/examples/dependency_logger.php b/examples/dependency_logger.php new file mode 100644 index 0000000..1a0659f --- /dev/null +++ b/examples/dependency_logger.php @@ -0,0 +1,13 @@ +info('Well, that works!'); +Cascade::getLogger('dependency')->error('Maybe not...'); diff --git a/src/Config/Loader/ClassLoader.php b/src/Config/Loader/ClassLoader.php index 451e735..8eb466f 100644 --- a/src/Config/Loader/ClassLoader.php +++ b/src/Config/Loader/ClassLoader.php @@ -98,6 +98,25 @@ protected function setClass() unset($this->rawOptions['class']); } + /** + * Recursively loads objects into any of the rawOptions that represent + * a class + * + * @author Dom Morgan + */ + protected function loadChildClasses() + { + foreach ($this->rawOptions as &$option) { + if (is_array($option) + && array_key_exists('class', $option) + && class_exists($option['class']) + ) { + $classLoader = new ClassLoader($option); + $option = $classLoader->load(); + } + } + } + /** * Return option values indexed by name using camelCased keys * @@ -163,6 +182,8 @@ private function resolveOptions() */ public function load() { + $this->loadChildClasses(); + list($constructorResolvedOptions, $extraResolvedOptions) = $this->resolveOptions(); $instance = $this->reflected->newInstanceArgs($constructorResolvedOptions); diff --git a/src/Config/Loader/ClassLoader/Resolver/ConstructorResolver.php b/src/Config/Loader/ClassLoader/Resolver/ConstructorResolver.php index a6da063..c976f2b 100644 --- a/src/Config/Loader/ClassLoader/Resolver/ConstructorResolver.php +++ b/src/Config/Loader/ClassLoader/Resolver/ConstructorResolver.php @@ -11,6 +11,7 @@ namespace Cascade\Config\Loader\ClassLoader\Resolver; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; /** * Constructor Resolver. Pull args from the contructor and set up an option @@ -55,15 +56,19 @@ public function __construct(\ReflectionClass $reflected) /** * Fetches constructor args (array of ReflectionParameter) from the reflected class * and set them as an associative array + * + * Convert the parameter names to camelCase for classes that have contructor + * params defined in snake_case for consistency with the options */ public function initConstructorArgs() { $constructor = $this->reflected->getConstructor(); + $nameConverter = new CamelCaseToSnakeCaseNameConverter(); if (!is_null($constructor)) { // Index parameters by their names foreach ($constructor->getParameters() as $param) { - $this->constructorArgs[$param->getName()] = $param; + $this->constructorArgs[$nameConverter->denormalize($param->getName())] = $param; } } } diff --git a/tests/Config/Loader/ClassLoader/Resolver/ConstructorResolverTest.php b/tests/Config/Loader/ClassLoader/Resolver/ConstructorResolverTest.php index 0164fa2..e6c364d 100644 --- a/tests/Config/Loader/ClassLoader/Resolver/ConstructorResolverTest.php +++ b/tests/Config/Loader/ClassLoader/Resolver/ConstructorResolverTest.php @@ -13,6 +13,8 @@ use Cascade\Config\Loader\ClassLoader\Resolver\ConstructorResolver; use Cascade\Tests\Fixtures\SampleClass; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + /** * Class ConstructorResolverTest * @@ -74,13 +76,17 @@ public function testConstructor() /** * Test that constructor args were pulled properly + * + * Notie that we need to deuplicate the CamelCase conversion here for old + * fashioned classes */ public function testInitConstructorArgs() { $expectedConstructorArgs = array(); + $nameConverter = new CamelCaseToSnakeCaseNameConverter(); foreach ($this->getConstructorArgs() as $param) { - $expectedConstructorArgs[$param->getName()] = $param; + $expectedConstructorArgs[$nameConverter->denormalize($param->getName())] = $param; } $this->assertEquals($expectedConstructorArgs, $this->resolver->getConstructorArgs()); } @@ -91,12 +97,13 @@ public function testInitConstructorArgs() public function testHashToArgsArray() { $this->assertEquals( - array('someValue', 'hello', 'there'), + array('someValue', 'hello', 'there', 'slither'), $this->resolver->hashToArgsArray( array( // Not properly ordered on purpose - 'optionalB' => 'there', - 'optionalA' => 'hello', - 'mandatory' => 'someValue' + 'optionalB' => 'there', + 'optionalA' => 'hello', + 'optionalSnake' => 'slither', + 'mandatory' => 'someValue', ) ) ); @@ -113,22 +120,23 @@ public function optionsProvider() { return array( array( - array('someValue', 'hello', 'there'), // Expected resolved options + array('someValue', 'hello', 'there', 'slither'), // Expected resolved options array( // Options (order should not matter, part of resolution) - 'optionalB' => 'there', - 'optionalA' => 'hello', - 'mandatory' => 'someValue' + 'optionalB' => 'there', + 'optionalA' => 'hello', + 'mandatory' => 'someValue', + 'optionalSnake' => 'slither', ) ), array( - array('someValue', 'hello', 'BBB'), + array('someValue', 'hello', 'BBB', 'snake'), array( 'mandatory' => 'someValue', - 'optionalA' => 'hello' + 'optionalA' => 'hello', ) ), array( - array('someValue', 'AAA', 'BBB'), + array('someValue', 'AAA', 'BBB', 'snake'), array('mandatory' => 'someValue') ) ); diff --git a/tests/Config/Loader/ClassLoaderTest.php b/tests/Config/Loader/ClassLoaderTest.php index 57471a5..4cd49f8 100644 --- a/tests/Config/Loader/ClassLoaderTest.php +++ b/tests/Config/Loader/ClassLoaderTest.php @@ -16,6 +16,7 @@ use Cascade\Config\Loader\ClassLoader; use Cascade\Tests\Fixtures\SampleClass; +use Cascade\Tests\Fixtures\DependentClass; /** * Class ClassLoaderTest @@ -145,4 +146,29 @@ public function testLoad() $this->assertEquals($expectedInstance, $instance); } + + /** + * Test a nested class to load + * + * @author Dom Morgan + */ + public function testLoadDependency() + { + $options = array( + 'class' => 'Cascade\Tests\Fixtures\DependentClass', + 'dependency' => [ + 'class' => 'Cascade\Tests\Fixtures\SampleClass', + 'mandatory' => 'someValue', + ] + ); + + $loader = new ClassLoader($options); + $instance = $loader->load(); + + $expectedInstance = new DependentClass( + new SampleClass('someValue') + ); + + $this->assertEquals($expectedInstance, $instance); + } } diff --git a/tests/Fixtures/DependentClass.php b/tests/Fixtures/DependentClass.php new file mode 100644 index 0000000..62414ed --- /dev/null +++ b/tests/Fixtures/DependentClass.php @@ -0,0 +1,47 @@ + + * (c) The Orchard + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Cascade\Tests\Fixtures; + +/** + * Class SampleClass + * + * @author Raphael Antonmattei + */ +class DependentClass +{ + /** + * An object dependency + * @var Cascade\Tests\Fixtures\SampleClass + */ + private $dependency; + + /** + * Constructor + * + * @param mixed $mandatory Some mandatory param + * @param string $optionalA Some optional param + */ + public function __construct( + SampleClass $dependency + ) { + $this->setDependency($dependency); + } + + /** + * Set the object dependency + * + * @param Cascade\Tests\Fixtures\SampleClass $dependency Some value + */ + public function setDependency($dependency) + { + $this->dependency = $dependency; + } +} diff --git a/tests/Fixtures/SampleClass.php b/tests/Fixtures/SampleClass.php index b57dbda..8a3e566 100644 --- a/tests/Fixtures/SampleClass.php +++ b/tests/Fixtures/SampleClass.php @@ -69,7 +69,8 @@ class SampleClass public function __construct( $mandatory, $optionalA = 'AAA', - $optionalB = 'BBB' + $optionalB = 'BBB', + $optional_snake = 'snake' ) { $this->setMandatory($mandatory); }