diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index dc853fbf..c58bd928 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -9,6 +9,7 @@ namespace PHPCI; +use PHPCI\Helper\MailerFactory; use PHPCI\Model\Build; use b8\Store; use b8\Config; @@ -90,6 +91,11 @@ class Builder implements LoggerAwareInterface */ public $quiet = false; + /** + * @var \PHPCI\Plugin\Util\Factory + */ + protected $pluginFactory; + /** * Set up the builder. * @param \PHPCI\Model\Build $build @@ -102,6 +108,7 @@ class Builder implements LoggerAwareInterface } $this->build = $build; $this->store = Store\Factory::getStore('Build'); + $this->setupPluginFactory($build); } /** @@ -425,7 +432,7 @@ class Builder implements LoggerAwareInterface // Try running it: try { - $obj = new $class($this, $this->build, $options); + $obj = $this->pluginFactory->buildPlugin($class, $options); if (!$obj->execute()) { $rtn = false; @@ -491,4 +498,35 @@ class Builder implements LoggerAwareInterface { return $this->logger; } + + private function setupPluginFactory(Build $build) + { + $this->pluginFactory = new Plugin\Util\Factory(); + + $self = $this; + $this->pluginFactory->registerResource( + function () use($self) { + return $self; + }, + null, + 'PHPCI\Builder' + ); + + $this->pluginFactory->registerResource( + function () use($build) { + return $build; + }, + null, + 'PHPCI\Model\Build' + ); + + $this->pluginFactory->registerResource( + function () use ($self) { + $factory = new MailerFactory($self->getSystemConfig('phpci')); + return $factory->getSwiftMailerFromConfig(); + }, + null, + 'Swift_Mailer' + ); + } } diff --git a/PHPCI/Helper/MailerFactory.php b/PHPCI/Helper/MailerFactory.php new file mode 100644 index 00000000..33bb8b74 --- /dev/null +++ b/PHPCI/Helper/MailerFactory.php @@ -0,0 +1,58 @@ +emailConfig = isset($phpCiSettings['email_settings']) ?: array(); + } + + /** + * Returns an instance of Swift_Mailer based on the config.s + * @return \Swift_Mailer + */ + public function getSwiftMailerFromConfig() + { + /** @var \Swift_SmtpTransport $transport */ + $transport = \Swift_SmtpTransport::newInstance( + $this->getMailConfig('smtp_address'), + $this->getMailConfig('smtp_port'), + $this->getMailConfig('smtp_encryption') + ); + $transport->setUsername($this->getMailConfig('smtp_username')); + $transport->setPassword($this->getMailConfig('smtp_password')); + + return \Swift_Mailer::newInstance($transport); + } + + protected function getMailConfig($configName) + { + if (isset($this->emailConfig[$configName]) && $this->emailConfig[$configName] != "") { + return $this->emailConfig[$configName]; + } else { + // Check defaults + + switch($configName) { + case 'smtp_address': + return "localhost"; + case 'default_mailto_address': + return null; + case 'smtp_port': + return '25'; + case 'smtp_encryption': + return null; + default: + return ""; + } + } + } + +} \ No newline at end of file diff --git a/PHPCI/Plugin.php b/PHPCI/Plugin.php index a924b39a..6f915e3b 100644 --- a/PHPCI/Plugin.php +++ b/PHPCI/Plugin.php @@ -18,6 +18,5 @@ use PHPCI\Model\Build; */ interface Plugin { - public function __construct(Builder $phpci, Build $build, array $options = array()); public function execute(); } diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 7b80d795..81671e1c 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -30,25 +30,34 @@ class Email implements \PHPCI\Plugin */ protected $options; - /** - * @var array - */ - protected $emailConfig; - /** * @var \Swift_Mailer */ protected $mailer; - public function __construct(Builder $phpci, Build $build, array $options = array()) + /** + * @var string + */ + protected $fromAddress; + + public function __construct(Builder $phpci, + Build $build, + \Swift_Mailer $mailer, + array $options = array() + + ) { - $phpCiSettings = $phpci->getSystemConfig('phpci'); $this->phpci = $phpci; $this->build = $build; $this->options = $options; - $this->emailConfig = isset($phpCiSettings['email_settings']) ? $phpCiSettings['email_settings'] : array(); - $this->loadSwiftMailerFromConfig(); + $phpCiSettings = $phpci->getSystemConfig('phpci'); + + $this->fromAddress = isset($phpCiSettings['email_settings']['from_address']) + ? $phpCiSettings['email_settings']['from_address'] + : "notifications-ci@phptesting.org"; + + $this->mailer = $mailer; } /** @@ -98,7 +107,7 @@ class Email implements \PHPCI\Plugin public function sendEmail($toAddresses, $subject, $body) { $message = \Swift_Message::newInstance($subject) - ->setFrom($this->getMailConfig('from_address')) + ->setFrom($this->fromAddress) ->setTo($toAddresses) ->setBody($body) ->setContentType("text/html"); @@ -120,44 +129,6 @@ class Email implements \PHPCI\Plugin return $failures; } - protected function loadSwiftMailerFromConfig() - { - /** @var \Swift_SmtpTransport $transport */ - $transport = \Swift_SmtpTransport::newInstance( - $this->getMailConfig('smtp_address'), - $this->getMailConfig('smtp_port'), - $this->getMailConfig('smtp_encryption') - ); - $transport->setUsername($this->getMailConfig('smtp_username')); - $transport->setPassword($this->getMailConfig('smtp_password')); - - $this->mailer = \Swift_Mailer::newInstance($transport); - } - - protected function getMailConfig($configName) - { - if (isset($this->emailConfig[$configName]) && $this->emailConfig[$configName] != "") { - return $this->emailConfig[$configName]; - } else { - // Check defaults - - switch($configName) { - case 'smtp_address': - return "localhost"; - case 'default_mailto_address': - return null; - case 'smtp_port': - return '25'; - case 'smtp_encryption': - return null; - case 'from_address': - return "notifications-ci@phptesting.org"; - default: - return ""; - } - } - } - protected function getEmailAddresses() { $addresses = array(); @@ -179,4 +150,4 @@ class Email implements \PHPCI\Plugin } return $addresses; } -} +} \ No newline at end of file diff --git a/PHPCI/Plugin/Util/Factory.php b/PHPCI/Plugin/Util/Factory.php new file mode 100644 index 00000000..4e6065e6 --- /dev/null +++ b/PHPCI/Plugin/Util/Factory.php @@ -0,0 +1,153 @@ +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; + } + + /** + * Builds an instance of plugin of class $className. $options will + * be passed along with any resources registered with the factory. + * + * @param $className + * @param array $options + * @return \PHPCI\Plugin + */ + 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/Email.php b/Tests/PHPCI/Plugin/EmailTest.php similarity index 95% rename from Tests/PHPCI/Plugin/Email.php rename to Tests/PHPCI/Plugin/EmailTest.php index 8000da7e..1343068c 100644 --- a/Tests/PHPCI/Plugin/Email.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -10,13 +10,12 @@ namespace PHPCI\Plugin\Tests; use PHPCI\Plugin\Email as EmailPlugin; -define('PHPCI_BIN_DIR', "FAKEPHPCIBIN"); /** * Unit test for the PHPUnit plugin. * @author meadsteve */ -class EmailTest extends \PHPUnit_Framework_TestCase +class EmailTest extends \PHPUnit_Framework_TestCase { /** @@ -43,7 +42,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase { $this->mockBuild = $this->getMock( '\PHPCI\Model\Build', - array('getLog'), + array('getLog', 'getStatus'), array(), "mockBuild", false @@ -53,6 +52,10 @@ class EmailTest extends \PHPUnit_Framework_TestCase ->method('getLog') ->will($this->returnValue("Build Log")); + $this->mockBuild->expects($this->any()) + ->method('getStatus') + ->will($this->returnValue(\PHPCI\Model\Build::STATUS_SUCCESS)); + $this->mockCiBuilder = $this->getMock( '\PHPCI\Builder', array('getSystemConfig', @@ -96,8 +99,9 @@ class EmailTest extends \PHPUnit_Framework_TestCase { $this->testedEmailPlugin = new EmailPlugin( $this->mockCiBuilder, - $arrOptions, - $this->mockMailer + $this->mockBuild, + $this->mockMailer, + $arrOptions ); } 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..59d9c930 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": "1.1.*" }, "suggest": { diff --git a/composer.lock b/composer.lock index 43452ea3..278e483a 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": "2f0615871ce4ee1eb8e4642bf0c731da", "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", @@ -242,17 +288,17 @@ }, { "name": "symfony/console", - "version": "v2.3.6", + "version": "v2.3.7", "target-dir": "Symfony/Component/Console", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "f880062d56edefb25b36f2defa65aafe65959dc7" + "reference": "00848d3e13cf512e77c7498c2b3b0192f61f4b18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/f880062d56edefb25b36f2defa65aafe65959dc7", - "reference": "f880062d56edefb25b36f2defa65aafe65959dc7", + "url": "https://api.github.com/repos/symfony/Console/zipball/00848d3e13cf512e77c7498c2b3b0192f61f4b18", + "reference": "00848d3e13cf512e77c7498c2b3b0192f61f4b18", "shasum": "" }, "require": { @@ -291,21 +337,21 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-09-25 06:04:15" + "time": "2013-11-13 21:27:40" }, { "name": "symfony/yaml", - "version": "v2.3.6", + "version": "v2.3.7", "target-dir": "Symfony/Component/Yaml", "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "6bb881b948368482e1abf1a75c08bcf88a1c5fc3" + "reference": "c1bda5b459d792cb253de12c65beba3040163b2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/6bb881b948368482e1abf1a75c08bcf88a1c5fc3", - "reference": "6bb881b948368482e1abf1a75c08bcf88a1c5fc3", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/c1bda5b459d792cb253de12c65beba3040163b2b", + "reference": "c1bda5b459d792cb253de12c65beba3040163b2b", "shasum": "" }, "require": { @@ -338,7 +384,7 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-09-22 18:04:39" + "time": "2013-10-17 11:48:01" } ], "packages-dev": [