From e1d8239e8aed0a9bce99cf13470aeeeba69e444d Mon Sep 17 00:00:00 2001 From: meadsteve Date: Sun, 17 Nov 2013 17:23:35 +0000 Subject: [PATCH] Create factory for plugins that resources can be registered with. --- PHPCI/Plugin/Util/Factory.php | 145 +++++++++++++++++ Tests/PHPCI/Plugin/Util/ExamplePlugins.php | 57 +++++++ Tests/PHPCI/Plugin/Util/FactoryTest.php | 174 +++++++++++++++++++++ composer.json | 3 +- composer.lock | 104 +++++++++++- 5 files changed, 481 insertions(+), 2 deletions(-) create mode 100644 PHPCI/Plugin/Util/Factory.php create mode 100644 Tests/PHPCI/Plugin/Util/ExamplePlugins.php create mode 100644 Tests/PHPCI/Plugin/Util/FactoryTest.php diff --git a/PHPCI/Plugin/Util/Factory.php b/PHPCI/Plugin/Util/Factory.php new file mode 100644 index 00000000..a72bfaba --- /dev/null +++ b/PHPCI/Plugin/Util/Factory.php @@ -0,0 +1,145 @@ +container = $container; + } + else { + $this->container = new \Pimple(); + } + + $self = $this; + $this->registerResource( + function() use ($self) { + return $self->getLastOptions(); + }, + 'options', + 'array' + ); + } + + public function getLastOptions() { + return $this->currentPluginOptions; + } + + public function buildPlugin($className, array $options = array()) + { + $this->currentPluginOptions = $options; + + $reflectedPlugin = new \ReflectionClass($className); + + $constructor = $reflectedPlugin->getConstructor(); + + if ($constructor) { + $argsToUse = array(); + foreach($constructor->getParameters() as $param) { + $argsToUse = $this->addArgFromParam($argsToUse, $param); + } + $plugin = $reflectedPlugin->newInstanceArgs($argsToUse); + } else { + $plugin = $reflectedPlugin->newInstance(); + } + + return $plugin; + } + + /** + * @param callable $loader + * @param string|null $name + * @param string|null $type + * @throws \InvalidArgumentException + * @internal param mixed $resource + */ + public function registerResource(callable $loader, + $name = null, + $type = null + ) + { + if ($name === null && $type === null) { + throw new \InvalidArgumentException( + "Type or Name must be specified" + ); + } + + $resourceID = $this->getInternalID($type, $name); + + $this->container[$resourceID] = $loader; + } + + private function getInternalID($type = null, $name = null) + { + $type = $type ? : ""; + $name = $name ? : ""; + return $type . "-" . $name; + } + + private function getResourceFor($type = null, $name = null) + { + $fullId = $this->getInternalID($type, $name); + if (isset($this->container[$fullId])) { + return $this->container[$fullId]; + } + + $typeOnlyID = $this->getInternalID($type, null); + if (isset($this->container[$typeOnlyID])) { + return $this->container[$typeOnlyID]; + } + + $nameOnlyID = $this->getInternalID(null, $name); + if (isset($this->container[$nameOnlyID])) { + return $this->container[$nameOnlyID]; + } + + return null; + } + + private function getParamType(\ReflectionParameter $param) + { + $class = $param->getClass(); + if ($class) { + return $class->getName(); + } elseif($param->isArray()) { + return self::TYPE_Array; + } elseif($param->isCallable()) { + return self::TYPE_Callable; + } else { + return null; + } + } + + private function addArgFromParam($existingArgs, \ReflectionParameter $param) + { + $name = $param->getName(); + $type = $this->getParamType($param); + $arg = $this->getResourceFor($type, $name); + + if ($arg !== null) { + $existingArgs[] = $arg; + } elseif ($arg === null && $param->isOptional()) { + $existingArgs[] = $param->getDefaultValue(); + } else { + throw new \DomainException( + "Unsatisfied dependency: " . $param->getName() + ); + } + + return $existingArgs; + } +} \ No newline at end of file diff --git a/Tests/PHPCI/Plugin/Util/ExamplePlugins.php b/Tests/PHPCI/Plugin/Util/ExamplePlugins.php new file mode 100644 index 00000000..9b5067a7 --- /dev/null +++ b/Tests/PHPCI/Plugin/Util/ExamplePlugins.php @@ -0,0 +1,57 @@ +RequiredArgument = $requiredArgument; + } +} + +class ExamplePluginWithSingleTypedRequiredArg { + + public $RequiredArgument; + + function __construct(\stdClass $requiredArgument) + { + $this->RequiredArgument = $requiredArgument; + } +} + +class ExamplePluginFull implements Plugin { + + public $Options; + + public function __construct( + Builder $phpci, + Build $build, + array $options = array() + ) + { + $this->Options = $options; + } + + public function execute() + { + + } + +} \ No newline at end of file diff --git a/Tests/PHPCI/Plugin/Util/FactoryTest.php b/Tests/PHPCI/Plugin/Util/FactoryTest.php new file mode 100644 index 00000000..fb2794c8 --- /dev/null +++ b/Tests/PHPCI/Plugin/Util/FactoryTest.php @@ -0,0 +1,174 @@ +testedFactory = new Factory(); + + // Setup a resource that can be returned and asserted against + $this->expectedResource = new \stdClass(); + $resourceLink = $this->expectedResource; + $this->resourceLoader = function() use (&$resourceLink) { + return $resourceLink; + }; + } + + protected function tearDown() + { + // Nothing to do. + } + + + public function testRegisterResourceThrowsExceptionWithoutTypeAndName() + { + $this->setExpectedException("InvalidArgumentException"); + $this->testedFactory->registerResource($this->resourceLoader, null, null); + } + + public function testBuildPluginWorksWithConstructorlessPlugins() + { + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace .'ExamplePluginWithNoConstructorArgs'; + $plugin = $this->testedFactory->buildPlugin($expectedPluginClass); + $this->assertInstanceOf($expectedPluginClass, $plugin); + } + + public function testBuildPluginWorksWithSingleOptionalArgConstructor() + { + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace . 'ExamplePluginWithSingleOptionalArg'; + $plugin = $this->testedFactory->buildPlugin($expectedPluginClass); + $this->assertInstanceOf($expectedPluginClass, $plugin); + } + + public function testBuildPluginThrowsExceptionIfMissingResourcesForRequiredArg() + { + $this->setExpectedException( + 'DomainException', + 'Unsatisfied dependency: requiredArgument' + ); + + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace . 'ExamplePluginWithSingleRequiredArg'; + $plugin = $this->testedFactory->buildPlugin($expectedPluginClass); + } + + public function testBuildPluginLoadsArgumentsBasedOnName() + { + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace . 'ExamplePluginWithSingleRequiredArg'; + + $this->testedFactory->registerResource( + $this->resourceLoader, + "requiredArgument" + ); + + /** @var ExamplePluginWithSingleRequiredArg $plugin */ + $plugin = $this->testedFactory->buildPlugin($expectedPluginClass); + + $this->assertEquals($this->expectedResource, $plugin->RequiredArgument); + } + + public function testBuildPluginLoadsArgumentsBasedOnType() + { + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace . 'ExamplePluginWithSingleTypedRequiredArg'; + + $this->testedFactory->registerResource( + $this->resourceLoader, + null, + "stdClass" + ); + + /** @var ExamplePluginWithSingleTypedRequiredArg $plugin */ + $plugin = $this->testedFactory->buildPlugin($expectedPluginClass); + + $this->assertEquals($this->expectedResource, $plugin->RequiredArgument); + } + + public function testBuildPluginLoadsFullExample() + { + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace . 'ExamplePluginFull'; + + $this->registerBuildAndBuilder(); + + /** @var ExamplePluginFull $plugin */ + $plugin = $this->testedFactory->buildPlugin($expectedPluginClass); + + $this->assertInstanceOf($expectedPluginClass, $plugin); + } + + public function testBuildPluginLoadsFullExampleWithOptions() + { + $namespace = '\\PHPCI\\Plugin\\Tests\\Util\\'; + $expectedPluginClass = $namespace . 'ExamplePluginFull'; + + $expectedArgs = array( + 'thing' => "stuff" + ); + + $this->registerBuildAndBuilder(); + + /** @var ExamplePluginFull $plugin */ + $plugin = $this->testedFactory->buildPlugin( + $expectedPluginClass, + $expectedArgs + ); + + $this->assertInternalType('array', $plugin->Options); + $this->assertArrayHasKey('thing', $plugin->Options); + } + + /** + * Registers mocked Builder and Build classes so that realistic plugins + * can be tested. + */ + private function registerBuildAndBuilder() + { + $this->testedFactory->registerResource( + function () { + return $this->getMock( + 'PHPCI\Builder', + array(), + array(), + '', + false + ); + }, + null, + 'PHPCI\\Builder' + ); + + $this->testedFactory->registerResource( + function () { + return $this->getMock( + 'PHPCI\Model\Build', + array(), + array(), + '', + false + ); + }, + null, + 'PHPCI\\Model\Build' + ); + } +} + \ No newline at end of file diff --git a/composer.json b/composer.json index e6fac6cb..92984fbd 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "symfony/yaml" : "2.*", "symfony/console" : "2.*", "psr/log": "1.0.0", - "monolog/monolog": "1.6.0" + "monolog/monolog": "1.6.0", + "pimple/pimple": "v1.1.*" }, "suggest": { diff --git a/composer.lock b/composer.lock index 43452ea3..00db7740 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "534baabecc11275d5cc7f375eecf738d", + "hash": "8ad9f4b137f30db71c8dcf45d8347655", "packages": [ { "name": "block8/b8framework", @@ -153,6 +153,52 @@ ], "time": "2013-07-28 22:38:30" }, + { + "name": "pimple/pimple", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/fabpot/Pimple.git", + "reference": "471c7d7c52ad6594e17b8ec33efdd1be592b5d83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fabpot/Pimple/zipball/471c7d7c52ad6594e17b8ec33efdd1be592b5d83", + "reference": "471c7d7c52ad6594e17b8ec33efdd1be592b5d83", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple is a simple Dependency Injection Container for PHP 5.3", + "homepage": "http://pimple.sensiolabs.org", + "keywords": [ + "container", + "dependency injection" + ], + "time": "2013-09-19 04:53:08" + }, { "name": "psr/log", "version": "1.0.0", @@ -293,6 +339,62 @@ "homepage": "http://symfony.com", "time": "2013-09-25 06:04:15" }, + { + "name": "symfony/dependency-injection", + "version": "v2.3.7", + "target-dir": "Symfony/Component/DependencyInjection", + "source": { + "type": "git", + "url": "https://github.com/symfony/DependencyInjection.git", + "reference": "3ead0b87b455289864d648152e0930629df687d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/DependencyInjection/zipball/3ead0b87b455289864d648152e0930629df687d2", + "reference": "3ead0b87b455289864d648152e0930629df687d2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/config": "~2.2", + "symfony/yaml": "~2.0" + }, + "suggest": { + "symfony/config": "", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\DependencyInjection\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "http://symfony.com", + "time": "2013-11-09 15:43:20" + }, { "name": "symfony/yaml", "version": "v2.3.6",