Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Improve command dispatcher API #407

Merged
merged 1 commit into from
Jan 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 31 additions & 10 deletions Classes/Command/CacheCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
*
*/

use Helhum\Typo3Console\Core\Booting\RunLevel;
use Helhum\Typo3Console\Core\ConsoleBootstrap;
use Helhum\Typo3Console\Mvc\Cli\CommandDispatcher;
use Helhum\Typo3Console\Mvc\Controller\CommandController;
use Helhum\Typo3Console\Service\CacheService;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException;

/**
Expand All @@ -24,31 +24,52 @@
class CacheCommandController extends CommandController
{
/**
* @var \Helhum\Typo3Console\Service\CacheService
* @inject
* @var CacheService
*/
protected $cacheService;
private $cacheService;

/**
* @var CommandDispatcher
*/
private $commandDispatcher;

/**
* @param CacheService $cacheService
* @param CommandDispatcher $commandDispatcher
*/
public function __construct(CacheService $cacheService, CommandDispatcher $commandDispatcher = null)
{
$this->cacheService = $cacheService;
$this->commandDispatcher = $commandDispatcher ?: CommandDispatcher::createFromCommandRun();
}

/**
* Flush all caches
*
* Flushes TYPO3 core caches first and after that, flushes caches from extensions.
*
* @param bool $force Cache is forcibly flushed (low level operations are performed)
* @throws \Helhum\Typo3Console\Mvc\Cli\FailedSubProcessCommandException
*/
public function flushCommand($force = false)
{
$this->cacheService->flush($force);
$this->commandDispatcher->executeCommand('cache:flushcomplete');
$this->outputLine(sprintf('%slushed all caches.', $force ? 'Force f' : 'F'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol @ %slushed ;-)

}

// TODO: use nicer API once available
ConsoleBootstrap::getInstance()->requestRunLevel(RunLevel::LEVEL_FULL);

/**
* Called only internally in a sub process of the cache:flush command
*
* This command will then use the full TYPO3 bootstrap.
* @internal
*/
public function flushCompleteCommand()
{
// Flush a second time to have extension caches and previously disabled core caches cleared when clearing not forced
$this->cacheService->flush();
// Also call the data handler API to cover legacy hook subscriber code
$this->cacheService->flushCachesWithDataHandler();

$this->outputLine(sprintf('%slushed all caches.', $force ? 'Force f' : 'F'));
}

/**
Expand Down
3 changes: 1 addition & 2 deletions Classes/Core/ConsoleBootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,8 @@ public function run(\Composer\Autoload\ClassLoader $classLoader)
}

/**
* TODO: Add other API that does not depend on bootstrap
*
* @param string $runLevel
* @deprecated Will be removed with 5.0
*/
public function requestRunLevel($runLevel)
{
Expand Down
47 changes: 29 additions & 18 deletions Classes/Install/CliSetupRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
*/

use Helhum\Typo3Console\Install\Status\RedirectStatus;
use Helhum\Typo3Console\Mvc\Cli\CommandDispatcher;
use Helhum\Typo3Console\Mvc\Cli\CommandManager;
use Helhum\Typo3Console\Mvc\Cli\ConsoleOutput;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Cli\CommandArgumentDefinition;
use TYPO3\CMS\Extbase\Mvc\Controller\Argument;
use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Install\Controller\Action\ActionInterface;
use TYPO3\CMS\Install\Controller\Action\Step\StepInterface;
use TYPO3\CMS\Install\Controller\Exception\RedirectException;
Expand All @@ -35,26 +39,22 @@ class CliSetupRequestHandler
const INSTALL_COMMAND_CONTROLLER_CLASS = \Helhum\Typo3Console\Command\InstallCommandController::class;

/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManager
* @inject
* @var ObjectManager
*/
protected $objectManager;

/**
* @var \Helhum\Typo3Console\Mvc\Cli\CommandManager
* @inject
* @var CommandManager
*/
protected $commandManager;

/**
* @var \Helhum\Typo3Console\Mvc\Cli\CommandDispatcher
* @inject
* @var CommandDispatcher
*/
protected $dispatcher;
private $commandDispatcher;

/**
* @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
* @inject
* @var ReflectionService
*/
protected $reflectionService;

Expand Down Expand Up @@ -85,13 +85,24 @@ class CliSetupRequestHandler
protected $interactiveSetup = true;

/**
* Creates a new output object during object creation
* CliSetupRequestHandler constructor.
*
* @param ObjectManager $objectManager
* @param CommandManager $commandManager
* @param ReflectionService $reflectionService
* @param CommandDispatcher $commandDispatcher
*/
public function initializeObject()
{
if ($this->output === null) {
$this->output = $this->objectManager->get(\Helhum\Typo3Console\Mvc\Cli\ConsoleOutput::class);
}
public function __construct(
ObjectManager $objectManager,
CommandManager $commandManager,
ReflectionService $reflectionService,
CommandDispatcher $commandDispatcher = null
) {
$this->objectManager = $objectManager;
$this->commandManager = $commandManager;
$this->reflectionService = $reflectionService;
$this->commandDispatcher = $commandDispatcher ?: CommandDispatcher::createFromCommandRun();
$this->output = new \Helhum\Typo3Console\Mvc\Cli\ConsoleOutput($output, $input);
}

/**
Expand Down Expand Up @@ -210,7 +221,7 @@ protected function dispatchAction($actionName)
$this->output->outputLine();
$this->output->outputLine(sprintf('%s:', $command->getShortDescription()));

if (!$this->dispatcher->executeCommand('install:' . strtolower($actionName) . 'needsexecution')) {
if (!$this->commandDispatcher->executeCommand('install:' . strtolower($actionName) . 'needsexecution')) {
$this->output->outputLine('<info>No execution needed, skipped step!</info>');
return;
}
Expand Down Expand Up @@ -264,10 +275,10 @@ protected function dispatchAction($actionName)
}

do {
$messages = @unserialize($this->dispatcher->executeCommand('install:' . strtolower($actionName), $actionArguments));
$messages = @unserialize($this->commandDispatcher->executeCommand('install:' . strtolower($actionName), $actionArguments));
} while (!empty($messages[0]) && $messages[0] instanceof RedirectStatus);

$stillNeedsExecution = (bool)$this->dispatcher->executeCommand('install:' . strtolower($actionName) . 'needsexecution');
$stillNeedsExecution = (bool)$this->commandDispatcher->executeCommand('install:' . strtolower($actionName) . 'needsexecution');
if ($stillNeedsExecution) {
if (empty($messages)) {
$warning = new WarningStatus();
Expand Down
153 changes: 127 additions & 26 deletions Classes/Mvc/Cli/CommandDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,149 @@
*
*/

use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\ProcessBuilder;

/**
* Class CommandDispatcher
* This class can be used to execute console commands in a sub process
* It is especially useful during initial TYPO3 setup, but also when commands
* need a minimal bootstrap first, but the execute some actions with a fill bootstrap
* like e.g. the cache:flush command
*/
class CommandDispatcher
{
/**
* @param string $commandIdentifier
* @param array $arguments
* @return string Json encoded output of the executed command
* @throws \Exception
* @var ProcessBuilder
*/
public function executeCommand($commandIdentifier, $arguments = [])
private $processBuilder;

/**
* Don't allow object creation without factory method
*
* @param ProcessBuilder $processBuilder
*/
private function __construct(ProcessBuilder $processBuilder)
{
$commandLine = isset($_SERVER['argv']) ? $_SERVER['argv'] : [];
$this->processBuilder = $processBuilder;
}

$processBuilder = new ProcessBuilder();
$processBuilder->setPrefix(PHP_BINARY);
$processBuilder->add(array_shift($commandLine));
$processBuilder->add($commandIdentifier);
/**
* Create the dispatcher from within a composer plugin context
*
* Provide the composer bin-dir as search dir to cover most cases
*
* @param array $searchDirs Directories to look for the typo3cms binary.
* @param ExecutableFinder $binFinder
* @param ProcessBuilder $processBuilder
* @param PhpExecutableFinder $phpFinder
* @return self
*/
public static function createFromComposerRun(array $searchDirs, ExecutableFinder $binFinder = null, ProcessBuilder $processBuilder = null, PhpExecutableFinder $phpFinder = null)
{
$binFinder = $binFinder ?: new ExecutableFinder();
$name = 'typo3cms';
$suffixes = [''];
if ('\\' === DIRECTORY_SEPARATOR) {
$suffixes[] = '.bat';
}
// The finder first looks in the system directories and then in the
// user-defined ones. We want to check the user-defined ones first.
foreach ($searchDirs as $dir) {
foreach ($suffixes as $suffix) {
$file = $dir . DIRECTORY_SEPARATOR . $name . $suffix;
if (is_file($file) && ('\\' === DIRECTORY_SEPARATOR || is_executable($file))) {
$typo3cmsCommandPath = $file;
break 2;
}
}
}
if (!isset($typo3cmsCommandPath)) {
$typo3cmsCommandPath = $binFinder->find($name);
}
$processBuilder = $processBuilder ?: new ProcessBuilder();
$processBuilder->addEnvironmentVariables(['TYPO3_CONSOLE_PLUGIN_RUN' => true]);

return self::create($typo3cmsCommandPath, $processBuilder, $phpFinder);
}

/**
* Useful for creating the object during the runtime of another command
*
* Just use the method without arguments for best results
*
* @param ProcessBuilder $processBuilder
* @param PhpExecutableFinder $phpFinder
* @return self
*/
public static function createFromCommandRun(ProcessBuilder $processBuilder = null, PhpExecutableFinder $phpFinder = null)
{
if (!isset($_SERVER['argv'][0]) && strpos($_SERVER['argv'][0], 'typo3cms') === false) {
throw new \RuntimeException('Tried to create typo3cms command runner from wrong context', 1484945065);
}
$typo3cmsCommandPath = $_SERVER['argv'][0];
return self::create($typo3cmsCommandPath, $processBuilder, $phpFinder);
}

/**
* Basic factory method, which need the exact path to the typo3cms binary to create the dispatcher
*
* @param string $typo3cmsCommandPath Absolute path to the typo3cms binary
* @param ProcessBuilder $processBuilder
* @param PhpExecutableFinder $phpFinder
* @return self
*/
public static function create($typo3cmsCommandPath, ProcessBuilder $processBuilder = null, PhpExecutableFinder $phpFinder = null)
{
$processBuilder = $processBuilder ?: new ProcessBuilder();
$content = file_get_contents($typo3cmsCommandPath, null, null, -1, 18);
if ($content === '#!/usr/bin/env php' || strpos($content, '<?php') === 0) {
$phpFinder = $phpFinder ?: new PhpExecutableFinder();
if (!($php = $phpFinder->find())) {
throw new \RuntimeException('The "php" binary could not be found.', 1485128615);
}
$processBuilder->setPrefix($php);
$processBuilder->add($typo3cmsCommandPath);
} else {
$processBuilder->setPrefix($typo3cmsCommandPath);
}
return new self($processBuilder);
}

/**
* Execute a command in a sub process
*
* @param string $command Command identifier
* @param array $arguments Argument names will automatically be converted to dashed version, fi not provided like so
* @return string Output of the executed command
* @throws FailedSubProcessCommandException
*/
public function executeCommand($command, array $arguments = [])
{
// We need to clone the builder to be able to re-use the object for multiple commands
$processBuilder = clone $this->processBuilder;

$processBuilder->add($command);
foreach ($arguments as $argumentName => $argumentValue) {
$dashedName = ucfirst($argumentName);
$dashedName = preg_replace('/([A-Z][a-z0-9]+)/', '$1-', $dashedName);
$dashedName = '--' . strtolower(substr($dashedName, 0, -1));
if (strpos($argumentName, '--') === 0) {
$dashedName = $argumentName;
} else {
$dashedName = ucfirst($argumentName);
$dashedName = preg_replace('/([A-Z][a-z0-9]+)/', '$1-', $dashedName);
$dashedName = '--' . strtolower(substr($dashedName, 0, -1));
}
$processBuilder->add($dashedName);
$processBuilder->add($argumentValue);
if ($argumentValue !== null) {
$processBuilder->add($argumentValue);
}
}

$process = $processBuilder->getProcess();
$exitCode = $process->run();
$output = trim($process->getOutput());
$errorOutput = trim($process->getErrorOutput());

if ($exitCode > 0) {
throw new \ErrorException(sprintf(
'Executing %s failed with message:' . LF . LF . '"%s"' . LF . LF . 'and error:' . LF . LF . '"%s"' . LF,
$commandIdentifier,
$output,
$errorOutput
));
$process->run();
$output = str_replace("\r\n", "\n", trim($process->getOutput()));

if (!$process->isSuccessful()) {
throw FailedSubProcessCommandException::forProcess($command, $process);
}

return $output;
Expand Down