<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Mvc\Cli\Symfony\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

use Helhum\Typo3Console\Mvc\Cli\Command as CommandDefinition;
use Helhum\Typo3Console\Mvc\Cli\RequestHandler;
use Helhum\Typo3Console\Mvc\Cli\Symfony\Application;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Wrapper to turn a command controller commands into a Symfony Command
 */
class CommandControllerCommand extends Command
{
    /**
     * @var CommandDefinition
     */
    private $commandDefinition;

    /**
     * @var Application
     */
    private $application;

    /**
     * @var bool
     */
    private $isLateCommand;

    public function __construct($name, CommandDefinition $commandDefinition, bool $isLateCommand = false)
    {
        $this->commandDefinition = $commandDefinition;
        parent::__construct($name);
        $this->isLateCommand = $isLateCommand;
    }

    /**
     * @return bool
     */
    public function isLateCommand(): bool
    {
        return $this->isLateCommand;
    }

    public function getNativeDefinition()
    {
        $commandInputDefinition = new InputDefinition($this->commandDefinition->getInputDefinitions(true));
        $commandInputDefinition->addOptions($this->application->getDefinition()->getOptions());

        return $commandInputDefinition;
    }

    public function getRelatedCommandNames()
    {
        return $this->commandDefinition->getRelatedCommandIdentifiers();
    }

    /**
     * Extbase has its own validation logic, so it is disabled in this place
     */
    protected function configure()
    {
        $this->setDescription($this->commandDefinition->getShortDescription());
        $this->setHelp($this->commandDefinition->getDescription());
        $strict = $this->commandDefinition->shouldValidateInputStrict();
        if (!$strict) {
            $this->ignoreValidationErrors();
        }
        $this->setDefinition($this->commandDefinition->getInputDefinitions());
    }

    /**
     * Sets the application instance for this command.
     * It is used later for isEnabled()
     *
     * @param BaseApplication $application An Application instance
     * @throws RuntimeException
     */
    public function setApplication(BaseApplication $application = null)
    {
        if ($application !== null && !$application instanceof Application) {
            throw new RuntimeException('Command controller commands only work with TYPO3 Console Applications', 1506381781);
        }
        $this->application = $application;
        parent::setApplication($application);
    }

    public function getSynopsis($short = false)
    {
        return sprintf('%s %s', $this->getName(), $this->commandDefinition->getSynopsis($short));
    }

    /**
     * Executes the command to find any Extbase command
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return null|int null or 0 if everything went fine, or an error code
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // @deprecated in 5.0 will be removed in 6.0
        $givenCommandName = $input->getArgument('command');
        $debugOutput = $output;
        if ($output instanceof ConsoleOutput) {
            $debugOutput = $output->getErrorOutput();
        }
        if ($givenCommandName === $this->getAliases()[0]) {
            $debugOutput->writeln('<warning>Specifying the full command name is deprecated.</warning>');
            $debugOutput->writeln(sprintf('<warning>Please use "%s" as command name instead.</warning>', $this->getName()));
        }
        if ($this->isLateCommand && $output->isVerbose()) {
            $debugOutput->writeln('<warning>Registering commands via $GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'extbase\'][\'commandControllers\'] is deprecated and will be removed with 6.0. Register Symfony commands in Configuration/Commands.php instead.</warning>');
        }
        $response = (new RequestHandler())->handle($this->commandDefinition, $input, $output);

        return $response->getExitCode();
    }

    public function isHidden(): bool
    {
        if ($this->application->isComposerManaged()
            && in_array($this->getName(), [
                // Hide commands than don't make sense when application is composer managed, but should still work for a while
                'extension:activate',
                'extension:deactivate',
                'extension:removeinactive',
            ], true)
        ) {
            return true;
        }

        return $this->commandDefinition->isInternal();
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Core;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Composer\Autoload\ClassLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Helhum\Typo3Console\Core\Booting\CompatibilityScripts;
use Helhum\Typo3Console\Core\Booting\RunLevel;
use Helhum\Typo3Console\Core\Booting\Scripts;
use Helhum\Typo3Console\Exception;
use Helhum\Typo3Console\Mvc\Cli\CommandCollection;
use Helhum\Typo3Console\Mvc\Cli\CommandConfiguration;
use Helhum\Typo3Console\Mvc\Cli\Symfony\Application;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * @internal
 */
class Kernel
{
    /**
     * @var ClassLoader
     */
    private $classLoader;

    /**
     * @var Bootstrap
     */
    private $bootstrap;

    /**
     * @var RunLevel
     */
    private $runLevel;

    /**
     * @var bool
     */
    private $initialized = false;

    /**
     * @var ClassLoader
     */
    public static $nonComposerCompatClassLoader;

    public function __construct(\Composer\Autoload\ClassLoader $classLoader)
    {
        $this->classLoader = $classLoader;
        $this->ensureRequiredEnvironment();
        $this->bootstrap = Bootstrap::getInstance();
        $this->bootstrap->initializeClassLoader($classLoader);
        // Initialize basic annotation loader until TYPO3 does so as well
        AnnotationRegistry::registerLoader('class_exists');
        $this->runLevel = new RunLevel($this->bootstrap);
    }

    /**
     * Checks PHP sapi type and sets required PHP options
     */
    private function ensureRequiredEnvironment()
    {
        if (!in_array(PHP_SAPI, ['cli', 'phpdbg'], true) || !isset($_SERVER['argc'], $_SERVER['argv'])) {
            echo 'The command line must be executed with a cli PHP binary! The current PHP sapi type is "' . PHP_SAPI . '".' . PHP_EOL;
            exit(1);
        }
        if (ini_get('memory_limit') !== '-1') {
            @ini_set('memory_limit', '-1');
        }
        if (ini_get('max_execution_time') !== '0') {
            @ini_set('max_execution_time', '0');
        }
    }

    /**
     * If detected TYPO3 version does not match the main supported version,
     * overlay compatibility classes for the detected branch, by registering
     * an autoloader and aliasing the compatibility class with the original class name.
     *
     * @param ClassLoader $classLoader
     * @internal
     */
    public static function initializeCompatibilityLayer(ClassLoader $classLoader)
    {
        $typo3Branch = '95';
        if (method_exists(Bootstrap::class, 'setCacheHashOptions')) {
            $typo3Branch = '87';
        }
        if ($typo3Branch === '95') {
            return;
        }
        $classLoader = self::$nonComposerCompatClassLoader ?? $classLoader;
        $compatibilityNamespace = 'Helhum\\Typo3Console\\TYPO3v' . $typo3Branch . '\\';
        spl_autoload_register(function ($className) use ($classLoader, $compatibilityNamespace) {
            if (strpos($className, 'Helhum\\Typo3Console\\') !== 0) {
                // We don't care about classes that are not within our namespace
                return;
            }
            $compatibilityClassName = str_replace('Helhum\\Typo3Console\\', $compatibilityNamespace, $className);
            if ($file = $classLoader->findFile($compatibilityClassName)) {
                require $file;
                class_alias($compatibilityClassName, $className);
            }
        }, true, true);
    }

    /**
     * This is useful to bootstrap the console application
     * without actually executing a command (e.g. during composer install)
     *
     * @param string $runLevel
     * @throws Exception
     * @throws InvalidArgumentException
     */
    public function initialize(string $runLevel = null)
    {
        if (!$this->initialized) {
            self::initializeCompatibilityLayer($this->classLoader);
            Scripts::baseSetup($this->bootstrap);
            $this->initialized = true;
        }
        if ($runLevel !== null) {
            $this->runLevel->runSequence($runLevel);
        }
    }

    /**
     * Handle the given command input and return the exit code of the called command
     *
     * @param InputInterface $input
     * @throws Exception
     * @throws InvalidArgumentException
     * @return int
     */
    public function handle(InputInterface $input): int
    {
        $this->initialize();

        $commandCollection = new CommandCollection(
            $this->runLevel,
            new CommandConfiguration(GeneralUtility::makeInstance(PackageManager::class))
        );

        $application = new Application($this->runLevel, CompatibilityScripts::isComposerMode());
        $application->setCommandLoader($commandCollection);

        // Try to resolve short command names and aliases
        $givenCommandName = $input->getFirstArgument() ?: 'list';
        $commandNameCandidate = $commandCollection->find($givenCommandName);
        if ($this->runLevel->isCommandAvailable($commandNameCandidate)) {
            $this->runLevel->runSequenceForCommand($commandNameCandidate);
            // @deprecated will be removed once command controller support is removed
            if ($this->runLevel->getRunLevelForCommand($commandNameCandidate) !== RunLevel::LEVEL_ESSENTIAL) {
                $commandCollection->addCommandControllerCommands($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'] ?? []);
                $commandName = $commandCollection->find($givenCommandName);
                if ($commandNameCandidate !== $commandName) {
                    // Mitigate #779 and #778 at least when command controller commands conflict with non low level
                    // previously registered commands
                    $this->runLevel->runSequenceForCommand($commandName);
                }
            }
        }

        return $application->run($input);
    }

    /**
     * Finish the current request and exit with the given exit code
     *
     * @param int $exitCode
     */
    public function terminate(int $exitCode = 0)
    {
        if ($exitCode > 255) {
            $exitCode = 255;
        }
        exit($exitCode);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Install;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Install\Upgrade\SilentConfigurationUpgrade;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Install\Controller\InstallerController;

/**
 * This class is responsible for properly creating install tool step actions
 * and executing them if needed
 */
class InstallStepActionExecutor
{
    /**
     * @var SilentConfigurationUpgrade
     */
    private $silentConfigurationUpgrade;

    /**
     * @var InstallerController
     */
    private $installerController;
    /**
     * @var callable
     */
    private $requestFactory;

    /**
     * @param SilentConfigurationUpgrade $silentConfigurationUpgrade
     */
    public function __construct(SilentConfigurationUpgrade $silentConfigurationUpgrade, InstallerController $installerController, callable $requestFactory = null)
    {
        $this->silentConfigurationUpgrade = $silentConfigurationUpgrade;
        $this->installerController = $installerController;
        $this->requestFactory = $requestFactory ?: function (array $arguments) {
            return (new ServerRequest())->withParsedBody(
                [
                    'install' => [
                        'values' => $arguments,
                    ],
                ]
            );
        };
    }

    /**
     * Executes the given action and returns their response messages
     *
     * @param string $actionName Name of the install step
     * @param array $arguments Arguments for the install step
     * @param bool $dryRun If true, do not execute the action, but only check if execution is necessary
     * @return InstallStepResponse
     */
    public function executeActionWithArguments($actionName, array $arguments = [], $dryRun = false)
    {
        $actionMethod = 'execute' . ucfirst($actionName) . 'Action';
        $checkMethod = 'check' . ucfirst($actionName) . 'Action';
        $messages = [];
        $needsExecution = file_exists(PATH_site . 'FIRST_INSTALL');
        if (is_callable([$this->installerController, $checkMethod])) {
            $needsExecution = !\json_decode((string)$this->installerController->$checkMethod()->getBody(), true)['success'];
        }
        if ($needsExecution && !$dryRun) {
            $request = ($this->requestFactory)($arguments);
            try {
                $response = \json_decode((string)$this->installerController->$actionMethod($request)->getBody(), true);
                if (!$response['success']) {
                    $messages = $response['status'];
                }
            } catch (\Throwable $e) {
                $messages = [
                    [
                        'severity' => AbstractMessage::ERROR,
                        'message' => $e->getMessage(),
                    ],
                ];
            }
            $this->silentConfigurationUpgrade->executeSilentConfigurationUpgradesIfNeeded();
            $needsExecution = false;
        }

        return new InstallStepResponse($needsExecution, $messages);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Error;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Mvc\Cli\FailedSubProcessCommandException;
use Helhum\Typo3Console\Mvc\Cli\SubProcessException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;

class ExceptionRenderer
{
    /**
     * @var Terminal
     */
    private $terminal;

    public function __construct(Terminal $terminal = null)
    {
        $this->terminal = $terminal ?: new Terminal();
    }

    /**
     * Renders Exception with trace and nested exceptions with trace.
     *
     * @param \Throwable $exception
     * @param OutputInterface $output
     * @param Application|null $application
     */
    public function render(\Throwable $exception, OutputInterface $output, Application $application = null)
    {
        if (getenv('TYPO3_CONSOLE_SUB_PROCESS')) {
            $output->write(\json_encode($this->serializeException($exception)), false, OutputInterface::VERBOSITY_QUIET);

            return;
        }
        $output->writeln('', OutputInterface::VERBOSITY_QUIET);
        do {
            $this->outputException($exception, $output);
            if ($output->isVerbose()) {
                $this->outputCode($exception, $output);
                $this->outputCommand($exception, $output);
                $this->outputTrace($exception, $output);
                $output->writeln('');
            }
            $exception = $exception->getPrevious();
            if ($exception) {
                $output->writeln('<comment>Caused by:</comment>', OutputInterface::VERBOSITY_QUIET);
            }
        } while ($exception);

        $this->outputSynopsis($output, $application);
    }

    /**
     * Output formatted exception.
     *
     * @param \Throwable $exception
     * @param OutputInterface $output
     */
    private function outputException(\Throwable $exception, OutputInterface $output)
    {
        $exceptionClass = get_class($exception);
        if ($exception instanceof SubProcessException) {
            $exceptionClass = $exception->getPreviousExceptionClass();
        }

        $title = sprintf('[ %s ]', $exceptionClass);

        $messageLength = Helper::strlen($title);
        $maxWidth = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;

        $lines = [];
        foreach (preg_split('/\r?\n/', trim($exception->getMessage())) as $line) {
            foreach ($this->splitStringByWidth($line, $maxWidth - 4) as $splitLine) {
                $lines[] = $splitLine;
                $messageLength = max(Helper::strlen($splitLine), $messageLength);
            }
        }

        $messages = [];
        $messages[] = $emptyLine = $this->padMessage('', $messageLength);
        $messages[] = $this->padMessage($title, $messageLength);
        foreach ($lines as $line) {
            $messages[] = $this->padMessage(OutputFormatter::escape($line), $messageLength);
        }
        $messages[] = $emptyLine;
        $messages[] = '';
        $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
    }

    /**
     * @param \Throwable $exception
     * @param OutputInterface $output
     */
    private function outputCode(\Throwable $exception, OutputInterface $output)
    {
        if ($exception->getCode() > 0) {
            $output->writeln(sprintf('<comment>Exception code:</comment> <info>%s</info>', $exception->getCode()));
            $output->writeln('');
        }
    }

    /**
     * @param \Throwable $exception
     * @param OutputInterface $output
     */
    private function outputCommand(\Throwable $exception, OutputInterface $output)
    {
        if ($exception instanceof FailedSubProcessCommandException || ($exception instanceof SubProcessException && $exception->getCommandLine())) {
            $output->writeln('<comment>Command line:</comment>');
            $output->writeln($exception->getCommandLine());
            $output->writeln('');
            if ($exception->getOutputMessage()) {
                $output->writeln('<comment>Command output:</comment>');
                $output->writeln($exception->getOutputMessage());
                $output->writeln('');
            }
            if ($exception->getErrorMessage()) {
                $output->writeln('<comment>Command error output:</comment>');
                $output->writeln($exception->getErrorMessage());
                $output->writeln('');
            }
        }
    }

    /**
     * Output formatted trace.
     *
     * @param \Throwable $exception
     */
    private function outputTrace(\Throwable $exception, OutputInterface $output)
    {
        $output->writeln('<comment>Exception trace:</comment>');
        $backtraceSteps = $this->getTrace($exception);
        foreach ($backtraceSteps as $index => $step) {
            $traceLine = '#' . $index . ' ';
            if (isset($backtraceSteps[$index]['class'])) {
                $traceLine .= $backtraceSteps[$index]['class'];
            }
            if (isset($backtraceSteps[$index]['function'])) {
                $traceLine .= (isset($backtraceSteps[$index]['class']) ? $backtraceSteps[$index]['type'] : '') . $backtraceSteps[$index]['function'] . '()';
            }
            $output->writeln(sprintf('<info>%s</info>', $traceLine));
            if (isset($backtraceSteps[$index]['file'])) {
                $output->writeln('   ' . $this->getPossibleShortenedFileName($backtraceSteps[$index]['file']) . (isset($backtraceSteps[$index]['line']) ? ':' . $backtraceSteps[$index]['line'] : ''));
            }
        }
    }

    private function outputSynopsis(OutputInterface $output, Application $application = null)
    {
        if (!$application || getenv('TYPO3_CONSOLE_SUB_PROCESS')) {
            return;
        }
        \Closure::bind(function () use (&$runningCommand, $application) {
            $property = 'runningCommand';
            $runningCommand = $application->$property;
        }, null, Application::class)();

        if ($runningCommand !== null) {
            $output->writeln(sprintf('<info>%s</info>', sprintf($runningCommand->getSynopsis(), $application->getName())), OutputInterface::VERBOSITY_QUIET);
            $output->writeln('', OutputInterface::VERBOSITY_QUIET);
        }
    }

    private function splitStringByWidth($string, $width)
    {
        // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
        // additionally, array_slice() is not enough as some character has doubled width.
        // we need a function to split string not by character count but by string width
        if (false === $encoding = mb_detect_encoding($string, null, true)) {
            return str_split($string, $width);
        }

        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
        $lines = [];
        $line = '';
        foreach (preg_split('//u', $utf8String) as $char) {
            // test if $char could be appended to current line
            if (mb_strwidth($line . $char, 'utf8') <= $width) {
                $line .= $char;
                continue;
            }
            // if not, push current line to array and make new line
            $lines[] = str_pad($line, $width);
            $line = $char;
        }

        $lines[] = count($lines) ? str_pad($line, $width) : $line;
        mb_convert_variables($encoding, 'utf8', $lines);

        return $lines;
    }

    /**
     * Right pad message.
     *
     * @param string $message
     * @param int $maxLength
     * @return string
     */
    private function padMessage($message, $maxLength): string
    {
        return '<error>  ' . $message . str_pad('', $maxLength - strlen($message), ' ') . '  </error>';
    }

    /**
     * Shorten file name if inside extension or core extension.
     *
     * @param string $fileName
     * @return string
     */
    private function getPossibleShortenedFileName($fileName): string
    {
        $pathPrefixes = [];
        if (getenv('TYPO3_PATH_COMPOSER_ROOT')) {
            $pathPrefixes = [getenv('TYPO3_PATH_COMPOSER_ROOT') . '/'];
        }
        $pathPrefixes[] = PATH_site;
        $fileName = str_replace($pathPrefixes, '', $fileName);
        $pathPosition = strpos($fileName, 'typo3conf/ext/');
        $pathAndFilename = ($pathPosition !== false) ? substr($fileName, $pathPosition) : $fileName;
        $pathPosition = strpos($pathAndFilename, 'typo3/sysext/');

        return ($pathPosition !== false) ? substr($pathAndFilename, $pathPosition) : $pathAndFilename;
    }

    /**
     * @param \Throwable $exception
     * @return array|null
     */
    private function serializeException(\Throwable $exception = null)
    {
        $serializedException = null;
        if ($exception) {
            $exceptionClass = get_class($exception);
            $line = $exception->getLine();
            $file = $exception->getFile();
            if ($exception instanceof SubProcessException) {
                $exceptionClass = $exception->getPreviousExceptionClass();
                $line = $exception->getPreviousExceptionLine();
                $file = $exception->getPreviousExceptionFile();
            } elseif ($exception instanceof FailedSubProcessCommandException) {
                $backtraceSteps = $exception->getTrace();
                $line = $backtraceSteps[1]['line'];
                $file = $backtraceSteps[1]['file'];
                $commandLine = $exception->getCommandLine();
                $outputMessage = $exception->getOutputMessage();
                $errorMessage = $exception->getErrorMessage();
            }
            $serializedException = [
                'class' => $exceptionClass,
                'line' => $line,
                'file' => $file,
                'message' => $exception->getMessage(),
                'code' => $exception->getCode(),
                'trace' => $this->getTrace($exception),
                'previous' => $this->serializeException($exception->getPrevious()),
                'commandline' => $commandLine ?? null,
                'output' => $outputMessage ?? null,
                'error' => $errorMessage ?? null,
            ];
        }

        return $serializedException;
    }

    private function getTrace(\Throwable $exception): array
    {
        $backtraceSteps = $exception->getTrace();
        if ($exception instanceof SubProcessException) {
            $backtraceSteps = $exception->getPreviousExceptionTrace();
        } elseif ($exception instanceof FailedSubProcessCommandException) {
            array_shift($backtraceSteps);
        } else {
            array_unshift($backtraceSteps, [
                'function' => '',
                'file' => $exception->getFile() ?: 'n/a',
                'line' => $exception->getLine() ?: 'n/a',
                'args' => [],
            ]);
        }

        return $backtraceSteps;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Mvc\Controller\CommandController;

/**
 * A Command Controller which provides help for available commands
 *
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
 */
class HelpCommandController extends CommandController
{
    /**
     * Displays an error message
     *
     * @internal
     * @param \TYPO3\CMS\Extbase\Mvc\Exception\CommandException $exception
     * @return void
     */
    public function errorCommand(\TYPO3\CMS\Extbase\Mvc\Exception\CommandException $exception)
    {
        $this->outputLine('<error>%s</error>', [$exception->getMessage()]);
        if ($exception instanceof \TYPO3\CMS\Extbase\Mvc\Exception\AmbiguousCommandIdentifierException) {
            $this->outputLine('Please specify the complete command identifier. Matched commands:');
            foreach ($exception->getMatchingCommands() as $matchingCommand) {
                $this->outputLine('    %s', [$matchingCommand->getCommandIdentifier()]);
            }
        }
        $this->outputLine();
        $this->outputLine('See <info>list</info> for an overview of all available commands');
        $this->outputLine('or <info>help</info> <command> for a detailed description of the corresponding command.');
        $this->quit(1);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Annotation\Command\Definition;
use Helhum\Typo3Console\Database\Configuration\ConnectionConfiguration;
use Helhum\Typo3Console\Database\Process\MysqlCommand;
use Helhum\Typo3Console\Database\Schema\SchemaUpdateResultRenderer;
use Helhum\Typo3Console\Database\Schema\SchemaUpdateType;
use Helhum\Typo3Console\Mvc\Controller\CommandController;
use Helhum\Typo3Console\Service\Database\SchemaService;
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;

/**
 * Database command controller
 */
class DatabaseCommandController extends CommandController
{
    /**
     * @var SchemaService
     */
    private $schemaService;

    /**
     * @var SchemaUpdateResultRenderer
     */
    private $schemaUpdateResultRenderer;

    /**
     * @var ConnectionConfiguration
     */
    private $connectionConfiguration;

    public function __construct(SchemaService $schemaService, SchemaUpdateResultRenderer $schemaUpdateResultRenderer, ConnectionConfiguration $connectionConfiguration)
    {
        $this->schemaService = $schemaService;
        $this->schemaUpdateResultRenderer = $schemaUpdateResultRenderer;
        $this->connectionConfiguration = $connectionConfiguration;
    }

    /**
     * Update database schema (TYPO3 Database Compare)
     *
     * Compares the current database schema with schema definition
     * from extensions's ext_tables.sql files and updates the schema based on the definition.
     *
     * Valid schema update types are:
     *
     * - field.add
     * - field.change
     * - field.prefix
     * - field.drop
     * - table.add
     * - table.change
     * - table.prefix
     * - table.drop
     * - safe (includes all necessary operations, to add or change fields or tables)
     * - destructive (includes all operations which rename or drop fields or tables)
     *
     * The list of schema update types supports wildcards to specify multiple types, e.g.:
     *
     * - "*" (all updates)
     * - "field.*" (all field updates)
     * - "*.add,*.change" (all add/change updates)
     *
     * To avoid shell matching all types with wildcards should be quoted.
     *
     * Example: %command.full_name% "*.add,*.change"
     *
     * @param array $schemaUpdateTypes List of schema update types (default: "safe")
     * @param bool $dryRun If set the updates are only collected and shown, but not executed
     * @Definition\Argument(name="schemaUpdateTypes")
     */
    public function updateSchemaCommand(array $schemaUpdateTypes = ['safe'], $dryRun = false)
    {
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        try {
            $expandedSchemaUpdateTypes = SchemaUpdateType::expandSchemaUpdateTypes($schemaUpdateTypes);
        } catch (InvalidEnumerationValueException $e) {
            $this->outputLine(sprintf('<error>%s</error>', $e->getMessage()));
            $this->quit(1);
        }

        $result = $this->schemaService->updateSchema($expandedSchemaUpdateTypes, $dryRun);

        if ($result->hasPerformedUpdates()) {
            $this->output->outputLine('<info>The following database schema updates %s performed:</info>', [$dryRun ? 'should be' : 'were']);
            $this->schemaUpdateResultRenderer->render($result, $this->output, $verbose);
        } else {
            $this->output->outputLine(
                '<info>No schema updates %s performed for update type%s:%s</info>',
                [
                    $dryRun ? 'must be' : 'were',
                    count($expandedSchemaUpdateTypes) > 1 ? 's' : '',
                    PHP_EOL . '"' . implode('", "', $expandedSchemaUpdateTypes) . '"',
                ]
            );
        }
        if ($result->hasErrors()) {
            $this->outputLine();
            $this->output->outputLine('<error>The following errors occurred:</error>');
            $this->schemaUpdateResultRenderer->renderErrors($result, $this->output, $verbose);
            $this->quit(1);
        }
    }

    /**
     * Import mysql queries from stdin
     *
     * This means that this can not only be used to pass insert statements,
     * it but works as well to pass SELECT statements to it.
     * The mysql binary must be available in the path for this command to work.
     * This obviously only works when MySQL is used as DBMS.
     *
     * Example (import): ssh remote.server '/path/to/typo3cms database:export' | %command.full_name%
     * Example (select): echo 'SELECT username from be_users WHERE admin=1;' | %command.full_name%
     * Example (interactive): %command.full_name% --interactive
     *
     * @param bool $interactive Open an interactive mysql shell using the TYPO3 connection settings.
     * @param string $connection TYPO3 database connection name
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
     */
    public function importCommand($interactive = false, string $connection = 'Default')
    {
        $availableConnectionNames = $this->connectionConfiguration->getAvailableConnectionNames('mysql');
        if (empty($availableConnectionNames) || !in_array($connection, $availableConnectionNames, true)) {
            $this->output('<error>No suitable MySQL connection found for import.</error>');
            $this->quit(2);
        }

        $mysqlCommand = new MysqlCommand($this->connectionConfiguration->build($connection), [], $this->output->getSymfonyConsoleOutput());
        $exitCode = $mysqlCommand->mysql(
            $interactive ? [] : ['--skip-column-names'],
            STDIN,
            null,
            $interactive
        );
        $this->quit($exitCode);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Install\Upgrade;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Mvc\Cli\ConsoleOutput;
use Symfony\Component\Console\Helper\TableSeparator;

/**
 * Renders a list of upgrade wizards
 */
class UpgradeWizardListRenderer
{
    /**
     * Renders a table for upgrade wizards
     *
     * @param array $upgradeWizardList
     * @param ConsoleOutput $output
     * @param mixed $verbose
     */
    public function render(array $upgradeWizardList, ConsoleOutput $output, $verbose = false)
    {
        if (empty($upgradeWizardList)) {
            $output->outputLine('<info>None</info>');

            return;
        }
        $tableHeader = ['Identifier', 'Title'];
        if ($verbose) {
            $tableHeader = ['Identifier', 'Description'];
        }

        $tableRows = [];
        foreach ($upgradeWizardList as $identifier => $info) {
            $row = [
                $identifier,
                wordwrap($info['title'], 40),
            ];
            if ($verbose) {
                $row = [
                    $identifier,
                    wordwrap($info['explanation'], 40),
                ];
            }
            $tableRows[] = $row;
            $tableRows[] = new TableSeparator();
        }
        array_pop($tableRows);

        $output->outputTable($tableRows, $tableHeader);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Install\Upgrade;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Core\Booting\CompatibilityScripts;
use Helhum\Typo3Console\Extension\ExtensionCompatibilityCheck;
use Helhum\Typo3Console\Extension\ExtensionConstraintCheck;
use Helhum\Typo3Console\Mvc\Cli\CommandDispatcher;
use Helhum\Typo3Console\Mvc\Cli\ConsoleOutput;
use Helhum\Typo3Console\Mvc\Cli\FailedSubProcessCommandException;
use Helhum\Typo3Console\Service\Configuration\ConfigurationService;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Updates\AbstractDownloadExtensionUpdate;
use TYPO3\CMS\Install\Updates\DatabaseCharsetUpdate;

/**
 * Executes a single upgrade wizard
 * Holds the information on possible user interactions
 */
class UpgradeHandling
{
    /**
     * @var UpgradeWizardFactory
     */
    private $factory;

    /**
     * @var UpgradeWizardExecutor
     */
    private $executor;

    /**
     * @var SilentConfigurationUpgrade
     */
    private $silentConfigurationUpgrade;

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

    /**
     * @var UpgradeWizardList
     */
    private $upgradeWizardList;

    /**
     * @var ConfigurationService
     */
    private $configurationService;

    /**
     * @var PackageManager
     */
    private $packageManager;

    /**
     * @var ExtensionConstraintCheck
     */
    private $extensionConstraintCheck;

    /**
     * @var ExtensionCompatibilityCheck
     */
    private $extensionCompatibilityCheck;

    /**
     * Flag for same process
     *
     * @var bool
     */
    private $initialUpgradeDone = false;

    /**
     * Wizards that have a user interaction with resulting argument
     *
     * @var array
     */
    private static $extensionWizardArguments = [['name' => 'install', 'type' => 'bool', 'default' => '0']];

    public function __construct(
        UpgradeWizardFactory $factory = null,
        UpgradeWizardExecutor $executor = null,
        UpgradeWizardList $upgradeWizardList = null,
        SilentConfigurationUpgrade $silentConfigurationUpgrade = null,
        CommandDispatcher $commandDispatcher = null,
        ConfigurationService $configurationService = null,
        PackageManager $packageManager = null,
        ExtensionConstraintCheck $extensionConstraintCheck = null,
        ExtensionCompatibilityCheck $extensionCompatibilityCheck = null
    ) {
        $this->factory = $factory ?: new UpgradeWizardFactory();
        $this->executor = $executor ?: new UpgradeWizardExecutor($this->factory);
        $this->upgradeWizardList = $upgradeWizardList ?: new UpgradeWizardList();
        $this->silentConfigurationUpgrade = $silentConfigurationUpgrade ?: new SilentConfigurationUpgrade();
        $this->commandDispatcher = $commandDispatcher ?: CommandDispatcher::createFromCommandRun();
        $this->configurationService = $configurationService ?: new ConfigurationService();
        $this->packageManager = $packageManager ?: GeneralUtility::makeInstance(PackageManager::class);
        $this->extensionConstraintCheck = $extensionConstraintCheck ?: new ExtensionConstraintCheck();
        $this->extensionCompatibilityCheck = $extensionCompatibilityCheck ?: new ExtensionCompatibilityCheck($this->packageManager, $this->commandDispatcher);
    }

    public function executeWizard(string $identifier, array $rawArguments = [], bool $force = false): UpgradeWizardResult
    {
        return $this->executor->executeWizard($identifier, $rawArguments, $force);
    }

    public function executeAll(array $arguments, ConsoleOutput $consoleOutput, array &$messages = []): array
    {
        $consoleOutput->progressStart(rand(6, 9));
        $consoleOutput->progressAdvance();

        $wizards = $this->executeInSubProcess('listWizards', [], $messages);

        $consoleOutput->progressStart(count($wizards['scheduled']) + 2);

        $results = [];
        if (!empty($wizards['scheduled'])) {
            foreach ($wizards['scheduled'] as $shortIdentifier => $wizardOptions) {
                $consoleOutput->progressAdvance();
                if (is_subclass_of($wizardOptions['className'], AbstractDownloadExtensionUpdate::class)) {
                    foreach (self::$extensionWizardArguments as $argumentDefinition) {
                        $argumentName = $argumentDefinition['name'];
                        $argumentDefault = $argumentDefinition['default'];
                        if ($this->wizardHasArgument($shortIdentifier, $argumentName, $arguments)) {
                            continue;
                        }
                        if (CompatibilityScripts::isComposerMode()) {
                            // In composer mode, skip all install extension wizards!
                            $arguments[] = sprintf('%s[%s]=%s', $shortIdentifier, $argumentName, $argumentDefault);
                            $messages[] = '<warning>Wizard "' . $shortIdentifier . '" was not executed but only marked as executed due to composer mode.</warning>';
                        } elseif ($argumentDefinition['type'] === 'bool') {
                            // We currently only handle one argument type
                            $wizard = $this->factory->create($wizardOptions['className']);
                            $consoleOutput->outputLine(PHP_EOL . PHP_EOL . '<info>' . $wizard->getTitle() . '</info>' . PHP_EOL);
                            if (is_callable([$wizard, 'getUserInput'])) {
                                $consoleOutput->outputLine(implode(PHP_EOL, array_filter(array_map('trim', explode(chr(10), html_entity_decode(strip_tags($wizard->getUserInput(''))))))));
                            }
                            $consoleOutput->outputLine();
                            $arguments[] = sprintf(
                                '%s[%s]=%s',
                                    $shortIdentifier,
                                    $argumentName,
                                    (string)(int)$consoleOutput->askConfirmation('<comment>Install (y/N)</comment> ', $argumentDefault)
                            );
                        }
                    }
                }
                $wizardMessages = [];
                $results[$shortIdentifier] = $this->executeInSubProcess('executeWizard', [$shortIdentifier, $arguments], $wizardMessages);
                $messages = array_merge($messages, $wizardMessages);
            }
        }

        $consoleOutput->progressAdvance();

        $this->commandDispatcher->executeCommand('database:updateschema');

        $consoleOutput->progressFinish();

        return $results;
    }

    private function wizardHasArgument(string $identifier, string $argumentName, array $arguments): bool
    {
        foreach ($arguments as $argument) {
            if (strpos($argument, sprintf('%s[%s]', $identifier, $argumentName)) !== false) {
                return true;
            }
            if (strpos($argument, '[') === false && strpos($argument, $argumentName) !== false) {
                return true;
            }
        }

        return false;
    }

    public function listWizards(): array
    {
        return [
            'scheduled' => $this->upgradeWizardList->listWizards(),
            'done' => $this->upgradeWizardList->listWizards(true),
        ];
    }

    public function matchExtensionConstraints(string $extensionKey, string $typo3Version): string
    {
        return $this->extensionConstraintCheck->matchConstraints($this->packageManager->getPackage($extensionKey), $typo3Version);
    }

    public function matchAllExtensionConstraints(string $typo3Version): array
    {
        return $this->extensionConstraintCheck->matchAllConstraints($this->packageManager->getActivePackages(), $typo3Version);
    }

    public function isCompatible(string $extensionKey, bool $configOnly = false): bool
    {
        return $this->extensionCompatibilityCheck->isCompatible($extensionKey, $configOnly);
    }

    public function findIncompatible(): array
    {
        return $this->extensionCompatibilityCheck->findIncompatible();
    }

    /**
     * Execute the command in a sub process,
     * but execute some automated migration steps beforehand
     *
     * @param string $command
     * @param array $arguments
     * @param array &$messages
     * @throws FailedSubProcessCommandException
     * @throws \UnexpectedValueException
     * @throws \RuntimeException
     * @return mixed
     */
    public function executeInSubProcess($command, array $arguments = [], array &$messages = [])
    {
        $messages = $this->ensureUpgradeIsPossible();

        return @unserialize($this->commandDispatcher->executeCommand('upgrade:subprocess', [$command, serialize($arguments)]));
    }

    /**
     * @throws FailedSubProcessCommandException
     * @throws \UnexpectedValueException
     * @throws \RuntimeException
     * @return string[]
     */
    private function ensureUpgradeIsPossible()
    {
        $messages = [];
        if (!$this->initialUpgradeDone
            && (
                    !$this->configurationService->hasLocal('EXTCONF/helhum-typo3-console/initialUpgradeDone')
                    || TYPO3_branch !== $this->configurationService->getLocal('EXTCONF/helhum-typo3-console/initialUpgradeDone')
                )
        ) {
            $this->initialUpgradeDone = true;
            $this->configurationService->setLocal('EXTCONF/helhum-typo3-console/initialUpgradeDone', TYPO3_branch, 'string');
            $this->commandDispatcher->executeCommand('install:fixfolderstructure');
            $messages = $this->ensureExtensionCompatibility();
            $this->silentConfigurationUpgrade->executeSilentConfigurationUpgradesIfNeeded();
            // TODO: Check what we can do here to get TYPO3 9 support for this feature
            if (class_exists(DatabaseCharsetUpdate::class)) {
                $this->commandDispatcher->executeCommand('upgrade:wizard', [DatabaseCharsetUpdate::class]);
            }
            $this->commandDispatcher->executeCommand('cache:flush');
            $this->commandDispatcher->executeCommand('database:updateschema');
        }

        return $messages;
    }

    /**
     * @return string[]
     */
    private function ensureExtensionCompatibility()
    {
        $messages = [];
        $failedPackageMessages = $this->matchAllExtensionConstraints(TYPO3_version);
        foreach ($failedPackageMessages as $extensionKey => $constraintMessage) {
            $this->packageManager->deactivatePackage($extensionKey);
            $messages[] = sprintf('<error>%s</error>', $constraintMessage);
            $messages[] = sprintf('<info>Deactivated extension "%s".</info>', $extensionKey);
        }
        foreach ($this->extensionCompatibilityCheck->findIncompatible() as $extensionKey) {
            $messages[] = sprintf('<error>Extension "%s" seems to be not compatible or broken</error>', $extensionKey);
            $messages[] = sprintf('<info>Deactivated extension "%s".</info>', $extensionKey);
        }

        return $messages;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Extension;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Database\Schema\SchemaUpdateResultRenderer;
use Helhum\Typo3Console\Database\Schema\SchemaUpdateType;
use Helhum\Typo3Console\Mvc\Cli\ConsoleOutput;
use Helhum\Typo3Console\Service\Database\SchemaService;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;

/**
 * Class to collect and render results of extension setup
 */
class ExtensionSetupResultRenderer
{
    /**
     * @var array
     */
    private $results = [];

    /**
     * Mapping between method name at this object and signal slot
     *
     * @var array
     */
    private static $signalsToRegister = [
        [
            'signalClass' => InstallUtility::class,
            'signalName' => 'afterExtensionFileImport',
            'resultName' => 'renderExtensionFileImportResult',
        ],
        [
            'signalClass' => InstallUtility::class,
            'signalName' => 'afterExtensionStaticSqlImport',
            'resultName' => 'renderImportedStaticDataResult',
        ],
        [
            'signalClass' => InstallUtility::class,
            'signalName' => 'afterExtensionT3DImport',
            'resultName' => 'renderExtensionDataImportResult',
        ],
        [
            'signalClass' => SchemaService::class,
            'signalName' => 'afterDatabaseUpdate',
            'resultName' => 'renderSchemaResult',
        ],
    ];

    public function __construct(Dispatcher $signalSlotDispatcher)
    {
        $collector = $this;
        foreach (static::$signalsToRegister as $signalConfig) {
            $signalSlotDispatcher->connect(
                $signalConfig['signalClass'],
                $signalConfig['signalName'],
                \Closure::bind(function ($result) use ($signalConfig, $collector) {
                    $collector->results[$signalConfig['resultName']][] = $result;
                }, null, $this)
            );
        }
    }

    /**
     * Output schema result
     * @param ConsoleOutput $output
     */
    public function renderSchemaResult(ConsoleOutput $output, SchemaUpdateResultRenderer $schemaUpdateResultRenderer = null)
    {
        if (!isset($this->results['renderSchemaResult'])) {
            return;
        }
        $result = reset($this->results['renderSchemaResult']);
        if ($result->hasPerformedUpdates()) {
            $schemaUpdateResultRenderer = $schemaUpdateResultRenderer ?: new SchemaUpdateResultRenderer();
            $output->outputLine('<info>The following database schema updates were performed:</info>');
            $schemaUpdateResultRenderer->render($result, $output, true);
        } else {
            $output->outputLine(
                '<info>No schema updates were performed for update types:%s</info>',
                [
                    PHP_EOL . '"' . implode('", "', SchemaUpdateType::expandSchemaUpdateTypes(['safe'])) . '"',
                ]
            );
        }
    }

    /**
     * Output at cli context imported static database content.
     *
     * @param ConsoleOutput $output
     */
    public function renderImportedStaticDataResult(ConsoleOutput $output)
    {
        if (!isset($this->results['renderImportedStaticDataResult'])) {
            return;
        }
        foreach ($this->results['renderImportedStaticDataResult'] as $pathToStaticSqlFile) {
            // Output content of $pathToStaticSqlFile at cli context
            $absolutePath = PATH_site . $pathToStaticSqlFile;
            if (file_exists($absolutePath)) {
                $output->outputFormatted('<info>Import content of file "%s" into database.</info>', [$pathToStaticSqlFile]);
                $output->outputFormatted(file_get_contents($absolutePath), [], 2);
            }
        }
    }

    /**
     * Output at cli context initialized imported files to $destinationAbsolutePath
     *
     * @param ConsoleOutput $output
     */
    public function renderExtensionFileImportResult(ConsoleOutput $output)
    {
        if (!isset($this->results['renderExtensionFileImportResult'])) {
            return;
        }
        foreach ($this->results['renderExtensionFileImportResult'] as $destinationAbsolutePath) {
            $output->outputFormatted(
                '<info>Files from extension was imported to path "%s"</info>',
                [PathUtility::stripPathSitePrefix($destinationAbsolutePath)]
            );
        }
    }

    /**
     * Output at cli context initialized imported files to $destinationAbsolutePath
     *
     * @param ConsoleOutput $output
     */
    public function renderExtensionDataImportResult(ConsoleOutput $output)
    {
        if (!isset($this->results['renderExtensionDataImportResult'])) {
            return;
        }
        foreach ($this->results['renderExtensionDataImportResult'] as $importedFile) {
            $output->outputFormatted(
                '<info>Data from from file "%s" was imported</info>',
                [$importedFile]
            );
        }
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Command\Delegation\ReferenceIndexUpdateDelegate;
use Helhum\Typo3Console\Mvc\Controller\CommandController;
use Helhum\Typo3Console\Service\Persistence\PersistenceIntegrityService;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class CleanupCommandController extends CommandController
{
    /**
     * @var PersistenceIntegrityService
     */
    protected $persistenceIntegrityService;

    public function __construct(PersistenceIntegrityService $persistenceIntegrityService)
    {
        $this->persistenceIntegrityService = $persistenceIntegrityService;
    }

    /**
     * Update reference index
     *
     * Updates reference index to ensure data integrity
     *
     * Example: %command.full_name% --dry-run --verbose
     *
     * @param bool $dryRun If set, index is only checked without performing any action
     * @param bool $showProgress Whether or not to output a progress bar
     */
    public function updateReferenceIndexCommand($dryRun = false, $showProgress = false)
    {
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        $this->outputLine('<info>' . ($dryRun ? 'Checking' : 'Updating') . ' reference index. This may take a while …</info>');

        $operation = $dryRun ? 'checkReferenceIndex' : 'updateReferenceIndex';

        list($errorCount, $recordCount, $processedTables) = $this->persistenceIntegrityService->{$operation}(
            $this->createReferenceIndexDelegateWithOptions($dryRun, $verbose, $showProgress)
        );

        if ($errorCount > 0) {
            $this->outputLine('<info>%d errors were ' . ($dryRun ? 'found' : 'fixed') . ', while ' . ($dryRun ? 'checking' : 'updating') . ' reference index for %d records from %d tables.</info>', [$errorCount, $recordCount, count($processedTables)]);
        } else {
            $this->outputLine();
            $this->outputLine('<info>Index integrity was perfect!</info>');
        }
    }

    /**
     * @param bool $dryRun
     * @param bool $verbose
     * @param bool $showProgress
     * @return ReferenceIndexUpdateDelegate
     */
    protected function createReferenceIndexDelegateWithOptions($dryRun, $verbose, $showProgress)
    {
        $delegate = new ReferenceIndexUpdateDelegate($this->createLogger($verbose, $showProgress));
        if ($showProgress) {
            $output = $this->output;
            $delegate->subscribeEvent(
                'willStartOperation',
                function ($max) use ($output) {
                    $output->progressStart($max);
                }
            );
            $delegate->subscribeEvent(
                'willUpdateRecord',
                function () use ($output) {
                    $output->progressAdvance();
                }
            );
            $delegate->subscribeEvent(
                'operationHasEnded',
                function () use ($output) {
                    $output->progressFinish();
                }
            );
        }

        if (!$dryRun) {
            $delegate->subscribeEvent(
                'operationHasEnded',
                function () {
                    GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class)->set('core', 'sys_refindex_lastUpdate', $GLOBALS['EXEC_TIME']);
                }
            );
        }

        return $delegate;
    }

    /**
     * @param bool $verbose
     * @param bool $addNewLines
     * @return LoggerInterface
     */
    protected function createLogger($verbose, $addNewLines = false)
    {
        $options = [];
        if ($addNewLines) {
            $options['messageWrap'] = LF . LF . '|' . LF;
        }

        return parent::createDefaultLogger($verbose ? LogLevel::DEBUG : LogLevel::WARNING, $options);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Service\Database;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Database\Schema\SchemaUpdateInterface;
use Helhum\Typo3Console\Database\Schema\SchemaUpdateResult;
use Helhum\Typo3Console\Database\Schema\SchemaUpdateType;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;

/**
 * Service for database schema migrations
 */
class SchemaService implements SingletonInterface
{
    /**
     * @var SchemaUpdateInterface
     */
    private $schemaUpdate;

    /**
     * @var Dispatcher
     */
    private $signalSlotDispatcher;

    /**
     * SchemaService constructor.
     *
     * @param SchemaUpdateInterface $schemaUpdate
     * @param Dispatcher $signalSlotDispatcher
     */
    public function __construct(SchemaUpdateInterface $schemaUpdate, Dispatcher $signalSlotDispatcher)
    {
        $this->schemaUpdate = $schemaUpdate;
        $this->signalSlotDispatcher = $signalSlotDispatcher;
    }

    /**
     * Perform necessary database schema migrations
     *
     * @param SchemaUpdateType[] $schemaUpdateTypes List of permitted schema update types
     * @param bool $dryRun If true, the database operations are not performed
     * @return SchemaUpdateResult Result of the schema update
     */
    public function updateSchema(array $schemaUpdateTypes, $dryRun = false)
    {
        $updateStatements = [
            SchemaUpdateType::GROUP_SAFE => $this->schemaUpdate->getSafeUpdates(),
            SchemaUpdateType::GROUP_DESTRUCTIVE => $this->schemaUpdate->getDestructiveUpdates(),
        ];

        $updateResult = new SchemaUpdateResult();

        foreach ($schemaUpdateTypes as $schemaUpdateType) {
            foreach ($schemaUpdateType->getStatementTypes() as $statementType => $statementGroup) {
                if (isset($updateStatements[$statementGroup][$statementType])) {
                    $statements = $updateStatements[$statementGroup][$statementType];
                    if (empty($statements)) {
                        continue;
                    }
                    if ($dryRun) {
                        $updateResult->addPerformedUpdates($schemaUpdateType, $statements);
                    } else {
                        $result = $this->schemaUpdate->migrate(
                            $statements,
                            // Generate a map of statements as keys and true as values
                            array_combine(array_keys($statements), array_fill(0, count($statements), true))
                        );
                        if (empty($result)) {
                            $updateResult->addPerformedUpdates($schemaUpdateType, $statements);
                        } else {
                            $updateResult->addErrors($schemaUpdateType, $result, $statements);
                        }
                    }
                }
            }
        }

        $this->emitDatabaseUpdateSignal($updateResult);

        return $updateResult;
    }

    /**
     * Emits database update executed signal
     * @param SchemaUpdateResult $updateResult
     */
    private function emitDatabaseUpdateSignal($updateResult)
    {
        $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterDatabaseUpdate', [$updateResult]);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Core\Booting;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Symfony\Component\Console\Exception\InvalidArgumentException;
use TYPO3\CMS\Core\Core\Bootstrap;

class RunLevel
{
    const LEVEL_ESSENTIAL = 'buildEssentialSequence';
    const LEVEL_COMPILE = 'buildEssentialSequence';
    const LEVEL_MINIMAL = 'buildBasicRuntimeSequence';
    const LEVEL_FULL = 'buildExtendedRuntimeSequence';

    /**
     * @var array
     */
    private $commandOptions = [];

    /**
     * @var array
     */
    private $executedSteps = [];

    /**
     * @var Bootstrap
     */
    private $bootstrap;

    /**
     * @var StepFailedException
     */
    private $error;

    public function __construct(Bootstrap $bootstrap)
    {
        $this->bootstrap = $bootstrap;
    }

    /**
     * @param string $commandIdentifier
     * @param string $runLevel
     * @internal
     */
    public function setRunLevelForCommand(string $commandIdentifier, string $runLevel)
    {
        if (!isset($this->commandOptions[$commandIdentifier]['runLevel'])) {
            $this->commandOptions[$commandIdentifier]['runLevel'] = $runLevel;
        }
    }

    /**
     * @param string $commandIdentifier
     * @param string $stepIdentifier
     * @internal
     */
    public function addBootingStepForCommand(string $commandIdentifier, string $stepIdentifier)
    {
        if (!isset($this->commandOptions[$commandIdentifier]['addSteps'][$stepIdentifier])) {
            $this->commandOptions[$commandIdentifier]['addSteps'][$stepIdentifier] = $stepIdentifier;
        }
    }

    /**
     * @param string $commandIdentifier
     * @param string $stepIdentifier
     * @internal
     */
    public function removeBootingStepForCommand(string $commandIdentifier, string $stepIdentifier)
    {
        if (!isset($this->commandOptions[$commandIdentifier]['removeSteps'])) {
            $this->commandOptions[$commandIdentifier]['removeSteps'][$stepIdentifier] = $stepIdentifier;
        }
    }

    /**
     * @param string $commandIdentifier
     * @throws \Exception
     * @internal
     */
    public function runSequenceForCommand(string $commandIdentifier)
    {
        $sequence = $this->buildSequenceForCommand($commandIdentifier);
        try {
            $sequence->invoke($this->bootstrap);
        } catch (StepFailedException $e) {
            $failedStep = $e->getFailedStep();
            if ($this->isLowLevelStep($failedStep)) {
                // This seems to be a severe error, so we directly throw to expose it
                throw $e->getPrevious();
            }

            $this->error = $e;
        }
    }

    /**
     * @return StepFailedException|null
     */
    public function getError()
    {
        return $this->error;
    }

    /**
     * @param string $commandIdentifier
     * @return bool
     * @internal
     */
    public function isInternalCommand(string $commandIdentifier): bool
    {
        return in_array($commandIdentifier, ['list', 'help'], true);
    }

    private function isLowLevelStep(Step $step): bool
    {
        $runLevelForStep = $this->executedSteps[$step->getIdentifier()];

        return in_array($runLevelForStep, [self::LEVEL_ESSENTIAL, self::LEVEL_COMPILE], true);
    }

    /**
     * @param string $runLevel
     * @internal
     * @throws InvalidArgumentException
     * @throws StepFailedException
     */
    public function runSequence(string $runLevel)
    {
        $this->buildSequence($runLevel)->invoke($this->bootstrap);
    }

    /**
     * Check if we have all mandatory files to assume we have a fully configured / installed TYPO3
     *
     * @return string
     */
    public function getMaximumAvailableRunLevel(): string
    {
        if (!file_exists(PATH_site . 'typo3conf/PackageStates.php') || !file_exists(PATH_site . 'typo3conf/LocalConfiguration.php')) {
            return self::LEVEL_COMPILE;
        }

        return $this->error ? self::LEVEL_COMPILE : self::LEVEL_FULL;
    }

    /**
     * @param string $commandIdentifier
     * @return bool
     */
    public function isCommandAvailable($commandIdentifier): bool
    {
        return $this->getMaximumAvailableRunLevel() === self::LEVEL_FULL
            || $this->getRunLevelForCommand($commandIdentifier) === self::LEVEL_COMPILE;
    }

    /**
     * @param string $commandIdentifier
     * @throws InvalidArgumentException
     * @return Sequence
     */
    private function buildSequenceForCommand(string $commandIdentifier): Sequence
    {
        $sequence = $this->buildSequence($this->getRunLevelForCommand($commandIdentifier));
        $this->addStepsForCommand($sequence, $commandIdentifier);
        $this->removeStepsForCommand($sequence, $commandIdentifier);

        return $sequence;
    }

    /**
     * Builds the sequence for the given run level
     *
     * @param string $runLevel
     * @throws InvalidArgumentException
     * @return Sequence
     */
    private function buildSequence(string $runLevel): Sequence
    {
        if (is_callable([$this, $runLevel])) {
            return $this->{$runLevel}($runLevel);
        }
        throw new InvalidArgumentException('Invalid run level "' . $runLevel . '"', 1402075492);
    }

    /**
     * Essential steps for a minimal usable system
     *
     * @param string $identifier
     * @return Sequence
     */
    private function buildEssentialSequence(string $identifier): Sequence
    {
        $sequence = new Sequence($identifier);
        $this->addStep($sequence, 'helhum.typo3console:coreconfiguration');
        $this->addStep($sequence, 'helhum.typo3console:providecleanclassimplementations');
        $this->addStep($sequence, 'helhum.typo3console:disabledcaching');
        $this->addStep($sequence, 'helhum.typo3console:errorhandling');

        return $sequence;
    }

    /**
     * System with complete configuration, but no database
     *
     * @param string $identifier
     * @return Sequence
     */
    private function buildBasicRuntimeSequence(string $identifier = self::LEVEL_MINIMAL): Sequence
    {
        $sequence = $this->buildEssentialSequence($identifier);

        $this->addStep($sequence, 'helhum.typo3console:extensionconfiguration');

        return $sequence;
    }

    /**
     * Fully capable system with database, persistence configuration (TCA) and authentication available
     *
     * @return Sequence
     */
    private function buildExtendedRuntimeSequence(): Sequence
    {
        $sequence = $this->buildBasicRuntimeSequence(self::LEVEL_FULL);

        $this->addStep($sequence, 'helhum.typo3console:caching');
        // @deprecated helhum.typo3console:database can be removed when TYPO3 8 support is removed
        $this->addStep($sequence, 'helhum.typo3console:database');
        $this->addStep($sequence, 'helhum.typo3console:persistence');
        $this->addStep($sequence, 'helhum.typo3console:authentication');

        return $sequence;
    }

    /**
     * @param Sequence $sequence
     * @param string $stepIdentifier
     * @throws InvalidArgumentException
     */
    private function addStep(Sequence $sequence, string $stepIdentifier)
    {
        if (isset($this->executedSteps[$stepIdentifier])) {
            $sequence->addStep(new Step($stepIdentifier, function () {
                // Don't do anything again, step has been executed already
            }));

            return;
        }

        switch ($stepIdentifier) {
            // Part of essential sequence
            case 'helhum.typo3console:coreconfiguration':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_ESSENTIAL;
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializeConfigurationManagement']));
                break;
            case 'helhum.typo3console:providecleanclassimplementations':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_ESSENTIAL;
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'provideCleanClassImplementations']), 'helhum.typo3console:coreconfiguration');
                break;
            case 'helhum.typo3console:disabledcaching':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_ESSENTIAL;
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializeDisabledCaching']), 'helhum.typo3console:coreconfiguration');
                break;
            case 'helhum.typo3console:errorhandling':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_ESSENTIAL;
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializeErrorHandling']));
                break;

            // Part of basic runtime
            case 'helhum.typo3console:extensionconfiguration':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_MINIMAL;
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializeExtensionConfiguration']));
                break;

            // Part of full runtime
            case 'helhum.typo3console:caching':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_FULL;
                unset($this->executedSteps['helhum.typo3console:disabledcaching']);
                $sequence->removeStep('helhum.typo3console:disabledcaching');
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializeCaching']), 'helhum.typo3console:coreconfiguration');
                break;
            case 'helhum.typo3console:database':
                // @deprecated can be removed if TYPO3 8 support is removed
                $this->executedSteps[$stepIdentifier] = self::LEVEL_FULL;
                $sequence->addStep(new Step($stepIdentifier, [CompatibilityScripts::class, 'initializeDatabaseConnection']), 'helhum.typo3console:errorhandling');
                break;
            case 'helhum.typo3console:persistence':
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializePersistence']), 'helhum.typo3console:extensionconfiguration');
                break;
            case 'helhum.typo3console:authentication':
                $this->executedSteps[$stepIdentifier] = self::LEVEL_FULL;
                $sequence->addStep(new Step($stepIdentifier, [Scripts::class, 'initializeAuthenticatedOperations']), 'helhum.typo3console:extensionconfiguration');
                break;

            default:
                throw new InvalidArgumentException('ERROR: cannot find step for identifier "' . $stepIdentifier . '"', 1402075819);
        }
    }

    /**
     * @param string $commandIdentifier
     * @return string
     * @internal
     */
    public function getRunLevelForCommand(string $commandIdentifier): string
    {
        if ($this->isInternalCommand($commandIdentifier)) {
            return $this->getMaximumAvailableRunLevel() === self::LEVEL_COMPILE ? self::LEVEL_COMPILE : self::LEVEL_MINIMAL;
        }
        $options = $this->getOptionsForCommand($commandIdentifier);

        return $options['runLevel'] ?? self::LEVEL_FULL;
    }

    /**
     * @param Sequence $sequence
     * @param string $commandIdentifier
     * @internal
     */
    private function addStepsForCommand(Sequence $sequence, string $commandIdentifier)
    {
        $options = $this->getOptionsForCommand($commandIdentifier);
        if (isset($options['addSteps'])) {
            foreach ($options['addSteps'] as $stepIdentifier) {
                $this->addStep($sequence, $stepIdentifier);
            }
        }
    }

    /**
     * @param Sequence $sequence
     * @param string $commandIdentifier
     * @internal
     */
    private function removeStepsForCommand(Sequence $sequence, string $commandIdentifier)
    {
        $options = $this->getOptionsForCommand($commandIdentifier);
        if (isset($options['removeSteps'])) {
            foreach ($options['removeSteps'] as $stepIdentifier) {
                $sequence->removeStep($stepIdentifier);
            }
        }
    }

    /**
     * @param string $commandIdentifier
     * @return array|null
     */
    private function getOptionsForCommand(string $commandIdentifier)
    {
        $commandIdentifierPrefix = $commandIdentifier;
        $position = strrpos($commandIdentifier, ':');
        if ($position !== false) {
            $commandIdentifierPrefix = substr($commandIdentifier, 0, $position);
        }

        if (isset($this->commandOptions[$commandIdentifier])) {
            return $this->commandOptions[$commandIdentifier];
        }

        $lookupKey = $commandIdentifierPrefix . ':*';
        if (isset($this->commandOptions[$lookupKey])) {
            return $this->commandOptions[$lookupKey];
        }

        return null;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Mvc\Controller\CommandController;
use Helhum\Typo3Console\Service\CacheService;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException;

/**
 * CommandController for flushing caches
 */
class CacheCommandController extends CommandController
{
    /**
     * @var CacheService
     */
    private $cacheService;

    public function __construct(CacheService $cacheService)
    {
        $this->cacheService = $cacheService;
    }

    /**
     * Flush all caches in specified groups
     *
     * Flushes all caches in specified groups.
     * Valid group names are by default:
     *
     * - lowlevel
     * - pages
     * - system
     *
     * Example: %command.full_name% pages,all
     *
     * @param array $groups An array of names (specified as comma separated values) of cache groups to flush
     */
    public function flushGroupsCommand(array $groups)
    {
        try {
            $this->cacheService->flushGroups($groups);
            $this->outputLine('Flushed all caches for group(s): "' . implode('","', $groups) . '".');
        } catch (NoSuchCacheGroupException $e) {
            $this->outputLine($e->getMessage());
            $this->quit(1);
        }
    }

    /**
     * Flush cache by tags
     *
     * Flushes caches by tags, optionally only caches in specified groups.
     *
     * Example: %command.full_name% news_123 --groups pages,all
     *
     * @param array $tags Array of tags (specified as comma separated values) to flush.
     * @param array $groups Optional array of groups (specified as comma separated values) for which to flush tags. If no group is specified, caches of all groups are flushed.
     */
    public function flushTagsCommand(array $tags, array $groups = null)
    {
        try {
            $this->cacheService->flushByTagsAndGroups($tags, $groups);
            if ($groups === null) {
                $this->outputLine('Flushed caches by tags "' . implode('","', $tags) . '".');
            } else {
                $this->outputLine('Flushed caches by tags "' . implode('","', $tags) . '" in groups: "' . implode('","', $groups) . '".');
            }
        } catch (NoSuchCacheGroupException $e) {
            $this->outputLine($e->getMessage());
            $this->quit(1);
        }
    }

    /**
     * List cache groups
     *
     * Lists all registered cache groups.
     */
    public function listGroupsCommand()
    {
        $groups = $this->cacheService->getValidCacheGroups();
        sort($groups);

        switch (count($groups)) {
            case 0:
                $this->outputLine('No cache groups are registered.');
                break;
            case 1:
                $this->outputLine('The following cache group is registered: "' . implode('", "', $groups) . '".');
                break;
            default:
                $this->outputLine('The following cache groups are registered: "' . implode('", "', $groups) . '".');
                break;
        }
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary\Factory;

use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;
use Knp\DictionaryBundle\ValueTransformer;

class ValueAsKey implements Dictionary\Factory
{
    /**
     * @var ValueTransformer
     */
    protected $transformer;

    public function __construct(ValueTransformer $transformer)
    {
        $this->transformer = $transformer;
    }

    /**
     * {@inheritdoc}
     *
     * @throw InvalidArgumentException if there is some problem with the config.
     */
    public function create(string $name, array $config): Dictionary
    {
        if (!isset($config['content'])) {
            throw new InvalidArgumentException(sprintf(
                'The key content for dictionary %s must be set.',
                $name
            ));
        }

        $content = $config['content'];
        $values  = [];

        foreach ($content as $value) {
            $builtValue          = $this->transformer->transform($value);
            $values[$builtValue] = $builtValue;
        }

        return new Dictionary\Simple($name, $values);
    }

    /**
     * {@inheritdoc}
     */
    public function supports(array $config): bool
    {
        return (isset($config['type'])) ? 'value_as_key' === $config['type'] : false;
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary\Factory;

use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;
use Psr\Container\ContainerInterface;

final class Invokable implements Dictionary\Factory
{
    /**
     * @var ContainerInterface
     */
    private $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     *
     * @throw InvalidArgumentException if there is some problem with the config.
     */
    public function create(string $name, array $config): Dictionary
    {
        if (!isset($config['service'])) {
            throw new InvalidArgumentException(sprintf(
                'The "service" config key must be set for the dictionary named "%s".',
                $name
            ));
        }

        $service  = $this->container->get($config['service']);
        $callable = [$service];

        if (isset($config['method'])) {
            $callable[] = $config['method'];
        }

        if (false === \is_callable($callable)) {
            throw new InvalidArgumentException(sprintf(
                'You must provide a valid callable for the dictionary named "%s".',
                $name
            ));
        }

        return new Dictionary\Invokable($name, $callable);
    }

    /**
     * {@inheritdoc}
     */
    public function supports(array $config): bool
    {
        return (isset($config['type'])) ? 'callable' === $config['type'] : false;
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary\Factory;

use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;

class Combined implements Dictionary\Factory
{
    const TYPE = 'combined';

    /**
     * @var Dictionary\Collection
     */
    private $dictionaries;

    public function __construct(Dictionary\Collection $dictionaries)
    {
        $this->dictionaries = $dictionaries;
    }

    /**
     * {@inheritdoc}
     */
    public function create(string $name, array $config): Dictionary
    {
        if (!isset($config['dictionaries'])) {
            throw new InvalidArgumentException(sprintf(
                'Dictionary of type %s must contains a key "dictionaries".',
                self::TYPE
            ));
        }

        $dictionaries = array_map(function ($name) {
            return $this->dictionaries[$name];
        }, $config['dictionaries']);

        return new Dictionary\Combined($name, $dictionaries);
    }

    /**
     * {@inheritdoc}
     */
    public function supports(array $config): bool
    {
        return isset($config['type']) ? self::TYPE === $config['type'] : false;
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary;

use ArrayAccess;
use Countable;
use IteratorAggregate;
use Knp\DictionaryBundle\Dictionary;
use Knp\DictionaryBundle\Exception\DictionaryNotFoundException;
use RuntimeException;

class DictionaryRegistry implements ArrayAccess, IteratorAggregate, Countable
{
    /**
     * @var Collection
     */
    private $collection;

    public function __construct(Collection $collection)
    {
        @trigger_error(
            sprintf(
                'Class %s is deprecated since version 2.2, to be removed in 3.0. Use %s instead.',
                __CLASS__,
                Collection::class
            ),
            E_USER_DEPRECATED
        );

        $this->collection = $collection;
    }

    /**
     * @var Dictionary[]
     */
    private $dictionaries = [];

    public function add(Dictionary $dictionary): void
    {
        $this->collection->add($dictionary);
    }

    public function set(string $key, Dictionary $dictionary): void
    {
        if (isset($this->collection[$key])) {
            throw new RuntimeException(sprintf(
                'The key "%s" already exists in the dictionary registry.',
                $key
            ));
        }

        $this->add($dictionary);
    }

    /**
     * @return Dictionary[]
     */
    public function all(): array
    {
        return iterator_to_array($this->collection);
    }

    /**
     * @throw DictionaryNotFoundException
     */
    public function get(string $offset): Dictionary
    {
        return $this->collection[$offset];
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($offset)
    {
        return isset($this->collection[$offset]);
    }

    /**
     * {@inheritdoc}
     *
     * @return Dictionary
     *
     * @throw DictionaryNotFoundException
     */
    public function offsetGet($offset)
    {
        if (false === $this->offsetExists($offset)) {
            throw new DictionaryNotFoundException($offset, array_keys($this->dictionaries));
        }

        return $this->get($offset);
    }

    /**
     * {@inheritdoc}
     *
     * @throw RuntimeException
     */
    public function offsetSet($offset, $value): void
    {
        throw new RuntimeException(
            'You can\'t use Knp\DictionaryBundle\Dictionary\Dictionary::offsetSet. Please use '.
            'Knp\DictionaryBundle\Dictionary\Dictionary::set instead.'
        );
    }

    /**
     * {@inheritdoc}
     *
     * @throw RuntimeException
     */
    public function offsetUnset($offset): void
    {
        throw new RuntimeException(
            'You can\'t destroy a dictionary registry value. It\'s used as application '.
            'constants.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function count()
    {
        return \count($this->collection);
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
        return $this->collection->getIterator();
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\DependencyInjection\Compiler;

use Knp\DictionaryBundle\Dictionary\Factory\Aggregate;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class DictionaryFactoryBuildingPass implements CompilerPassInterface
{
    const TAG_FACTORY = 'knp_dictionary.factory';

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container): void
    {
        foreach ($container->findTaggedServiceIds(self::TAG_FACTORY) as $id => $tags) {
            $container
                ->findDefinition(Aggregate::class)
                ->addMethodCall('addFactory', [new Reference($id)])
            ;
        }
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary\Factory;

use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;

class FactoryAggregate implements Dictionary\Factory
{
    /**
     * @var Dictionary\Factory\Aggregate
     */
    private $factory;

    public function __construct(Dictionary\Factory\Aggregate $aggregate)
    {
        @trigger_error(
            sprintf(
                'Class %s is deprecated since version 2.2, to be removed in 3.0. Use %s instead.',
                __CLASS__,
                Dictionary\Factory\Aggregate::class
            ),
            E_USER_DEPRECATED
        );

        $this->factory = $aggregate;
    }

    public function addFactory(Dictionary\Factory $factory): void
    {
        $this->factory->addFactory($factory);
    }

    /**
     * {@inheritdoc}
     *
     * @throw InvalidArgumentException Not able to create a dictionary with the given name
     */
    public function create(string $name, array $config): Dictionary
    {
        return $this->factory->create($name, $config);
    }

    /**
     * {@inheritdoc}
     */
    public function supports(array $config): bool
    {
        return $this->factory->supports($config);
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary\Factory;

use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;

class Aggregate implements Dictionary\Factory
{
    /**
     * @var Dictionary\Factory[]
     */
    private $factories = [];

    public function addFactory(Dictionary\Factory $factory): void
    {
        $this->factories[] = $factory;
    }

    /**
     * {@inheritdoc}
     *
     * @throw InvalidArgumentException Not able to create a dictionary with the given name
     */
    public function create(string $name, array $config): Dictionary
    {
        foreach ($this->factories as $factory) {
            if ($factory->supports($config)) {
                return $factory->create($name, $config);
            }
        }

        throw new InvalidArgumentException(sprintf(
            'The dictionary with named "%s" cannot be created.',
            $name
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function supports(array $config): bool
    {
        foreach ($this->factories as $factory) {
            if ($factory->supports($config)) {
                return true;
            }
        }

        return false;
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Form\Type;

use Knp\DictionaryBundle\Dictionary\Collection;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DictionaryType extends AbstractType
{
    /**
     * @var Collection
     */
    private $dictionaries;

    public function __construct(Collection $dictionaries)
    {
        $this->dictionaries = $dictionaries;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver): void
    {
        $dictionaries = $this->dictionaries;

        $choices = function (Options $options) use ($dictionaries) {
            $name    = $options['name'];
            $choices = $dictionaries[$name]->getValues();

            return array_flip($choices);
        };

        $resolver
            ->setDefault('choices', $choices)
            ->setRequired(['name'])
            ->setAllowedValues('name', array_keys(iterator_to_array($this->dictionaries)));
    }

    /**
     * {@inheritdoc}
     */
    public function getParent()
    {
        return ChoiceType::class;
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary;

use ArrayIterator;
use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;

final class Invokable implements Dictionary
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var array
     */
    private $values;

    /**
     * @var callable
     */
    private $callable;

    /**
     * @var array
     */
    private $callableArgs;

    public function __construct(string $name, callable $callable, array $callableArgs = [])
    {
        $this->name         = $name;
        $this->callable     = $callable;
        $this->callableArgs = $callableArgs;
    }

    /**
     * {@inheritdoc}
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * {@inheritdoc}
     */
    public function getValues(): array
    {
        $this->hydrate();

        return $this->values;
    }

    /**
     * {@inheritdoc}
     */
    public function getKeys(): array
    {
        $this->hydrate();

        return array_keys($this->values);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($offset)
    {
        $this->hydrate();

        return array_key_exists($offset, $this->values);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($offset)
    {
        $this->hydrate();

        return $this->values[$offset];
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($offset, $value): void
    {
        $this->hydrate();

        $this->values[$offset] = $value;
    }

    /**
     * {@inheritdoc}
     */
    public function offsetUnset($offset): void
    {
        $this->hydrate();

        unset($this->values[$offset]);
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
        $this->hydrate();

        return new ArrayIterator($this->values);
    }

    /**
     * Hydrate values from callable.
     */
    private function hydrate(): void
    {
        if (null !== $this->values) {
            return;
        }

        $values = \call_user_func_array($this->callable, $this->callableArgs);

        if (false === \is_array($values)) {
            throw new InvalidArgumentException(
                'Dictionary callable must return an array or an instance of ArrayAccess.'
            );
        }

        $this->values = $values;
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\DependencyInjection\Compiler;

use Knp\DictionaryBundle\Dictionary\Collection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class DictionaryRegistrationPass implements CompilerPassInterface
{
    const TAG_DICTIONARY = 'knp_dictionary.dictionary';

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container): void
    {
        foreach ($container->findTaggedServiceIds(self::TAG_DICTIONARY) as $id => $tags) {
            $container
                ->getDefinition(Collection::class)
                ->addMethodCall('add', [new Reference($id)])
            ;
        }
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary;

use Knp\DictionaryBundle\DataCollector\DictionaryDataCollector;
use Knp\DictionaryBundle\Dictionary;

class Traceable implements Dictionary
{
    /**
     * @var Dictionary
     */
    private $dictionary;

    /**
     * @var DictionaryDataCollector
     */
    private $collector;

    public function __construct(Dictionary $dictionary, DictionaryDataCollector $collector)
    {
        $this->dictionary = $dictionary;
        $this->collector  = $collector;
    }

    /**
     * {@inheritdoc}
     */
    public function getName(): string
    {
        return $this->dictionary->getName();
    }

    /**
     * {@inheritdoc}
     */
    public function getValues(): array
    {
        $this->trace();

        return $this->dictionary->getValues();
    }

    /**
     * {@inheritdoc}
     */
    public function getKeys(): array
    {
        $this->trace();

        return $this->dictionary->getKeys();
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($offset)
    {
        $this->trace();

        return $this->dictionary->offsetExists($offset);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($offset)
    {
        $this->trace();

        return $this->dictionary->offsetGet($offset);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($offset, $value): void
    {
        $this->dictionary->offsetSet($offset, $value);

        $this->trace();
    }

    /**
     * {@inheritdoc}
     */
    public function offsetUnset($offset): void
    {
        $this->dictionary->offsetUnset($offset);

        $this->trace();
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
        $this->trace();

        return $this->dictionary->getIterator();
    }

    /**
     * Register this dictioanry as used.
     */
    private function trace(): void
    {
        $this->collector->addDictionary(
            $this->dictionary->getName(),
            $this->dictionary->getKeys(),
            array_values($this->dictionary->getValues())
        );
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\Dictionary\Factory;

use InvalidArgumentException;
use Knp\DictionaryBundle\Dictionary;

class Extended implements Dictionary\Factory
{
    /**
     * @var Dictionary\Factory
     */
    private $factory;

    /**
     * @var Dictionary\Collection
     */
    private $dictionaries;

    public function __construct(Dictionary\Factory $factory, Dictionary\Collection $dictionaries)
    {
        $this->factory      = $factory;
        $this->dictionaries = $dictionaries;
    }

    /**
     * {@inheritdoc}
     */
    public function create(string $name, array $config): Dictionary
    {
        if (false === $this->factory->supports($config)) {
            throw new InvalidArgumentException(sprintf(
                'The dictionary with named "%s" cannot be created.',
                $name
            ));
        }

        $extends = $config['extends'];

        unset($config['extends']);

        $dictionaries   = [];
        $dictionaries[] = $this->dictionaries[$extends];
        $dictionaries[] = $this->factory->create($name, $config);

        return new Dictionary\Combined($name, $dictionaries);
    }

    /**
     * {@inheritdoc}
     */
    public function supports(array $config): bool
    {
        return isset($config['extends']);
    }
}


<?php

declare(strict_types=1);

namespace Knp\DictionaryBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $builder = new TreeBuilder();
        $builder
            ->root('knp_dictionary')
            ->children()
            ->arrayNode('dictionaries')
                ->useAttributeAsKey('name')
                    ->prototype('array')
                        ->beforeNormalization()
                            ->always()
                            ->then(function ($values) {
                                if (false === array_key_exists('type', $values)) {
                                    if (false === array_key_exists('content', $values)) {
                                        return ['type' => 'value', 'content' => $values];
                                    }

                                    return array_merge($values, ['type' => 'value']);
                                }

                                return $values;
                            })
                        ->end()
                        ->children()
                            ->scalarNode('type')->defaultValue('value')->end()
                            ->scalarNode('extends')->end()
                            ->arrayNode('dictionaries')
                                ->normalizeKeys(false)->prototype('scalar')->end()
                            ->end()
                            ->arrayNode('content')
                                ->normalizeKeys(false)->prototype('scalar')->end()
                            ->end()
                            ->scalarNode('service')->end()
                            ->scalarNode('method')->end()
                        ->end()
                    ->end()
                ->end()
            ->end();

        return $builder;
    }
}