diff --git a/Classes/Command/AppCommandController.php b/Classes/Command/AppCommandController.php new file mode 100644 index 0000000..f7427bc --- /dev/null +++ b/Classes/Command/AppCommandController.php @@ -0,0 +1,123 @@ +onBeforeTask(function (TaskInterface $task) { + $this->output('Executing %s... ', [$task->getName()]); + }); + $taskRunner->onTaskResult(function (TaskInterface $task, Result $result) { + if ($result->hasErrors()) { + $this->outputFormatted('%s', [$task->getErrorLabel()]); + } else if ($result->hasNotices()) { + $this->outputFormatted('%s', [$task->getNoticeLabel()]); + } else { + $this->outputFormatted('%s', [$task->getSuccessLabel()]); + } + }); + + try { + return $taskRunner->run(); + } catch (\Exception $exception) { + $this->output->output('%s', [$exception->getMessage()]); + $this->systemLogger->logException($exception); + $result = new Result(); + $result->addError(new Error('Chain failed')); + return $result; + } + } + + /** + * @return Result + */ + protected function runTests(): Result + { + $testRunner = new TestRunner(); + + $testRunner->onBeforeTest(function (TestInterface $test) { + $this->output->output('Testing %s... ', [$test->getName()]); + }); + $testRunner->onTestResult(function (TestInterface $test, Result $result) { + if ($result->hasErrors()) { + $this->output->outputLine('%s', [$test->getErrorLabel()]); + } else if ($result->hasNotices()) { + $this->output->outputLine('%s', [$test->getNoticeLabel()]); + } else { + $this->output->outputLine('%s', [$test->getSuccessLabel()]); + } + }); + + return $testRunner->run(); + } + + /** + * Checks the readiness of the application + */ + public function isReadyCommand() + { + $this->outputLine(); + $this->outputLine('Running tests...'); + $this->outputLine(); + $testResult = $this->runTests(); + $this->outputLine(); + if (!$testResult->hasErrors()) { + $this->outputLine(); + $this->outputLine('All checks passed, executing ready tasks...'); + $this->outputLine(); + $readyResult = $this->runReadyTasks(); + $this->outputLine(); + + if (!$readyResult->hasErrors()) { + $this->outputLine('Application is ready'); + $this->outputLine(); + $this->outputLine(); + $this->quit(0); + } else { + $this->outputLine('Application did not pass all ready tasks and is not ready'); + $this->outputLine(); + $this->outputLine(); + $this->quit(1); + } + } else { + $this->outputLine(); + $this->outputLine('Application did not pass all checks and is not ready'); + $this->outputLine(); + $this->quit(1); + } + } +} diff --git a/Classes/Eel/Helper/ChainHelper.php b/Classes/Eel/Helper/ChainHelper.php new file mode 100644 index 0000000..4de2c25 --- /dev/null +++ b/Classes/Eel/Helper/ChainHelper.php @@ -0,0 +1,67 @@ +runtime->getChainResult(); + return !$result->hasErrors(); + } + + /** + * @return bool + */ + public function isInvalid(): bool + { + $result = $this->runtime->getChainResult(); + return $result->hasErrors(); + } + + /** + * @return string + */ + public function getCombinedErrorMessages(): string + { + $result = $this->runtime->getChainResult(); + $messages = array_map(function (Error $error) { + return $error->getMessage(); + }, $result->getErrors()); + + return implode(PHP_EOL, $messages); + } + + /** + * @param string $methodName + * @return boolean + */ + public function allowsCallOfMethod($methodName) + { + return $this->runtime->isTaskContext(); + } +} diff --git a/Classes/Eel/Helper/LockHelper.php b/Classes/Eel/Helper/LockHelper.php new file mode 100644 index 0000000..2e5f6ec --- /dev/null +++ b/Classes/Eel/Helper/LockHelper.php @@ -0,0 +1,127 @@ +lockPrefix) ? $this->lockPrefix . '_' : '') . $name; + } + + /** + * @return FrontendInterface + */ + protected function getCache(): FrontendInterface + { + try { + $cache = $this->cacheManager->getCache($this->overrideCacheName ?? $this->defaultCacheName); + } finally { + $this->overrideCacheName = null; + } + + return $cache; + } + + /** + * @param string $cacheName + * @return $this + */ + public function withCache(string $cacheName) + { + $this->overrideCacheName = $cacheName; + return $this; + } + + /** + * @param string $lockName + * @param string $value + */ + public function set(string $lockName, string $value = '1') + { + $this->getCache()->set($this->getEntryIdentifier($lockName), $value, [], 0); + } + + /** + * @param string $lockName + */ + public function unset(string $lockName) + { + $this->getCache()->remove($this->getEntryIdentifier($lockName)); + } + + /** + * @param string $lockName + * @return bool + */ + public function isSet(string $lockName) + { + return $this->getCache()->has($this->getEntryIdentifier($lockName)); + } + + /** + * @param string $lockName + * @return bool + */ + public function isUnset(string $lockName) + { + return !$this->isSet($lockName); + } + + /** + * @param string $methodName + * @return boolean + */ + public function allowsCallOfMethod($methodName) + { + return substr($methodName, 0, 2) === 'is' || $this->runtime->isTaskContext(); + } +} diff --git a/Classes/Service/AbstractTaskRunner.php b/Classes/Service/AbstractTaskRunner.php new file mode 100644 index 0000000..fbbf799 --- /dev/null +++ b/Classes/Service/AbstractTaskRunner.php @@ -0,0 +1,221 @@ +objectManager = $objectManager; + } + + /** + * + */ + public function __construct() + { + $this->onTaskResultClosure = function () { + }; + $this->onBeforeTaskClosure = function () { + }; + } + + /** + * @param \Closure $onTaskResultClosure + * @return $this + */ + public function onTaskResult(\Closure $onTaskResultClosure) + { + $this->onTaskResultClosure = $onTaskResultClosure; + return $this; + } + + /** + * @param \Closure $onBeforeTaskClosure + * @return $this + */ + public function onBeforeTask(\Closure $onBeforeTaskClosure) + { + $this->onBeforeTaskClosure = $onBeforeTaskClosure; + return $this; + } + + /** + * @param string $objectName + * @return string + */ + protected function resolveTaskClassName(string $objectName): string + { + if ($this->objectManager->isRegistered($objectName)) { + return $objectName; + } + + return sprintf($this->defaultTaskClassName, ucfirst($objectName)); + } + + /** + * @param TaskInterface $task + * @param array $configuration + * @return bool + */ + protected function shouldSkipTask(TaskInterface $task, array $configuration): bool + { + return !$this->runtime->evaluate($configuration['condition'] ?? $this->defaultCondition); + } + + + /** + * @param TaskInterface $task + * @param array $configuration + */ + protected function afterTaskInvocation(TaskInterface $task, array $configuration) + { + if (isset($configuration['afterInvocation'])) { + $this->runtime->evaluate($configuration['afterInvocation']); + } + } + + /** + * @param string $name + * @param array $configuration + * @return Result + * @throws InvalidConfigurationException + */ + protected function runTask(string $name, array $configuration): Result + { + $implementationClassName = $configuration[$this->context]; + $className = $this->resolveTaskClassName($implementationClassName); + + $task = new $className($name, $configuration['options'] ?? []); + + if (!($task instanceof TaskInterface)) { + throw new InvalidConfigurationException(sprintf('%s does not implement \Yeebase\Readiness\Task\TaskInterface', get_class($task)), 1502699058); + } + + $onBeforeTask = $this->onBeforeTaskClosure; + $onBeforeTask($task); + + $result = $task->getResult(); + try { + if ($this->shouldSkipTask($task, $configuration)) { + $result->addNotice(new Notice('Skipped')); + } else { + $task->run(); + $this->afterTaskInvocation($task, $configuration); + } + } catch (\Exception $exception) { + $result->addError(new Error($exception->getMessage(), $exception->getCode())); + $this->systemLogger->logException($exception); + } + + $onTaskResult = $this->onTaskResultClosure; + $onTaskResult($task, $result); + + return $result; + } + + /** + * @return Result + */ + public function run(): Result + { + $this->chainResult = new Result(); + $this->runtime->setTaskContext($this->context); + $this->runtime->setChainResult($this->chainResult); + + $sorter = new PositionalArraySorter($this->chain); + + foreach ($sorter->toArray() as $key => $task) { + $result = $this->runTask($task['name'] ?? ucfirst($key), $task); + $this->chainResult->merge($result); + if ($result->hasErrors() && $this->stopOnFail) { + break; + } + } + + return $this->chainResult; + } +} diff --git a/Classes/Service/EelRuntime.php b/Classes/Service/EelRuntime.php new file mode 100644 index 0000000..59fafe4 --- /dev/null +++ b/Classes/Service/EelRuntime.php @@ -0,0 +1,109 @@ +taskContext = $taskContext; + } + + /** + * @return bool + */ + public function isTaskContext(): bool + { + return $this->taskContext === self::CONTEXT_TASK; + } + + /** + * @return bool + */ + public function isTestContext(): bool + { + return $this->taskContext === self::CONTEXT_TEST; + } + + /** + * @param Result $chainResult + */ + public function setChainResult(Result $chainResult) + { + $this->chainResult = $chainResult; + } + + /** + * @return Result + */ + public function getChainResult(): Result + { + return $this->chainResult; + } + + /** + * @return array + */ + protected function getDefaultContextVariables() + { + if ($this->defaultContextVariables === null) { + $this->defaultContextVariables = array(); + $this->defaultContextVariables = EelUtility::getDefaultContextVariables($this->eelContext); + } + return $this->defaultContextVariables; + } + + /** + * @param string $expression + * @return mixed + */ + public function evaluate(string $expression) + { + $evaluator = new InterpretedEvaluator(); + return EelUtility::evaluateEelExpression($expression, $evaluator, $this->getDefaultContextVariables()); + } +} diff --git a/Classes/Service/ReadyTaskRunner.php b/Classes/Service/ReadyTaskRunner.php new file mode 100644 index 0000000..316f9a7 --- /dev/null +++ b/Classes/Service/ReadyTaskRunner.php @@ -0,0 +1,54 @@ +onBeforeTask($onBeforeTest); + } + + /** + * @param \Closure $onTestResult + */ + public function onTestResult(\Closure $onTestResult) + { + $this->onTaskResult($onTestResult); + } +} diff --git a/Classes/Task/AbstractTask.php b/Classes/Task/AbstractTask.php new file mode 100644 index 0000000..1113fe5 --- /dev/null +++ b/Classes/Task/AbstractTask.php @@ -0,0 +1,101 @@ +name = $name; + $this->options = $options; + $this->result = new Result(); + $this->validateOptions($options); + } + + /** + * @param array $options + */ + protected function validateOptions(array $options) + { + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getSuccessLabel(): string + { + return 'Done'; + } + + /** + * @return string + */ + public function getErrorLabel(): string + { + $error = $this->result->getFirstError(); + return $error ? $error->getMessage() : 'Failed'; + } + + /** + * @return string + */ + public function getNoticeLabel(): string + { + return $this->result->getFirstNotice()->getMessage(); + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * @return Result + */ + public function getResult(): Result + { + return $this->result; + } +} diff --git a/Classes/Task/CommandTask.php b/Classes/Task/CommandTask.php new file mode 100644 index 0000000..f65d103 --- /dev/null +++ b/Classes/Task/CommandTask.php @@ -0,0 +1,55 @@ +options['command'], $this->flowSettings, false, $this->options['arguments'] ?? []); + if (!$success) { + throw new SubProcessException(sprintf('Command "%s" did not return true', $this->options['command']), 1511529104); + } + } +} diff --git a/Classes/Task/EelTask.php b/Classes/Task/EelTask.php new file mode 100644 index 0000000..c609d08 --- /dev/null +++ b/Classes/Task/EelTask.php @@ -0,0 +1,51 @@ +runtime->evaluate($this->options['expression']); + } +} diff --git a/Classes/Task/RedisTask.php b/Classes/Task/RedisTask.php new file mode 100644 index 0000000..a7da016 --- /dev/null +++ b/Classes/Task/RedisTask.php @@ -0,0 +1,55 @@ +connect($this->options['hostname']); + $redis->select($this->options['database']); + $success = $redis->rawCommand($this->options['command'], ...($this->options['arguments'] ?? [])); + + if (!$success) { + throw new \RedisException($redis->getLastError()); + } + } +} diff --git a/Classes/Task/TaskInterface.php b/Classes/Task/TaskInterface.php new file mode 100644 index 0000000..d2579b5 --- /dev/null +++ b/Classes/Task/TaskInterface.php @@ -0,0 +1,52 @@ +test(); + if (!$passed) { + $this->result->addError(new Error($this->getErrorLabel())); + } + } + + /** + * @return string + */ + public function getSuccessLabel(): string + { + return 'Ready'; + } +} diff --git a/Classes/Test/BeanstalkTest.php b/Classes/Test/BeanstalkTest.php new file mode 100644 index 0000000..6a0213b --- /dev/null +++ b/Classes/Test/BeanstalkTest.php @@ -0,0 +1,38 @@ +options['hostname']); + return $beanstalkClient->getConnection()->isServiceListening(); + } +} diff --git a/Classes/Test/DatabaseTest.php b/Classes/Test/DatabaseTest.php new file mode 100644 index 0000000..ff9020c --- /dev/null +++ b/Classes/Test/DatabaseTest.php @@ -0,0 +1,46 @@ +options['hostname'], + $this->options['username'] ?? null, + $this->options['password'] ?? null + ); + return true; + } catch (\PDOException $exception) { + return false; + } + } +} diff --git a/Classes/Test/DoctrineTest.php b/Classes/Test/DoctrineTest.php new file mode 100644 index 0000000..1771d0f --- /dev/null +++ b/Classes/Test/DoctrineTest.php @@ -0,0 +1,42 @@ +entityManager = $entityManager; + } + + + /** + * @return bool + */ + public function test(): bool + { + $databaseConnection = $this->entityManager->getConnection(); + return $databaseConnection->isConnected() || $databaseConnection->connect(); + } +} diff --git a/Classes/Test/EelTest.php b/Classes/Test/EelTest.php new file mode 100644 index 0000000..30d681a --- /dev/null +++ b/Classes/Test/EelTest.php @@ -0,0 +1,46 @@ +runtime->setTaskContext('test'); + $result = $this->runtime->evaluate($this->options['expression']); + return $result ?: false; + } +} diff --git a/Classes/Test/ElasticSearchTest.php b/Classes/Test/ElasticSearchTest.php new file mode 100644 index 0000000..898e0e7 --- /dev/null +++ b/Classes/Test/ElasticSearchTest.php @@ -0,0 +1,44 @@ +objectManager->get(ElasticSearchFactory::class); + $elasticSearchClient = $elasticSearchFactory->create(); + $response = $elasticSearchClient->request('GET', '/_cluster/health'); + return $response->getTreatedContent()['status'] !== 'red'; + } +} diff --git a/Classes/Test/RedisTest.php b/Classes/Test/RedisTest.php new file mode 100644 index 0000000..7cdd56e --- /dev/null +++ b/Classes/Test/RedisTest.php @@ -0,0 +1,43 @@ +connect($this->options['hostname']); + + if ($result) { + $redis->close(); + } + + return $result; + } +} diff --git a/Classes/Test/TestInterface.php b/Classes/Test/TestInterface.php new file mode 100644 index 0000000..780ae74 --- /dev/null +++ b/Classes/Test/TestInterface.php @@ -0,0 +1,22 @@ +