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 @@
+