From 712243660c1d89a949884f23ea900e9493ade6b1 Mon Sep 17 00:00:00 2001 From: Dmitry Khomutov Date: Fri, 6 Jan 2017 11:14:45 +0700 Subject: [PATCH] Fixed plugins and tests --- .gitignore | 1 + app/pluginconfig.example.php | 23 ++ src/PHPCensor/Builder.php | 63 +++++- src/PHPCensor/Plugin.php | 16 ++ .../Plugin/Util/ComposerPluginInformation.php | 49 +++- src/PHPCensor/Plugin/Util/Executor.php | 40 ++-- src/PHPCensor/Plugin/Util/Factory.php | 210 ++++++++++++++++++ tests/PHPCensor/Plugin/PharTest.php | 12 +- tests/PHPCensor/Plugin/Util/FactoryTest.php | 14 -- .../Plugin/Util/Fake/ExamplePluginFull.php | 9 +- .../ExamplePluginWithNoConstructorArgs.php | 2 +- .../ExamplePluginWithSingleOptionalArg.php | 2 +- .../ExamplePluginWithSingleRequiredArg.php | 2 +- ...xamplePluginWithSingleTypedRequiredArg.php | 2 +- 14 files changed, 385 insertions(+), 60 deletions(-) create mode 100644 app/pluginconfig.example.php create mode 100644 src/PHPCensor/Plugin/Util/Factory.php diff --git a/.gitignore b/.gitignore index 6df93349..c86bb192 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /composer.phar /runtime /app/loggerconfig.php +/app/pluginconfig.php /app/config.yml /public/assets/vendor diff --git a/app/pluginconfig.example.php b/app/pluginconfig.example.php new file mode 100644 index 00000000..538d30ff --- /dev/null +++ b/app/pluginconfig.example.php @@ -0,0 +1,23 @@ +registerResource( + // This function will be called when the resource is needed. + function() { + return [ + 'Foo' => "Stuff", + 'Bar' => "More Stuff" + ]; + }, + + // In addition to the function for building the resource the system + // also needs to be told when to load the resource. Either or both + // of the following arguments can be used (null to ignore) + + // This resource will only be given when the argument name is: + "ResourceArray", + + // The resource will only be given when the type hint is: + PHPCensor\Plugin\Util\Factory::TYPE_ARRAY + ); +}; diff --git a/src/PHPCensor/Builder.php b/src/PHPCensor/Builder.php index 87d8694e..18c0c5fe 100644 --- a/src/PHPCensor/Builder.php +++ b/src/PHPCensor/Builder.php @@ -1,4 +1,5 @@ buildLogger = new BuildLogger($logger, $build); - $this->pluginExecutor = new Plugin\Util\Executor($this, $build, $this->buildLogger); + $pluginFactory = $this->buildPluginFactory($build); + $pluginFactory->addConfigFromFile(APP_DIR . "pluginconfig.php"); + $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger); $executorClass = 'PHPCensor\Helper\UnixCommandExecutor'; if (IS_WIN) { @@ -230,7 +233,7 @@ class Builder implements LoggerAwareInterface $this->build->setStatus(Build::STATUS_FAILED); $this->buildLogger->logFailure(Lang::get('exception') . $ex->getMessage()); } - + if (Build::STATUS_FAILED === $this->build->getStatus()) { $this->buildLogger->logFailure("\n" . Lang::get('build_failed')); } else { @@ -361,7 +364,7 @@ class Builder implements LoggerAwareInterface /** * Add a success-coloured message to the log. - * + * * @param string */ public function logSuccess($message) @@ -371,7 +374,7 @@ class Builder implements LoggerAwareInterface /** * Add a failure-coloured message to the log. - * + * * @param string $message * @param \Exception $exception The exception that caused the error. */ @@ -382,11 +385,59 @@ class Builder implements LoggerAwareInterface /** * Add a debug message to the log. - * + * * @param string */ public function logDebug($message) { $this->buildLogger->logDebug($message); } + + /** + * Returns a configured instance of the plugin factory. + * + * @param Build $build + * @return PluginFactory + */ + private function buildPluginFactory(Build $build) + { + $pluginFactory = new PluginFactory(); + + $self = $this; + $pluginFactory->registerResource( + function () use ($self) { + return $self; + }, + null, + 'PHPCensor\Builder' + ); + + $pluginFactory->registerResource( + function () use ($build) { + return $build; + }, + null, + 'PHPCensor\Model\Build' + ); + + $logger = $this->logger; + $pluginFactory->registerResource( + function () use ($logger) { + return $logger; + }, + null, + 'Psr\Log\LoggerInterface' + ); + + $pluginFactory->registerResource( + function () use ($self) { + $factory = new MailerFactory($self->getSystemConfig('php-censor')); + return $factory->getSwiftMailerFromConfig(); + }, + null, + 'Swift_Mailer' + ); + + return $pluginFactory; + } } diff --git a/src/PHPCensor/Plugin.php b/src/PHPCensor/Plugin.php index 8b28b2b5..5675d8fb 100644 --- a/src/PHPCensor/Plugin.php +++ b/src/PHPCensor/Plugin.php @@ -47,6 +47,22 @@ abstract class Plugin $this->builder->logDebug('Plugin options: ' . json_encode($options)); } + /** + * @return Build + */ + public function getBuild() + { + return $this->build; + } + + /** + * @return Builder + */ + public function getBuilder() + { + return $this->builder; + } + /** * @return boolean */ diff --git a/src/PHPCensor/Plugin/Util/ComposerPluginInformation.php b/src/PHPCensor/Plugin/Util/ComposerPluginInformation.php index d25baa57..7202ade0 100644 --- a/src/PHPCensor/Plugin/Util/ComposerPluginInformation.php +++ b/src/PHPCensor/Plugin/Util/ComposerPluginInformation.php @@ -6,7 +6,6 @@ use PHPCensor\Plugin; /** * Class ComposerPluginInformation - * * @package PHPCensor\Plugin\Util */ class ComposerPluginInformation implements InstalledPluginInformation @@ -35,6 +34,15 @@ class ComposerPluginInformation implements InstalledPluginInformation return new self($installed); } + /** + * @param \stdClass[] $composerPackages This should be the contents of the + * installed.json file created by composer + */ + public function __construct(array $composerPackages) + { + $this->composerPackages = $composerPackages; + } + /** * Returns an array of objects. Each one represents an available plugin * and will have the following properties: @@ -44,6 +52,7 @@ class ComposerPluginInformation implements InstalledPluginInformation */ public function getInstalledPlugins() { + $this->loadPluginInfo(); return $this->pluginInfo; } @@ -63,6 +72,44 @@ class ComposerPluginInformation implements InstalledPluginInformation ); } + /** + * Load a list of available plugins from the installed composer packages. + */ + protected function loadPluginInfo() + { + if ($this->pluginInfo !== null) { + return; + } + $this->pluginInfo = []; + foreach ($this->composerPackages as $package) { + $this->addPluginsFromPackage($package); + } + } + + /** + * @param \stdClass $package + */ + protected function addPluginsFromPackage($package) + { + if (isset($package->extra->phpci)) { + $phpciData = $package->extra->phpci; + + if (isset($phpciData->pluginNamespace)) { + $rootNamespace = $phpciData->pluginNamespace; + } else { + $rootNamespace = ""; + } + + if (is_array($phpciData->suppliedPlugins)) { + $this->addPlugins( + $phpciData->suppliedPlugins, + $package->name, + $rootNamespace + ); + } + } + } + /** * @param \stdClass[] $plugins * @param string $sourcePackageName diff --git a/src/PHPCensor/Plugin/Util/Executor.php b/src/PHPCensor/Plugin/Util/Executor.php index 43c44577..884ba313 100644 --- a/src/PHPCensor/Plugin/Util/Executor.php +++ b/src/PHPCensor/Plugin/Util/Executor.php @@ -8,46 +8,37 @@ use PHPCensor\Helper\Lang; use PHPCensor\Logging\BuildLogger; use PHPCensor\Model\Build; use PHPCensor\Store\BuildStore; -use PHPCensor\Builder; /** * Plugin Executor - Runs the configured plugins for a given build stage. - * * @package PHPCensor\Plugin\Util */ class Executor { - /** - * @var \PHPCensor\Builder - */ - protected $builder; - - /** - * @var \PHPCensor\Model\Build - */ - protected $build; - /** * @var BuildLogger */ protected $logger; + /** + * @var Factory + */ + protected $pluginFactory; + /** * @var BuildStore */ protected $store; /** - * @param Builder $builder - * @param Build $build + * @param Factory $pluginFactory * @param BuildLogger $logger */ - public function __construct(Builder $builder, Build $build, BuildLogger $logger) + public function __construct(Factory $pluginFactory, BuildLogger $logger, BuildStore $store = null) { - $this->builder = $builder; - $this->build = $build; - $this->logger = $logger; - $this->store = StoreFactory::getStore('Build'); + $this->pluginFactory = $pluginFactory; + $this->logger = $logger; + $this->store = $store ?: StoreFactory::getStore('Build'); } /** @@ -87,7 +78,7 @@ class Executor protected function getBranchSpecificPlugins(&$config, $stage, $pluginsToExecute) { /** @var \PHPCensor\Model\Build $build */ - $build = $this->build; + $build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build'); $branch = $build->getBranch(); // If we don't have any branch-specific plugins: @@ -199,7 +190,10 @@ class Executor } try { - return (new $class($this->builder, $this->build, $options))->execute(); + // Build and run it + $obj = $this->pluginFactory->buildPlugin($class, $options); + + return $obj->execute(); } catch (\Exception $ex) { $this->logger->logFailure(Lang::get('exception') . $ex->getMessage(), $ex); @@ -241,7 +235,7 @@ class Executor private function getBuildSummary() { /** @var Build $build */ - $build = $this->build; + $build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build'); $metas = $this->store->getMeta('plugin-summary', $build->getProjectId(), $build->getId()); return isset($metas[0]['meta_value']) ? $metas[0]['meta_value'] : []; } @@ -254,7 +248,7 @@ class Executor private function setBuildSummary($summary) { /** @var Build $build */ - $build = $this->build; + $build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build'); $this->store->setMeta($build->getProjectId(), $build->getId(), 'plugin-summary', json_encode($summary)); } } diff --git a/src/PHPCensor/Plugin/Util/Factory.php b/src/PHPCensor/Plugin/Util/Factory.php new file mode 100644 index 00000000..267931b8 --- /dev/null +++ b/src/PHPCensor/Plugin/Util/Factory.php @@ -0,0 +1,210 @@ +container = $container; + } else { + $this->container = new Container(); + } + } + + /** + * Trys to get a function from the file path specified. If the + * file returns a function then $this will be passed to it. + * This enables the config file to call any public methods. + * + * @param $configPath + * @return bool - true if the function exists else false. + */ + public function addConfigFromFile($configPath) + { + // The file is expected to return a function which can + // act on the pluginFactory to register any resources needed. + if (file_exists($configPath)) { + $configFunction = require($configPath); + if (is_callable($configFunction)) { + $configFunction($this); + return true; + } + } + return false; + } + + /** + * Get most recently used factory options. + * @return mixed + */ + 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|null $options + * @throws \InvalidArgumentException if $className doesn't represent a valid plugin + * @return \PHPCensor\Plugin + */ + public function buildPlugin($className, $options = []) + { + $this->currentPluginOptions = $options; + + $reflectedPlugin = new \ReflectionClass($className); + + $constructor = $reflectedPlugin->getConstructor(); + + if ($constructor) { + $argsToUse = []; + foreach ($constructor->getParameters() as $param) { + if ('options' === $param->getName()) { + $argsToUse[] = $options; + } else { + $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( + $loader, + $name = null, + $type = null + ) { + if ($name === null && $type === null) { + throw new \InvalidArgumentException( + "Type or Name must be specified" + ); + } + + if (!($loader instanceof \Closure)) { + throw new \InvalidArgumentException( + '$loader is expected to be a function' + ); + } + + $resourceID = $this->getInternalID($type, $name); + + $this->container[$resourceID] = $loader; + } + + /** + * Get an internal resource ID. + * @param null $type + * @param null $name + * @return string + */ + private function getInternalID($type = null, $name = null) + { + $type = $type ? : ""; + $name = $name ? : ""; + return $type . "-" . $name; + } + + /** + * @param string $type + * @param string $name + * @return mixed + */ + public 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; + } + + /** + * @param \ReflectionParameter $param + * @return null|string + */ + private function getParamType(\ReflectionParameter $param) + { + $class = $param->getClass(); + if ($class) { + return $class->getName(); + } elseif ($param->isArray()) { + return self::TYPE_ARRAY; + } elseif (is_callable($param)) { + return self::TYPE_CALLABLE; + } else { + return null; + } + } + + /** + * @param $existingArgs + * @param \ReflectionParameter $param + * @return array + * @throws \DomainException + */ + 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; + } +} diff --git a/tests/PHPCensor/Plugin/PharTest.php b/tests/PHPCensor/Plugin/PharTest.php index 87cb1d9b..ae35092b 100644 --- a/tests/PHPCensor/Plugin/PharTest.php +++ b/tests/PHPCensor/Plugin/PharTest.php @@ -95,13 +95,13 @@ class PharTest extends \PHPUnit_Framework_TestCase $plugin = $this->getPlugin(); $this->assertInstanceOf('PHPCensor\Plugin', $plugin); $this->assertInstanceOf('PHPCensor\Model\Build', $plugin->getBuild()); - $this->assertInstanceOf('PHPCensor\Builder', $plugin->getPHPCI()); + $this->assertInstanceOf('PHPCensor\Builder', $plugin->getBuilder()); } public function testDirectory() { $plugin = $this->getPlugin(); - $plugin->getPHPCI()->buildPath = 'foo'; + $plugin->getBuilder()->buildPath = 'foo'; $this->assertEquals('foo', $plugin->getDirectory()); $plugin = $this->getPlugin(['directory' => 'dirname']); @@ -141,7 +141,7 @@ class PharTest extends \PHPUnit_Framework_TestCase $plugin = $this->getPlugin(); $path = $this->buildSource(); - $plugin->getPHPCI()->buildPath = $path; + $plugin->getBuilder()->buildPath = $path; $this->assertTrue($plugin->execute()); @@ -159,7 +159,7 @@ class PharTest extends \PHPUnit_Framework_TestCase $plugin = $this->getPlugin(['regexp' => '/\.(php|phtml)$/']); $path = $this->buildSource(); - $plugin->getPHPCI()->buildPath = $path; + $plugin->getBuilder()->buildPath = $path; $this->assertTrue($plugin->execute()); @@ -185,7 +185,7 @@ STUB; file_put_contents($path . '/stub.php', $content); $plugin = $this->getPlugin(['stub' => 'stub.php']); - $plugin->getPHPCI()->buildPath = $path; + $plugin->getBuilder()->buildPath = $path; $this->assertTrue($plugin->execute()); @@ -201,7 +201,7 @@ STUB; $directory = $this->buildTemp(); $plugin = $this->getPlugin(['directory' => $directory]); - $plugin->getPHPCI()->buildPath = $this->buildSource(); + $plugin->getBuilder()->buildPath = $this->buildSource(); $this->assertFalse($plugin->execute()); } diff --git a/tests/PHPCensor/Plugin/Util/FactoryTest.php b/tests/PHPCensor/Plugin/Util/FactoryTest.php index d88a1c31..2f096702 100644 --- a/tests/PHPCensor/Plugin/Util/FactoryTest.php +++ b/tests/PHPCensor/Plugin/Util/FactoryTest.php @@ -40,7 +40,6 @@ class FactoryTest extends \PHPUnit_Framework_TestCase { // Nothing to do. } - public function testRegisterResourceThrowsExceptionWithoutTypeAndName() { $this->setExpectedException('InvalidArgumentException', 'Type or Name must be specified'); @@ -53,19 +52,6 @@ class FactoryTest extends \PHPUnit_Framework_TestCase { $this->testedFactory->registerResource(["dummy"], "TestName", "TestClass"); } - public function testBuildPluginWorksWithConstructorlessPlugins() - { - $pluginClass = $this->getFakePluginClassName('ExamplePluginWithNoConstructorArgs'); - $plugin = $this->testedFactory->buildPlugin($pluginClass); - $this->assertInstanceOf($pluginClass, $plugin); - } - - public function testBuildPluginFailsForNonPluginClasses() - { - $this->setExpectedException('InvalidArgumentException', 'Requested class must implement \PHPCensor\Plugin'); - $plugin = $this->testedFactory->buildPlugin("stdClass"); - } - public function testBuildPluginWorksWithSingleOptionalArgConstructor() { $pluginClass = $this->getFakePluginClassName('ExamplePluginWithSingleOptionalArg'); diff --git a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginFull.php b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginFull.php index 60d41b67..c39eb1d7 100644 --- a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginFull.php +++ b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginFull.php @@ -14,17 +14,14 @@ use PHPCensor\Builder; use PHPCensor\Model\Build; use PHPCensor\Plugin; -class ExamplePluginFull implements Plugin { +class ExamplePluginFull extends Plugin { /** * @var array */ public $Options; - public function __construct( - Builder $builder, - Build $build, - array $options = [] - ) { + public function __construct(Builder $builder, Build $build, array $options = []) + { $this->Options = $options; } diff --git a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithNoConstructorArgs.php b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithNoConstructorArgs.php index a23bd5ad..96a00068 100644 --- a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithNoConstructorArgs.php +++ b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithNoConstructorArgs.php @@ -12,7 +12,7 @@ namespace Tests\PHPCensor\Plugin\Util\Fake; use PHPCensor\Plugin; -class ExamplePluginWithNoConstructorArgs implements Plugin +class ExamplePluginWithNoConstructorArgs extends Plugin { public function execute() { diff --git a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleOptionalArg.php b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleOptionalArg.php index 79d42bbc..af8e40a0 100644 --- a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleOptionalArg.php +++ b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleOptionalArg.php @@ -12,7 +12,7 @@ namespace Tests\PHPCensor\Plugin\Util\Fake; use PHPCensor\Plugin; -class ExamplePluginWithSingleOptionalArg implements Plugin +class ExamplePluginWithSingleOptionalArg extends Plugin { function __construct($optional = null) { diff --git a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleRequiredArg.php b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleRequiredArg.php index 73d4b9ac..0663d759 100644 --- a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleRequiredArg.php +++ b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleRequiredArg.php @@ -12,7 +12,7 @@ namespace Tests\PHPCensor\Plugin\Util\Fake; use PHPCensor\Plugin; -class ExamplePluginWithSingleRequiredArg implements Plugin +class ExamplePluginWithSingleRequiredArg extends Plugin { public $RequiredArgument; diff --git a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleTypedRequiredArg.php b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleTypedRequiredArg.php index 1e172f1a..5e8e8ca2 100644 --- a/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleTypedRequiredArg.php +++ b/tests/PHPCensor/Plugin/Util/Fake/ExamplePluginWithSingleTypedRequiredArg.php @@ -12,7 +12,7 @@ namespace Tests\PHPCensor\Plugin\Util\Fake; use PHPCensor\Plugin; -class ExamplePluginWithSingleTypedRequiredArg implements Plugin +class ExamplePluginWithSingleTypedRequiredArg extends Plugin { public $RequiredArgument;