<?php
declare(strict_types=1);
namespace Viserio\Component\Parser\Command;

use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;

/**
 * Validates Yaml files syntax and outputs encountered errors.
 *
 * Some of this code has been ported from Symfony. The original
 *
 * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Yaml/Command/LintCommand.php
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 */
class YamlLintCommand extends AbstractLintCommand
{
    /**
     * {@inheritdoc}
     */
    protected static $defaultName = 'lint:yaml';

    /**
     * {@inheritdoc}
     */
    protected $signature = 'lint:yaml
        [filename : A file or a directory or STDIN.]
        [--format=txt : The output format. Supports `txt` or `json`.]
        [--parse-tags= : Parse custom tags.]
    ';

    /**
     * {@inheritdoc}
     */
    protected $description = 'Lints a Yaml file and outputs encountered errors.';

    /**
     * A Parser instance.
     *
     * @var \Symfony\Component\Yaml\Parser
     */
    private $parser;

    /**
     * {@inheritdoc}
     */
    protected function validate(string $content, ?string $file = null): array
    {
        $prevErrorHandler = \set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler): bool {
            if ($level === \E_USER_DEPRECATED) {
                throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1);
            }

            return $prevErrorHandler !== null ? $prevErrorHandler($level, $message, $file, $line) : false;
        });

        $flags = $this->option('parse-tags') ? Yaml::PARSE_CUSTOM_TAGS : 0;

        try {
            $this->getParser()->parse($content, Yaml::PARSE_CONSTANT | $flags);
        } catch (ParseException $e) {
            return ['file' => $file, 'line' => $e->getParsedLine(), 'valid' => false, 'message' => $e->getMessage()];
        } finally {
            \restore_error_handler();
        }

        return ['file' => $file, 'valid' => true];
    }

    /**
     * {@inheritdoc}
     */
    protected function displayTxt(array $filesInfo, bool $displayCorrectFiles): int
    {
        $countFiles   = \count($filesInfo);
        $erroredFiles = 0;
        $output       = $this->getOutput();

        foreach ($filesInfo as $info) {
            if ($displayCorrectFiles && $info['valid']) {
                $output->comment('<info>OK</info>' . ($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
            } elseif (! $info['valid']) {
                $erroredFiles++;

                $output->text('<error>ERROR</error>' . ($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
                $output->text(\sprintf('<error> >> %s</error>', $info['message']));
            }
        }

        if ($erroredFiles === 0) {
            $output->success(\sprintf('All %d YAML files contain valid syntax.', $countFiles));
        } else {
            $output->warning(\sprintf('%d YAML files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
        }

        return \min($erroredFiles, 1);
    }

    /**
     * Get a parser instance.
     *
     * @return \Symfony\Component\Yaml\Parser
     */
    private function getParser(): Parser
    {
        if ($this->parser === null) {
            $this->parser = new Parser();
        }

        return $this->parser;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Parser\Utils;

use DOMComment;
use DOMDocument;
use DOMElement;
use DOMText;
use SimpleXMLElement;
use Throwable;
use Viserio\Component\Contract\Parser\Exception\FileNotFoundException;
use Viserio\Component\Contract\Parser\Exception\InvalidArgumentException;
use Viserio\Component\Contract\Parser\Exception\ParseException;

/**
 * This file has been ported from Symfony. The original
 * code is (c) Fabien Potencier <fabien@symfony.com>.
 */
final class XmlUtils
{
    /**
     * Private constructor; non-instantiable.
     *
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }

    /**
     * A simplexml_import_dom wrapper.
     *
     * @param \DOMDocument $dom
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\ParseException
     *
     * @return \SimpleXMLElement
     */
    public static function importDom(DOMDocument $dom): SimpleXMLElement
    {
        $xml = \simplexml_import_dom($dom);

        if ($xml === false) {
            throw new ParseException(['message' => 'A failure happend on importing a DOMDocument.']);
        }

        return $xml;
    }

    /**
     * Validates and parses the given file into a DOMDocument.
     *
     * @param \DOMDocument $dom
     * @param string       $schema source of the schema
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\InvalidArgumentException
     *
     * @return array
     */
    public static function validateSchema(DOMDocument $dom, string $schema): array
    {
        $internalErrors  = \libxml_use_internal_errors(true);
        $disableEntities = \libxml_disable_entity_loader(false);
        $isValid         = @$dom->schemaValidateSource($schema);

        if (! $isValid) {
            \libxml_disable_entity_loader($disableEntities);

            return self::getXmlErrors($internalErrors);
        }

        \libxml_disable_entity_loader($disableEntities);

        $dom->normalizeDocument();

        \libxml_clear_errors();
        \libxml_use_internal_errors($internalErrors);

        return [];
    }

    /**
     * @param array $xmlErrors
     *
     * @return string
     */
    public static function getErrorsAsString(array $xmlErrors): string
    {
        $errorsAsString = '';

        foreach ($xmlErrors as $error) {
            $errorsAsString .= \sprintf(
                "[%s %s] %s (in %s - line %d, column %d)\n",
                \LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR',
                $error['code'],
                $error['message'],
                $error['file'],
                $error['line'],
                $error['column']
            );
        }

        return $errorsAsString;
    }

    /**
     * Returns the XML errors of the internal XML parser.
     *
     * @param bool $internalErrors
     *
     * @return array An array of errors
     */
    public static function getXmlErrors(bool $internalErrors): array
    {
        $errors = [];

        foreach (\libxml_get_errors() as $error) {
            $errors[] = [
                'level'   => $error->level === \LIBXML_ERR_WARNING ? 'WARNING' : 'ERROR',
                'code'    => $error->code,
                'message' => \trim($error->message),
                'file'    => $error->file ?? 'n/a',
                'line'    => $error->line,
                'column'  => $error->column,
            ];
        }

        \libxml_clear_errors();
        \libxml_use_internal_errors($internalErrors);

        return $errors;
    }

    /**
     * Loads an XML file.
     *
     * @param string               $file             An XML file path
     * @param null|callable|string $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\InvalidArgumentException When loading of XML file returns error
     * @throws \Viserio\Component\Contract\Parser\Exception\FileNotFoundException
     *
     * @return \DOMDocument
     */
    public static function loadFile(string $file, $schemaOrCallable = null): DOMDocument
    {
        if (! \file_exists($file)) {
            throw new FileNotFoundException(\sprintf('No such file [%s] found.', $file));
        }

        return self::loadString(@\file_get_contents($file), $schemaOrCallable);
    }

    /**
     * Loads an XML string.
     *
     * @param string               $content          An XML string content
     * @param null|callable|string $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\InvalidArgumentException When loading of XML file returns error
     *
     * @return \DOMDocument
     */
    public static function loadString(string $content, $schemaOrCallable = null): DOMDocument
    {
        if (\trim($content) === '') {
            throw new InvalidArgumentException('Content does not contain valid XML, it is empty.');
        }

        $internalErrors  = \libxml_use_internal_errors(true);
        $disableEntities = \libxml_disable_entity_loader();

        \libxml_clear_errors();

        $dom                  = new DOMDocument();
        $dom->validateOnParse = true;

        if (! $dom->loadXML($content, \LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT : 0))) {
            \libxml_disable_entity_loader($disableEntities);

            if ($errors = XliffUtils::validateSchema($dom)) {
                throw new InvalidArgumentException(self::getErrorsAsString($errors));
            }
        }

        $dom->normalizeDocument();

        \libxml_use_internal_errors($internalErrors);
        \libxml_disable_entity_loader($disableEntities);

        foreach ($dom->childNodes as $child) {
            if ($child->nodeType === \XML_DOCUMENT_TYPE_NODE) {
                throw new InvalidArgumentException('Document types are not allowed.');
            }
        }

        if ($schemaOrCallable !== null) {
            self::validateXmlDom($dom, $schemaOrCallable);
        }

        \libxml_clear_errors();
        \libxml_use_internal_errors($internalErrors);

        return $dom;
    }

    /**
     * Converts a \DomElement object to a PHP array.
     *
     * The following rules applies during the conversion:
     *
     *  * Each tag is converted to a key value or an array
     *    if there is more than one "value"
     *
     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
     *    if the tag also has some nested tags
     *
     *  * The attributes are converted to keys (<foo foo="bar"/>)
     *
     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
     *
     * @param \DOMElement $element     A \DomElement instance
     * @param bool        $checkPrefix Check prefix in an element or an attribute name
     *
     * @return null|array|string A PHP array
     */
    public static function convertDomElementToArray(DOMElement $element, bool $checkPrefix = true)
    {
        $prefix = $element->prefix;
        $empty  = true;
        $config = [];

        foreach ($element->attributes as $name => $node) {
            if ($checkPrefix && ! \in_array((string) $node->prefix, ['', $prefix], true)) {
                continue;
            }

            $config[$name] = self::phpize($node->value);
            $empty         = false;
        }

        $nodeValue = false;

        foreach ($element->childNodes as $node) {
            if ($node instanceof DOMText) {
                if (\trim($node->nodeValue) !== '') {
                    $nodeValue = \trim($node->nodeValue);
                    $empty     = false;
                }
            } elseif ($checkPrefix && $prefix !== (string) $node->prefix) {
                continue;
            } elseif (! $node instanceof DOMComment) {
                $value = self::convertDomElementToArray($node, $checkPrefix);
                $key   = $node->localName;

                if (isset($config[$key])) {
                    if (! \is_array($config[$key]) || ! \is_int(\key($config[$key]))) {
                        $config[$key] = [$config[$key]];
                    }

                    $config[$key][] = $value;
                } else {
                    $config[$key] = $value;
                }

                $empty = false;
            }
        }

        if ($nodeValue !== false) {
            $value = self::phpize($nodeValue);

            if (\count($config) !== 0) {
                $config['value'] = $value;
            } else {
                $config = $value;
            }
        }

        return ! $empty ? $config : null;
    }

    /**
     * Converts an xml value to a PHP type.
     *
     * @param mixed $value
     *
     * @return mixed
     */
    public static function phpize($value)
    {
        $value          = (string) $value;
        $lowercaseValue = \mb_strtolower($value);

        switch (true) {
            case 'null' === $lowercaseValue:
                return;
            case \ctype_digit($value):
                return self::transformToNumber($value, 0);
            case isset($value[1]) && '-' === $value[0] && \ctype_digit(\mb_substr($value, 1)):
                return self::transformToNumber($value, 1);
            case 'true' === $lowercaseValue:
                return true;
            case 'false' === $lowercaseValue:
                return false;
            case isset($value[1]) && '0b' === $value[0] . $value[1]:
                return \bindec($value);
            case \is_numeric($value):
                return '0x' === $value[0] . $value[1] ? \hexdec($value) : (float) $value;
            case \preg_match('/^0x[0-9a-f]++$/i', $value):
                return \hexdec($value);
            case \preg_match('/^(-|\+)?\d+(\.\d+)?$/', $value):
                return (float) $value;

            default:
                return $value;
        }
    }

    /**
     * Validates DOMDocument against a file or callback.
     *
     * @param \DOMDocument    $dom
     * @param callable|string $schemaOrCallable
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\InvalidArgumentException
     *
     * @return void
     */
    private static function validateXmlDom(DOMDocument $dom, $schemaOrCallable): void
    {
        $internalErrors = \libxml_use_internal_errors(true);
        \libxml_clear_errors();

        $exception = null;

        if (\is_callable($schemaOrCallable)) {
            try {
                /** @var callable $schemaOrCallable */
                $valid = $schemaOrCallable($dom, $internalErrors);
            } catch (Throwable $exception) {
                $valid = false;
            }
        } elseif (\is_string($schemaOrCallable) && \is_file($schemaOrCallable)) {
            $valid = @$dom->schemaValidateSource(\file_get_contents($schemaOrCallable));
        } else {
            \libxml_use_internal_errors($internalErrors);

            throw new InvalidArgumentException('The schemaOrCallable argument has to be a valid path to XSD file or callable.');
        }

        if (! $valid) {
            $errors = self::getErrorsAsString(self::getXmlErrors($internalErrors));

            if ($errors === '') {
                $errors = 'The XML file is not valid.';
            }

            throw new InvalidArgumentException($errors, 0, $exception);
        }
    }

    /**
     * @param string $value
     * @param int    $position
     *
     * @return int
     */
    private static function transformToNumber(string $value, int $position): int
    {
        $raw  = $value;
        $cast = (int) $value;

        if ($raw === (string) $cast) {
            return $value[$position] === '0' ? \octdec($value) : $cast;
        }

        return $value[$position] === '0' ? \octdec($value) : $raw;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Parser\Parser;

use Viserio\Component\Contract\Parser\Exception\ParseException;
use Viserio\Component\Contract\Parser\Parser as ParserContract;

class PoParser implements ParserContract
{
    private const DEFAULT_ENTRY = [
        'msgid'      => [],
        'msgstr'     => [],
        'msgctxt'    => [],
        'ccomment'   => [],
        'tcomment'   => [],
        'obsolete'   => false,
        'fuzzy'      => false,
        'flags'      => [],
        'references' => [],
    ];

    /**
     * {@inheritdoc}
     *
     * array[]
     *     ['headers']        array  If a multi-line header is provided
     *                               than the value is a array else a string
     *     array[]
     *         ['msgid']        array  ID of the message.
     *         ['msgstr']       array  Message translation.
     *         ['msgctxt']      array  Message context.
     *         ['tcomment']     array  Comment from translator.
     *         ['ccomment']     array  Extracted comments from code.
     *         ['references']   array  Location of string in code.
     *         ['obsolete']     bool   Is the message obsolete?
     *         ['fuzzy']        bool   Is the message "fuzzy"?
     *         ['flags']        array  Flags of the entry.
     *         ['previous']     array
     *             ['msgid']    string This is a previous string
     *             ['msgstr']   string This is a previous translation string
     *         ['msgid_plural'] array  The plural string
     *         ['msgstr[0]']    array  The string when the number is equal
     *         ['msgstr[1]']    array  The string when the number is equal
     */
    public function parse(string $payload): array
    {
        $lines = \explode("\n", $payload);
        $i     = 0;

        $entries         = [];
        $headers         = [];
        $entry           = [];
        $justNewEntry    = false; // A new entries has been just inserted.
        $lastPreviousKey = null; // Used to remember last key in a multiline previous entries.
        $state           = null;
        $firstLine       = true;

        for ($n = \count($lines); $i < $n; $i++) {
            $line      = \trim($lines[$i]);
            $splitLine = \preg_split('/\s+/', $line, 2);
            $key       = $splitLine[0];

            if ($line === '' || ($key === 'msgid' && isset($entry['msgid']))) {
                // Two consecutive blank lines
                if ($justNewEntry) {
                    continue;
                }

                if ($firstLine) {
                    $firstLine = false;

                    if (isset($entry['msgstr']) && self::isHeader($entry)) {
                        $headers = $entry['msgstr'];
                    } else {
                        $entries[] = $entry;
                    }
                } else {
                    $entries[] = $entry;
                }

                $state           = null;
                $justNewEntry    = true;
                $lastPreviousKey = null;
                $entry           = [];

                if ($line === '') {
                    continue;
                }
            } else {
                $justNewEntry = false;
            }

            $data = $splitLine[1] ?? '';

            switch ($key) {
                case '#': // # Translator comments
                    $entry['tcomment'][] = self::convertString($data);

                    break;
                case '#.': // #. Comments extracted from source code
                    $entry['ccomment'][] = self::convertString($data);

                    break;
                case '#,': // Flagged translation
                    $entry['flags'] = \preg_split('/,\s*/', $data);
                    $entry['fuzzy'] = \in_array('fuzzy', $entry['flags'], true);

                    break;
                case '#:':
                    $entry = self::addReferences($data, $entry);

                    break;
                case '#|':  // Previous untranslated string
                case '#~':  // Old entries
                case '#~|': // Previous-Old untranslated string.
                    if ($key === '#|') {
                        $key = 'previous';
                    } elseif ($key === '#~') {
                        $key = 'obsolete';
                    } elseif ($key === '#~|') {
                        $key = 'previous-obsolete';
                    }

                    $tmpParts = \explode(' ', $data);
                    $tmpKey   = $tmpParts[0];

                    if (! \in_array($tmpKey, ['msgid', 'msgid_plural', 'msgstr', 'msgctxt'], true)) {
                        $tmpKey = $lastPreviousKey;
                        $str    = $data;
                    } else {
                        $str = \implode(' ', \array_slice($tmpParts, 1));
                    }

                    $entry[$key] = $entry[$key] ?? ['msgid' => [], 'msgstr' => []];

                    if ($key === 'obsolete' || $key === 'previous-obsolete') {
                        [$entry, $lastPreviousKey] = self::processObsoleteEntry($lastPreviousKey, $tmpKey, $str, $entry);
                    }

                    if ($key === 'previous') {
                        [$entry, $lastPreviousKey] = self::processPreviousEntry($lastPreviousKey, $tmpKey, $str, $entry, $key);
                    }

                    break;
                case '#@': // ignore #@ default
                    $entry['@'] = self::convertString($data);

                    break;
                // Allows disambiguation of different messages that have same msgid.
                case 'msgctxt':
                case 'msgid':        // untranslated-string
                case 'msgid_plural': // untranslated-string-plural
                    $state           = $key;
                    $entry[$state][] = self::convertString($data);

                    break;
                case 'msgstr':       // translated-string
                    $state           = 'msgstr';
                    $entry[$state][] = self::convertString($data);

                    break;

                default:
                    if (\mb_strpos($key, 'msgstr[') !== false) {
                        // translated-string-case-n
                        $state           = $key;
                        $entry[$state][] = self::convertString($data);
                    } else {
                        // "multiline" lines
                        $entry = self::extractMultiLines($state, $entry, $line, $key, $i);
                    }

                    break;
            }
        }

        if ($state === 'msgstr') {
            $entries[] = $entry;
        }

        foreach ($entries as $key => $entry) {
            $entries[$key] = \array_merge(self::DEFAULT_ENTRY, $entry);
        }

        $entries['headers'] = [];

        return self::extractHeaders($headers, $entries);
    }

    /**
     * Convert a string from its PO representation.
     *
     * @param string $value
     *
     * @return string
     */
    private static function convertString(string $value): string
    {
        if ($value === '') {
            return '';
        }

        if ($value[0] === '"') {
            $value = \mb_substr($value, 1, -1);
        }

        return \strtr(
            $value,
            [
                '\\\\' => '\\',
                '\\a'  => "\x07",
                '\\b'  => "\x08",
                '\\t'  => "\t",
                '\\n'  => "\n",
                '\\v'  => "\x0b",
                '\\f'  => "\x0c",
                '\\r'  => "\r",
                '\\"'  => '"',
            ]
        );
    }

    /**
     * Checks if entry is a header.
     *
     * @param array $entry
     *
     * @return bool
     */
    private static function isHeader(array $entry): bool
    {
        $headerKeys = [
            'Project-Id-Version' => false,
            'PO-Revision-Date'   => false,
            'MIME-Version'       => false,
        ];

        $keys        = \array_keys($headerKeys);
        $headerItems = 0;
        $headers     = \array_map('trim', $entry['msgstr']);

        foreach ($headers as $header) {
            \preg_match_all('/(.*):\s/', $header, $matches, \PREG_SET_ORDER);

            if (isset($matches[0]) && \in_array($matches[0][1], $keys, true)) {
                $headerItems++;

                unset($headerKeys[$matches[0][1]]);

                $keys = \array_keys($headerKeys);
            }
        }

        return $headerItems !== 0;
    }

    /**
     * Checks if it is a header definition line. Useful for distguishing between header definitions
     * and possible continuations of a header entry.
     *
     * @param string $line Line to parse
     *
     * @return bool
     */
    private static function isHeaderDefinition(string $line): bool
    {
        return (bool) \preg_match('/^[\w-]+:/', $line);
    }

    /**
     * Export reference infos.
     *
     * @param string $data
     * @param array  $entry
     *
     * @return array
     */
    private static function addReferences(string $data, array $entry): array
    {
        foreach (\preg_split('/#:\s+/', \trim($data)) as $value) {
            if (\count(\preg_split('/\s+/', $value)) >= 2) {
                if (\preg_match_all('/([.\/a-zA-Z]+)(:(\d*))/', ' ' . $value, $matches, \PREG_SET_ORDER, 1)) {
                    $key    = '';
                    $values = [];

                    foreach ($matches as $match) {
                        $filename = $match[1];
                        $line     = $match[3] ?? null;

                        $key .= \sprintf('{%s}:{%s} ', $filename, $line);
                        $values[] = [$filename, $line];
                    }

                    $entry['references'][\trim($key)] = $values;
                }
            } else {
                if (\preg_match('/^(.+)(:(\d*))?$/U', $value, $matches)) {
                    $filename = $matches[1];
                    $line     = $matches[3] ?? null;
                    $key      = \sprintf('{%s}:{%s}', $filename, $line);

                    $entry['references'][$key] = [$filename, $line];
                }
            }
        }

        return $entry;
    }

    /**
     * Export obsolete entries.
     *
     * @param null|string $lastPreviousKey
     * @param null|string $tmpKey
     * @param string      $str
     * @param array       $entry
     *
     * @return array
     */
    private static function processObsoleteEntry(
        ?string $lastPreviousKey,
        ?string $tmpKey,
        string $str,
        array $entry
    ): array {
        $entry['obsolete'] = true;

        switch ($tmpKey) {
            case 'msgid':
                $entry['msgid'][] = self::convertString($str);
                $lastPreviousKey  = $tmpKey;

                break;
            case 'msgstr':
                $entry['msgstr'][] = self::convertString($str);

                $lastPreviousKey = $tmpKey;

                break;

            default:
                break;
        }

        return [$entry, $lastPreviousKey];
    }

    /**
     * Export previous entries.
     *
     * @param null|string $lastPreviousKey
     * @param null|string $tmpKey
     * @param string      $str
     * @param array       $entry
     * @param string      $key
     *
     * @return array
     */
    private static function processPreviousEntry(
        ?string $lastPreviousKey,
        ?string $tmpKey,
        string $str,
        array $entry,
        string $key
    ): array {
        switch ($tmpKey) {
            case 'msgid':
            case 'msgid_plural':
            case 'msgstr':
                $entry[$key][$tmpKey][] = self::convertString($str);
                $lastPreviousKey        = $tmpKey;

                break;

            default:
                $entry[$key][$tmpKey] = self::convertString($str);

                break;
        }

        return [$entry, $lastPreviousKey];
    }

    /**
     * Export multi-lines from given line.
     * Throws a exception if state is not found or broken comment is given.
     *
     * @param null|string $state
     * @param array       $entry
     * @param string      $line
     * @param string      $key
     * @param int         $i
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\ParseException
     *
     * @return array
     */
    private static function extractMultiLines(
        ?string $state,
        array $entry,
        string $line,
        string $key,
        int $i
    ): array {
        $addEntry = static function (array $entry, ?string $state, string $line): array {
            if (! isset($entry[$state])) {
                throw new ParseException([
                    'message' => \sprintf('Parse error! Missing state: [%s].', $state),
                ]);
            }

            // Convert it to array
            if (\is_string($entry[$state])) {
                $entry[$state] = [$entry[$state]];
            }

            $entry[$state][] = self::convertString($line);

            return $entry;
        };

        switch ($state) {
            case 'msgctxt':
            case 'msgid':
            case 'msgid_plural':
                $entry = $addEntry($entry, $state, $line);

                break;
            case 'msgstr':
                $entry['msgstr'][] = self::convertString($line);

                break;

            default:
                if ($state !== null && (\mb_strpos($state, 'msgstr[') !== false)) {
                    $entry = $addEntry($entry, $state, $line);
                } elseif ($key[0] === '#' && $key[1] !== ' ') {
                    throw new ParseException([
                        'message' => \sprintf(
                            'Parse error! Comments must have a space after them on line: [%s].',
                            $i
                        ),
                    ]);
                } else {
                    throw new ParseException([
                        'message' => \sprintf(
                            'Parse error! Unknown key [%s] on line: [%s].',
                            $key,
                            $i
                        ),
                        'line' => $i,
                    ]);
                }
        }

        return $entry;
    }

    /**
     * Add the headers found to the translations instance.
     *
     * @param array $headers
     * @param array $entries
     *
     * @return array
     */
    private static function extractHeaders(array $headers, array $entries): array
    {
        $currentHeader = null;

        foreach ($headers as $header) {
            $header = \trim($header);
            $header = self::convertString($header);

            if ($header === '') {
                continue;
            }

            if (self::isHeaderDefinition($header)) {
                $header                             = \explode(':', $header, 2);
                $currentHeader                      = \trim($header[0]);
                $entries['headers'][$currentHeader] = \trim($header[1]);
            } else {
                $entries['headers'][$currentHeader] = [$entries['headers'][$currentHeader] ?? '', \trim($header)];
            }
        }

        return $entries;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Translation\Extractor;

use SplFileInfo;
use Traversable;
use Viserio\Component\Contract\Translation\Exception\InvalidArgumentException;
use Viserio\Component\Contract\Translation\Extractor as ExtractorContract;

abstract class AbstractFileExtractor implements ExtractorContract
{
    /**
     * Prefix for new found message.
     *
     * @var string
     */
    protected $prefix = '';

    /**
     * {@inheritdoc}
     */
    public function setPrefix(string $prefix): void
    {
        $this->prefix = $prefix;
    }

    /**
     * @param array|string $resource Files, a file or a directory
     *
     * @return array
     */
    protected function extractFiles($resource): array
    {
        if (\is_array($resource) || $resource instanceof Traversable) {
            $files = [];

            foreach ($resource as $file) {
                if ($file instanceof SplFileInfo) {
                    $file = $file->getPathname();
                }

                if ($this->canBeExtracted($file)) {
                    $files[] = $file;
                }
            }
        } else {
            if ($resource instanceof SplFileInfo) {
                $resource = $resource->getPathname();
            }

            if (\is_file($resource)) {
                $files = $this->canBeExtracted($resource) ? [$resource] : [];
            } else {
                $files = $this->extractFromDirectory($resource);
            }
        }

        return $files;
    }

    /**
     * Check if is file.
     *
     * @param string $file
     *
     * @throws \Viserio\Component\Contract\Translation\Exception\InvalidArgumentException
     *
     * @return bool
     */
    protected function isFile(string $file): bool
    {
        if (! \is_file($file)) {
            throw new InvalidArgumentException(\sprintf('The [%s] file does not exist.', $file));
        }

        return true;
    }

    /**
     * Check if file can be extracted.
     *
     * @param string $file
     *
     * @return bool
     */
    abstract protected function canBeExtracted(string $file): bool;

    /**
     * @param array|string $resource Files, a file or a directory
     *
     * @return array files to be extracted
     */
    abstract protected function extractFromDirectory($resource): array;
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Session\Handler;

use SessionHandlerInterface;
use SessionUpdateTimestampHandlerInterface;

/**
 * Migrating session handler for migrating from one handler to another. It reads
 * from the current handler and writes both the current and new ones.
 *
 * It ignores errors from the new handler.
 *
 * @author Ross Motley <ross.motley@amara.com>
 * @author Oliver Radwell <oliver.radwell@amara.com>
 */
class MigratingSessionHandler implements SessionHandlerInterface, SessionUpdateTimestampHandlerInterface
{
    /**
     * A session handler instance.
     *
     * @var \SessionHandlerInterface & \SessionUpdateTimestampHandlerInterface
     */
    private $currentHandler;

    /**
     * A session handler instance.
     *
     * @var \SessionHandlerInterface & \SessionUpdateTimestampHandlerInterface
     */
    private $writeOnlyHandler;

    /**
     * Create a new MigratingSessionHandler instance.
     *
     * @param \SessionHandlerInterface $currentHandler
     * @param \SessionHandlerInterface $writeOnlyHandler
     */
    public function __construct(SessionHandlerInterface $currentHandler, SessionHandlerInterface $writeOnlyHandler)
    {
        if (! $currentHandler instanceof SessionUpdateTimestampHandlerInterface) {
            $currentHandler = new StrictSessionHandler($currentHandler);
        }

        if (! $writeOnlyHandler instanceof SessionUpdateTimestampHandlerInterface) {
            $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler);
        }

        $this->currentHandler   = $currentHandler;
        $this->writeOnlyHandler = $writeOnlyHandler;
    }

    /**
     * {@inheritdoc}
     */
    public function close(): bool
    {
        $result = $this->currentHandler->close();

        $this->writeOnlyHandler->close();

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function destroy($sessionId): bool
    {
        $result = $this->currentHandler->destroy($sessionId);

        $this->writeOnlyHandler->destroy($sessionId);

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function gc($maxlifetime): bool
    {
        $result = $this->currentHandler->gc($maxlifetime);

        $this->writeOnlyHandler->gc($maxlifetime);

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function open($savePath, $sessionName): bool
    {
        $result = $this->currentHandler->open($savePath, $sessionName);

        $this->writeOnlyHandler->open($savePath, $sessionName);

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function read($sessionId): string
    {
        // No reading from new handler until switch-over
        return $this->currentHandler->read($sessionId);
    }

    /**
     * {@inheritdoc}
     */
    public function write($sessionId, $sessionData): bool
    {
        $result = $this->currentHandler->write($sessionId, $sessionData);

        $this->writeOnlyHandler->write($sessionId, $sessionData);

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function validateId($sessionId): bool
    {
        // No reading from new handler until switch-over
        return $this->currentHandler->validateId($sessionId);
    }

    /**
     * {@inheritdoc}
     */
    public function updateTimestamp($sessionId, $sessionData): bool
    {
        $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData);

        $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData);

        return $result;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Foundation\Bootstrap;

use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Viserio\Component\Contract\Foundation\Bootstrap as BootstrapContract;
use Viserio\Component\Contract\Foundation\Kernel as KernelContract;

class SetRequestForConsole implements BootstrapContract
{
    /**
     * {@inheritdoc}
     */
    public static function getPriority(): int
    {
        return 256;
    }

    /**
     * {@inheritdoc}
     */
    public static function bootstrap(KernelContract $kernel): void
    {
        $config = $kernel->getKernelConfigurations();

        $kernel->getContainer()->register(new class($config) implements ServiceProviderInterface {
            /**
             * Config array.
             *
             * @var array
             */
            protected static $config;

            /**
             * Create a new server request instance.
             *
             * @param array $config
             */
            public function __construct(array $config)
            {
                self::$config = $config;
            }

            /**
             * {@inheritdoc}
             */
            public function getFactories(): array
            {
                return [
                    ServerRequestInterface::class => [self::class, 'createRequest'],
                ];
            }

            /**
             * {@inheritdoc}
             */
            public function getExtensions(): array
            {
                return [];
            }

            /**
             * Create a new console request.
             *
             * @param \Psr\Container\ContainerInterface $container
             *
             * @return \Psr\Http\Message\ServerRequestInterface
             */
            public static function createRequest(ContainerInterface $container): ServerRequestInterface
            {
                return $container->get(ServerRequestFactoryInterface::class)->createServerRequest(
                    'GET',
                    self::$config['url']
                );
            }
        });
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Translation\Formatter;

use IntlException;
use MessageFormatter;
use Viserio\Component\Contract\Translation\Exception\CannotFormatException;
use Viserio\Component\Contract\Translation\Exception\CannotInstantiateFormatterException;
use Viserio\Component\Contract\Translation\MessageFormatter as MessageFormatterContract;

class IntlMessageFormatter implements MessageFormatterContract
{
    /**
     * {@inheritdoc}
     */
    public function format(string $message, string $locale, array $parameters = []): string
    {
        if ($message === '') {
            // Empty strings are not accepted as message pattern by the \MessageFormatter.
            return $message;
        }

        try {
            $formatter = new MessageFormatter($locale, $message);
        } catch (IntlException $exception) {
            throw new CannotInstantiateFormatterException(
                $exception->getMessage(),
                $exception->getCode(),
                $exception
            );
        }

        // @codeCoverageIgnoreStart
        if ($formatter === null) {
            throw new CannotInstantiateFormatterException(
                \intl_get_error_message(),
                \intl_get_error_code()
            );
        }
        // @codeCoverageIgnoreEnd
        $result = $formatter->format($parameters);

        if ($formatter->getErrorCode() !== \U_ZERO_ERROR) {
            throw new CannotFormatException(\sprintf('Unable to format message. Reason: %s (error #%s)', $formatter->getErrorMessage(), $formatter->getErrorCode()));
        }

        return $result;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Console\Command;

use Symfony\Component\Console\Command\Command as BaseCommand;
use Viserio\Component\Console\Input\InputArgument;
use Viserio\Component\Console\Input\InputOption;
use Viserio\Component\Contract\Console\Exception\InvalidArgumentException;

/**
 * Code in this class it taken from silly.
 *
 * See the original here: https://github.com/mnapoli/silly/blob/master/src/Command/Command.php
 *
 * @author Matthieu Napoli https://github.com/mnapoli
 * @copyright Copyright (c) Matthieu Napoli
 */
class StringCommand extends BaseCommand
{
    /**
     * Define descriptions for the command and it's arguments/options.
     *
     * @param string $description                   description of the command
     * @param array  $argumentAndOptionDescriptions descriptions of the arguments and options
     *
     * @return $this
     */
    public function descriptions(string $description, array $argumentAndOptionDescriptions = []): self
    {
        $definition = $this->getDefinition();

        $this->setDescription($description);

        foreach ($argumentAndOptionDescriptions as $name => $value) {
            if (\mb_strpos($name, '--') === 0) {
                $argument = $definition->getOption(\mb_substr($name, 2));

                if ($argument instanceof InputOption) {
                    $argument->setDescription($value);
                }
            } else {
                $argument = $definition->getArgument($name);

                if ($argument instanceof InputArgument) {
                    $argument->setDescription($value);
                }
            }
        }

        return $this;
    }

    /**
     * Define default values for the arguments of the command.
     *
     * @param array $defaults default argument values
     *
     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
     * @throws \Viserio\Component\Contract\Console\Exception\InvalidArgumentException
     *
     * @return $this
     */
    public function defaults(array $defaults = []): self
    {
        $definition = $this->getDefinition();

        foreach ($defaults as $name => $default) {
            if ($definition->hasArgument($name)) {
                $input = $definition->getArgument($name);
            } elseif ($definition->hasOption($name)) {
                $input = $definition->getOption($name);
            } else {
                throw new InvalidArgumentException(\sprintf(
                    'Unable to set default for [%s]. It does not exist as an argument or option.',
                    $name
                ));
            }

            $input->setDefault($default);
        }

        return $this;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Http;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Viserio\Component\Contract\Http\Exception\InvalidArgumentException;
use Viserio\Component\Contract\Http\Exception\RuntimeException;
use Viserio\Component\Http\Stream\LazyOpenStream;

class UploadedFile implements UploadedFileInterface
{
    /**
     * All errors which can happen on a upload.
     *
     * @var int[]
     */
    protected const ERRORS = [
        \UPLOAD_ERR_OK,
        \UPLOAD_ERR_INI_SIZE,
        \UPLOAD_ERR_FORM_SIZE,
        \UPLOAD_ERR_PARTIAL,
        \UPLOAD_ERR_NO_FILE,
        \UPLOAD_ERR_NO_TMP_DIR,
        \UPLOAD_ERR_CANT_WRITE,
        \UPLOAD_ERR_EXTENSION,
    ];

    /**
     * The client-provided full path to the file.
     *
     * @note this is public to maintain BC with 3.1.0 and earlier.
     *
     * @var string
     */
    public $file;

    /**
     * The client-provided file name.
     *
     * @var string
     */
    protected $name;

    /**
     * The client-provided media type of the file.
     *
     * @var string
     */
    protected $type;

    /**
     * The size of the file in bytes.
     *
     * @var int
     */
    protected $size;

    /**
     * A valid PHP UPLOAD_ERR_xxx code for the file upload.
     *
     * @var int
     */
    protected $error;

    /**
     * An optional StreamInterface wrapping the file resource.
     *
     * @var StreamInterface
     */
    protected $stream;

    /**
     * Indicates if the uploaded file has already been moved.
     *
     * @var bool
     */
    protected $moved = false;

    /**
     * Client media type of a file.
     *
     * @var null|string
     */
    protected $clientMediaType;

    /**
     * Client filename.
     *
     * @var null|string
     */
    protected $clientFilename;

    /**
     * Help textes for upload error.
     *
     * @var array
     */
    private static $errorMessages = [
        \UPLOAD_ERR_OK         => 'There is no error, the file uploaded with success.',
        \UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
        \UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
        \UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded.',
        \UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
        \UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
        \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
        \UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload.',
    ];

    /**
     * Create a new uploaded file instance.
     *
     * @param resource|StreamInterface|string $streamOrFile
     * @param int                             $size
     * @param int                             $errorStatus
     * @param null|string                     $clientFilename
     * @param null|string                     $clientMediaType
     *
     * @throws \Viserio\Component\Contract\Http\Exception\InvalidArgumentException
     */
    public function __construct(
        $streamOrFile,
        int $size,
        int $errorStatus         = \UPLOAD_ERR_OK,
        ?string $clientFilename  = null,
        ?string $clientMediaType = null
    ) {
        $this->setError($errorStatus);
        $this->size            = $size;
        $this->clientFilename  = $clientFilename;
        $this->clientMediaType = $clientMediaType;

        if ($this->isOk()) {
            $this->setStreamOrFile($streamOrFile);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return null|int the file size in bytes or null if unknown
     */
    public function getSize(): ?int
    {
        return $this->size;
    }

    /**
     * {@inheritdoc}
     *
     * @see http://php.net/manual/en/features.file-upload.errors.php
     *
     * @return int one of PHP's UPLOAD_ERR_XXX constants
     */
    public function getError(): int
    {
        return $this->error;
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Viserio\Component\Contract\Http\Exception\RuntimeException if the upload was not successful
     */
    public function getStream(): StreamInterface
    {
        $this->validateActive();

        if ($this->stream instanceof StreamInterface) {
            return $this->stream;
        }

        return new LazyOpenStream($this->file, 'r+');
    }

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

    /**
     * {@inheritdoc}
     */
    public function getClientMediaType(): ?string
    {
        return $this->clientMediaType;
    }

    /**
     * {@inheritdoc}
     *
     * @return null|string the filename sent by the client or null if none
     *                     was provided
     */
    public function getClientFilename(): ?string
    {
        return $this->clientFilename;
    }

    /**
     * {@inheritdoc}
     *
     * @see http://php.net/is_uploaded_file
     * @see http://php.net/move_uploaded_file
     *
     * @param string $targetPath path to which to move the uploaded file
     *
     * @throws \Viserio\Component\Contract\Http\Exception\InvalidArgumentException if the $path specified is invalid
     * @throws \Viserio\Component\Contract\Http\Exception\RuntimeException         if the upload was not successful or on any error during the move operation, or on
     *                                                                             the second or subsequent call to the method
     */
    public function moveTo($targetPath): void
    {
        $this->validateActive();

        if ($this->isStringNotEmpty($targetPath) === false) {
            throw new InvalidArgumentException(
                'Invalid path provided for move operation; must be a non-empty string.'
            );
        }

        if ($this->file) {
            ($this->moved = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true))
                ? \rename($this->file, $targetPath)
                : \move_uploaded_file($this->file, $targetPath);
        } else {
            Util::copyToStream(
                $this->getStream(),
                new LazyOpenStream($targetPath, 'w')
            );

            $this->moved = true;
        }

        if ($this->moved === false) {
            throw new RuntimeException(
                \sprintf('Uploaded file could not be moved to %s', $targetPath)
            );
        }
    }

    /**
     * Check if error is a int or a array, then set it.
     *
     * @param int $error
     *
     * @throws \Viserio\Component\Contract\Http\Exception\InvalidArgumentException
     *
     * @return void
     */
    private function setError(int $error): void
    {
        if (! \in_array($error, self::ERRORS, true)) {
            throw new InvalidArgumentException('Invalid error status for UploadedFile.');
        }

        $this->error = $error;
    }

    /**
     * Set the fill the right variable.
     *
     * @param mixed $streamOrFile
     *
     * @throws \Viserio\Component\Contract\Http\Exception\InvalidArgumentException
     *
     * @return void
     */
    private function setStreamOrFile($streamOrFile): void
    {
        if (\is_string($streamOrFile)) {
            $this->file = $streamOrFile;

            return;
        }

        if (\is_resource($streamOrFile)) {
            $this->stream = new Stream($streamOrFile);

            return;
        }

        if ($streamOrFile instanceof StreamInterface) {
            $this->stream = $streamOrFile;

            return;
        }

        throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile.');
    }

    /**
     * Check if is a string or is empty.
     *
     * @param mixed $param
     *
     * @return bool
     */
    private function isStringNotEmpty($param): bool
    {
        return \is_string($param) && ! empty($param);
    }

    /**
     * Return true if there is no upload error.
     *
     * @return bool
     */
    private function isOk(): bool
    {
        return $this->error === \UPLOAD_ERR_OK;
    }

    /**
     * Validate retrieve stream.
     *
     * @throws \Viserio\Component\Contract\Http\Exception\RuntimeException if is moved or not ok
     *
     * @return void
     */
    private function validateActive(): void
    {
        if ($this->isOk() === false) {
            throw new RuntimeException(\sprintf(
                'Cannot retrieve stream due to upload error: %s',
                self::$errorMessages[$this->error]
            ));
        }

        if ($this->isMoved()) {
            throw new RuntimeException('Cannot retrieve stream after it has already been moved.');
        }
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Cron\Provider;

use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Viserio\Component\Console\Application;
use Viserio\Component\Contract\OptionsResolver\ProvidesDefaultOptions as ProvidesDefaultOptionsContract;
use Viserio\Component\Contract\OptionsResolver\RequiresComponentConfig as RequiresComponentConfigContract;
use Viserio\Component\Cron\Command\CronListCommand;
use Viserio\Component\Cron\Command\ScheduleRunCommand;

class ConsoleCommandsServiceProvider implements
    ServiceProviderInterface,
    RequiresComponentConfigContract,
    ProvidesDefaultOptionsContract
{
    /**
     * {@inheritdoc}
     */
    public function getFactories(): array
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    public function getExtensions(): array
    {
        return [
            Application::class => [self::class, 'extendConsole'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public static function getDimensions(): array
    {
        return ['viserio', 'console'];
    }

    /**
     * {@inheritdoc}
     */
    public static function getDefaultOptions(): array
    {
        return [
            'lazily_commands' => [
                'cron:list' => CronListCommand::class,
                'cron:run'  => ScheduleRunCommand::class,
            ],
        ];
    }

    /**
     * Extend viserio console with commands.
     *
     * @param \Psr\Container\ContainerInterface           $container
     * @param null|\Viserio\Component\Console\Application $console
     *
     * @return null|\Viserio\Component\Console\Application
     */
    public static function extendConsole(ContainerInterface $container, ?Application $console = null): ?Application
    {
        if ($console !== null) {
            // @var Application $console
            $console->addCommands([
                new CronListCommand(),
                new ScheduleRunCommand(),
            ]);
        }

        return $console;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Container;

use Mockery;
use Mockery\MockInterface;
use Viserio\Component\Contract\Container\Exception\InvalidArgumentException;

class MockContainer extends Container
{
    /**
     * @var array
     */
    protected $mockedServices = [];

    /**
     * @return array
     */
    public function getMockedServices(): array
    {
        return $this->mockedServices;
    }

    /**
     * Takes an id of the service as the first argument.
     * Any other arguments are passed to the Mockery factory.
     *
     * @param array<int, mixed> $args
     *
     * @throws \Viserio\Component\Contract\Container\Exception\InvalidArgumentException
     *
     * @return \Mockery\MockInterface
     */
    public function mock(...$args): MockInterface
    {
        $id = \array_shift($args);

        if (! $this->has($id)) {
            throw new InvalidArgumentException(\sprintf('Cannot mock a non-existent service: [%s]', $id));
        }

        $mock = 'mock::' . $id;

        if (! isset($this->mockedServices[$mock])) {
            $this->mockedServices[$mock] = \call_user_func_array([Mockery::class, 'mock'], $args);
        }

        return $this->mockedServices[$mock];
    }

    /**
     * Unset a mocked services.
     *
     * @param string $id
     */
    public function unmock(string $id): void
    {
        unset($this->mockedServices['mock::' . $id]);
    }

    /**
     * {@inheritdoc}
     */
    public function get($id)
    {
        return $this->mockedServices['mock::' . $id] ?? parent::get($id);
    }

    /**
     * {@inheritdoc}
     */
    public function has($id)
    {
        if (isset($this->mockedServices['mock::' . $id])) {
            return true;
        }

        return parent::has($id);
    }

    /**
     * Unsets a parameter or an object.
     *
     * @param string $offset
     */
    public function offsetUnset($offset): void
    {
        unset($this->bindings[$offset], $this->mockedServices['mock::' . $offset]);
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Routing\Matcher;

use Viserio\Component\Contract\Routing\Exception\InvalidArgumentException;
use Viserio\Component\Contract\Routing\SegmentMatcher as SegmentMatcherContract;

abstract class AbstractMatcher implements SegmentMatcherContract
{
    /**
     * Stores all parameters keys.
     *
     * @var array
     */
    protected $parameterKeys = [];

    /**
     * {@inheritdoc}
     */
    public function getParameterKeys(): array
    {
        return $this->parameterKeys;
    }

    /**
     * {@inheritdoc}
     */
    public function getMatchedParameterExpressions(string $segmentVariable, int $uniqueKey = null): array
    {
        return \array_fill_keys($this->parameterKeys, $segmentVariable);
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Viserio\Component\Contract\Routing\Exception\InvalidArgumentException
     */
    public function mergeParameterKeys(SegmentMatcherContract $matcher): void
    {
        if ($matcher->getHash() !== $this->getHash()) {
            throw new InvalidArgumentException(
                \sprintf(
                    'Cannot merge parameters: Matcher\'s must be equivalent, [%s] expected, [%s] given.',
                    $matcher->getHash(),
                    $this->getHash()
                )
            );
        }

        $this->parameterKeys = \array_unique(
            \array_merge($this->parameterKeys, $matcher->getParameterKeys()),
            \SORT_NUMERIC
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getHash(): string
    {
        return \get_class($this) . ':' . $this->getMatchHash();
    }

    /**
     * Returns a unique hash for the matching criteria of the segment.
     *
     * @return string
     */
    abstract protected function getMatchHash(): string;
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Console\Command;

use Viserio\Component\Console\Input\InputArgument;
use Viserio\Component\Console\Input\InputOption;
use Viserio\Component\Contract\Console\Exception\InvalidCommandExpression;

final class ExpressionParser
{
    /**
     * Parse given command string.
     *
     * @param string $expression
     *
     * @throws \Viserio\Component\Contract\Console\Exception\InvalidCommandExpression
     *
     * @return array
     */
    public static function parse(string $expression): array
    {
        \preg_match_all('/^\S*|(\[\s*(.*?)\]|[[:alnum:]_-]+\=\*|[[:alnum:]_-]+\?|[[:alnum:]_-]+|-+[[:alnum:]_\-=*]+)/', $expression, $matches);

        if (\trim($expression) === '') {
            throw new InvalidCommandExpression('The expression was empty.');
        }

        $tokens    = \array_values(\array_filter(\array_map('trim', $matches[0])));
        $name      = \array_shift($tokens);
        $arguments = [];
        $options   = [];

        foreach ($tokens as $token) {
            if (self::startsWith($token, '--')) {
                throw new InvalidCommandExpression('An option must be enclosed by brackets: [--option].');
            }

            if (self::isOption($token)) {
                $options[] = self::parseOption($token);
            } else {
                $arguments[] = self::parseArgument($token);
            }
        }

        return [
            'name'      => $name,
            'arguments' => $arguments,
            'options'   => $options,
        ];
    }

    /**
     * Check if token is a option.
     *
     * @param string $token
     *
     * @return bool
     */
    private static function isOption(string $token): bool
    {
        return self::startsWith($token, '[-');
    }

    /**
     * Parse arguments.
     *
     * @param string $token
     *
     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
     *
     * @return \Viserio\Component\Console\Input\InputArgument
     */
    private static function parseArgument(string $token): InputArgument
    {
        [$token, $description] = static::extractDescription($token);

        switch (true) {
            case self::endsWith($token, '=*]'):
                return new InputArgument(\trim($token, '[=*]'), InputArgument::IS_ARRAY, $description);
            case self::endsWith($token, '=*'):
                return new InputArgument(\trim($token, '=*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description);
            case \preg_match('/(.*)\?$/', $token, $matches):
                return new InputArgument($matches[1], InputArgument::OPTIONAL, $description);
            case \preg_match('/\[(.+)\?\]/', $token, $matches):
                return new InputArgument($matches[1], InputArgument::OPTIONAL, $description);
            case \preg_match('/\[(.+)\=\*(.+)\]/', $token, $matches):
                return new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, \preg_split('/,\s?/', $matches[2]));
            case \preg_match('/\[(.+)\=(.+)\]/', $token, $matches):
                return new InputArgument($matches[1], InputArgument::OPTIONAL, $description, $matches[2]);
            case self::startsWith($token, '[') && self::endsWith($token, ']'):
                return new InputArgument(\trim($token, '[]'), InputArgument::OPTIONAL, $description);

            default:
                return new InputArgument($token, InputArgument::REQUIRED, $description);
        }
    }

    /**
     * Parse options.
     *
     * @param string $token
     *
     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
     *
     * @return \Viserio\Component\Console\Input\InputOption
     */
    private static function parseOption(string $token): InputOption
    {
        [$token, $description] = static::extractDescription(\trim($token, '[]'));

        // Shortcut [-y|--yell]
        if (\mb_strpos($token, '|') !== false) {
            [$shortcut, $token] = \explode('|', $token, 2);
            $shortcut           = \ltrim($shortcut, '-');
        } else {
            $shortcut = null;
        }

        $name    = \ltrim($token, '-');
        $default = null;

        switch (true) {
            case self::endsWith($token, '=*'):
                return new InputOption(\rtrim($name, '=*'), $shortcut, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, $description);
            case self::endsWith($token, '='):
                return new InputOption(\rtrim($name, '='), $shortcut, InputOption::VALUE_REQUIRED, $description);
            case \preg_match('/(.+)\=\*(.+)/', $token, $matches):
                return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, \preg_split('/,\s?/', $matches[2]));
            case \preg_match('/(.+)\=(.+)/', $token, $matches):
                return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL, $description, $matches[2]);

            default:
                return new InputOption($token, $shortcut, InputOption::VALUE_NONE, $description);
        }
    }

    /**
     * Parse the token into its token and description segments.
     *
     * @param string $token
     *
     * @return array
     */
    private static function extractDescription(string $token): array
    {
        \preg_match('/(.*)\s:(\s+.*(?<!]))(.*)/', \trim($token), $parts);

        return \count($parts) === 4 ? [$parts[1] . $parts[3], \trim($parts[2])] : [$token, ''];
    }

    /**
     * Determine if a given string starts with a given substring.
     *
     * @param string $haystack
     * @param string $needle
     *
     * @return bool
     */
    private static function startsWith(string $haystack, string $needle): bool
    {
        return $needle !== '' && \mb_strrpos($haystack, $needle) === 0;
    }

    /**
     * Determine if a given string ends with a given substring.
     *
     * @param string $haystack
     * @param string $needle
     *
     * @return bool
     */
    private static function endsWith(string $haystack, string $needle): bool
    {
        return \mb_substr($haystack, -\mb_strlen($needle)) === $needle;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Cookie\Traits;

use Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException;

trait CookieValidatorTrait
{
    /**
     * Validates the name attribute.
     *
     * @param string $name
     *
     * @throws \Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException
     *
     * @see http://tools.ietf.org/search/rfc2616#section-2.2
     *
     * @return void
     */
    protected function validateName(string $name): void
    {
        if ($name === '') {
            throw new InvalidArgumentException('The name cannot be empty.');
        }

        // Name attribute is a token as per spec in RFC 2616
        if (\preg_match('/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5b-\x5d\x7b\x7d\x7f]/', $name)) {
            throw new InvalidArgumentException(\sprintf(
                'Cookie name [%s] must not contain invalid characters: ASCII Control characters (0-31;127), space, tab and the following characters: ()<>@,;:\"/[]?={}',
                $name
            ));
        }
    }

    /**
     * Validates a value.
     *
     * @param null|string $value
     *
     * @throws \Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException
     *
     * @see http://tools.ietf.org/html/rfc6265#section-4.1.1
     *
     * @return void
     */
    protected function validateValue(?string $value = null): void
    {
        if ($value !== null && \preg_match('/[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/', $value)) {
            throw new InvalidArgumentException(
                \sprintf(
                    'The cookie value [%s] contains invalid characters.',
                    $value
                )
            );
        }
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Bridge\Twig\NodeVisitor;

/**
 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
 */
class Scope
{
    /**
     * @var \Viserio\Bridge\Twig\NodeVisitor\Scope
     */
    private $parent;

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

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

    /**
     * Create a new Scope instance.
     *
     * @param null|\Viserio\Bridge\Twig\NodeVisitor\Scope $parent
     */
    public function __construct(self $parent = null)
    {
        $this->parent = $parent;
    }

    /**
     * Opens a new child scope.
     *
     * @return self
     */
    public function enter(): self
    {
        return new self($this);
    }

    /**
     * Closes current scope and returns parent one.
     *
     * @return null|self
     */
    public function leave(): ?self
    {
        $this->left = true;

        return $this->parent;
    }

    /**
     * Stores data into current scope.
     *
     * @param string $key
     * @param mixed  $value
     *
     * @throws \LogicException
     *
     * @return $this
     */
    public function set(string $key, $value)
    {
        if ($this->left) {
            throw new \LogicException('Left scope is not mutable.');
        }

        $this->data[$key] = $value;

        return $this;
    }

    /**
     * Tests if a data is visible from current scope.
     *
     * @param string $key
     *
     * @return bool
     */
    public function has(string $key): bool
    {
        if (\array_key_exists($key, $this->data)) {
            return true;
        }

        if ($this->parent === null) {
            return false;
        }

        return $this->parent->has($key);
    }

    /**
     * Returns data visible from current scope.
     *
     * @param string $key
     * @param mixed  $default
     *
     * @return mixed
     */
    public function get(string $key, $default = null)
    {
        if (\array_key_exists($key, $this->data)) {
            return $this->data[$key];
        }

        if ($this->parent === null) {
            return $default;
        }

        return $this->parent->get($key, $default);
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Mail\Transport;

use GuzzleHttp\Client;
use Swift_Mime_SimpleMessage;

class MandrillTransport extends AbstractTransport
{
    /**
     * Guzzle client instance.
     *
     * @var \GuzzleHttp\Client
     */
    protected $client;

    /**
     * The Mandrill API key.
     *
     * @var string
     */
    protected $key;

    /**
     * Create a new Mandrill transport instance.
     *
     * @param \GuzzleHttp\Client $client
     * @param string             $key
     */
    public function __construct(Client $client, string $key)
    {
        $this->client = $client;
        $this->key    = $key;
    }

    /**
     * Get the API key being used by the transport.
     *
     * @return string
     */
    public function getKey(): string
    {
        return $this->key;
    }

    /**
     * Set the API key being used by the transport.
     *
     * @param string $key
     *
     * @return $this
     */
    public function setKey(string $key): self
    {
        $this->key = $key;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null): int
    {
        $this->beforeSendPerformed($message);

        $data = [
            'key'         => $this->key,
            'to'          => $this->getToAddresses($message),
            'raw_message' => $message->toString(),
            'async'       => false,
        ];

        $options = ['form_params' => $data];

        $this->client->post('https://mandrillapp.com/api/1.0/messages/send-raw.json', $options);

        $this->sendPerformed($message);

        return $this->numberOfRecipients($message);
    }

    /**
     * {@inheritdoc}
     *
     * @codeCoverageIgnore
     */
    public function ping(): bool
    {
        return true;
    }

    /**
     * Get all the addresses this email should be sent to,
     * including "to", "cc" and "bcc" addresses.
     *
     * @param \Swift_Mime_SimpleMessage $message
     *
     * @return array
     */
    protected function getToAddresses(Swift_Mime_SimpleMessage $message): array
    {
        $to = [];

        $getTo = $message->getTo();

        if (\is_array($getTo)) {
            $to = \array_merge($to, \array_keys($getTo));
        }

        $cc = $message->getCc();

        if (\is_array($cc)) {
            $to = \array_merge($to, \array_keys($cc));
        }

        $bcc = $message->getBcc();

        if (\is_array($bcc)) {
            $to = \array_merge($to, \array_keys($bcc));
        }

        return $to;
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Cache;

use Cache\Adapter\Filesystem\FilesystemCachePool;
use Cache\Adapter\Memcache\MemcacheCachePool;
use Cache\Adapter\Memcached\MemcachedCachePool;
use Cache\Adapter\MongoDB\MongoDBCachePool;
use Cache\Adapter\PHPArray\ArrayCachePool;
use Cache\Adapter\Predis\PredisCachePool;
use Cache\Adapter\Redis\RedisCachePool;
use Cache\Adapter\Void\VoidCachePool;
use Cache\Hierarchy\HierarchicalPoolInterface;
use Cache\Namespaced\NamespacedCachePool;
use League\Flysystem\Filesystem as Flysystem;
use Memcache;
use Memcached;
use MongoDB\Driver\Manager as MongoDBManager;
use Predis\Client as PredisClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Redis;
use Viserio\Component\Contract\Cache\Manager as CacheManagerContract;
use Viserio\Component\Contract\OptionsResolver\ProvidesDefaultOptions as ProvidesDefaultOptionsContract;
use Viserio\Component\Manager\AbstractManager;

class CacheManager extends AbstractManager implements
    CacheManagerContract,
    LoggerAwareInterface,
    ProvidesDefaultOptionsContract
{
    use LoggerAwareTrait;

    /**
     * Create a new cache manager instance.
     *
     * @param array|\ArrayAccess $config
     */
    public function __construct($config)
    {
        parent::__construct($config);

        $this->logger = new NullLogger();
    }

    /**
     * {@inheritdoc}
     */
    public static function getDefaultOptions(): array
    {
        return [
            'default'   => 'array',
            'namespace' => false,
            'key'       => false,
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function createDriver(array $config)
    {
        $driver    = parent::createDriver($config);
        $namespace = $this->resolvedOptions['namespace'];

        $driver->setLogger($this->logger);

        if ($namespace !== false &&
            $driver instanceof HierarchicalPoolInterface &&
            \class_exists(NamespacedCachePool::class)
        ) {
            $driver = $this->getNamespacedPool($driver, $namespace);
        }

        return $driver;
    }

    /**
     * Create an instance of the Array cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\PHPArray\ArrayCachePool
     */
    protected function createArrayDriver(array $config): ArrayCachePool
    {
        return new ArrayCachePool();
    }

    /**
     * Create an instance of the MongoDB cache driver.
     *
     * @param array $config
     *
     * @throws \MongoDB\Driver\Exception\RuntimeException
     * @throws \MongoDB\Driver\Exception\InvalidArgumentException
     *
     * @return \Cache\Adapter\MongoDB\MongoDBCachePool
     *
     * @codeCoverageIgnore
     */
    protected function createMongodbDriver(array $config): MongoDBCachePool
    {
        if (isset($config['username'], $config['password'])) {
            $dns = \sprintf(
                'mongodb://%s:%s@%s:%s',
                $config['username'],
                $config['password'],
                $config['server'],
                $config['port']
            );
        } else {
            $dns = \sprintf('mongodb://%s:%s', $config['server'], $config['port']);
        }

        $collection = MongoDBCachePool::createCollection(
            new MongoDBManager($dns),
            $config['database'],
            $config['prefix']
        );

        return new MongoDBCachePool($collection);
    }

    /**
     * Create an instance of the Redis cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\Redis\RedisCachePool
     *
     * @codeCoverageIgnore
     */
    protected function createRedisDriver(array $config): RedisCachePool
    {
        $client = new Redis();
        $client->connect($config['host'], $config['port']);

        return new RedisCachePool($client);
    }

    /**
     * Create an instance of the Predis cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\Predis\PredisCachePool
     *
     * @codeCoverageIgnore
     */
    protected function createPredisDriver(array $config): PredisCachePool
    {
        $client = new PredisClient(\sprintf('tcp:/%s:%s', $config['server'], $config['port']));

        return new PredisCachePool($client);
    }

    /**
     * Create an instance of the Flysystem cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\Filesystem\FilesystemCachePool
     */
    protected function createFilesystemDriver(array $config): FilesystemCachePool
    {
        $adapter = $this->container->get($config['connection']);

        return new FilesystemCachePool(new Flysystem($adapter));
    }

    /**
     * Create an instance of the Memcached cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\Memcached\MemcachedCachePool
     *
     * @codeCoverageIgnore
     */
    protected function createMemcachedDriver(array $config): MemcachedCachePool
    {
        $client = new Memcached();
        $client->addServer($config['host'], $config['port']);

        return new MemcachedCachePool($client);
    }

    /**
     * Create an instance of the Memcache cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\Memcache\MemcacheCachePool
     *
     * @codeCoverageIgnore
     */
    protected function createMemcacheDriver(array $config): MemcacheCachePool
    {
        $client = new Memcache();
        $client->addServer($config['host'], $config['port']);

        return new MemcacheCachePool($client);
    }

    /**
     * Create an instance of the Void cache driver.
     *
     * @param array $config
     *
     * @return \Cache\Adapter\Void\VoidCachePool
     */
    protected function createNullDriver(array $config): VoidCachePool
    {
        return new VoidCachePool();
    }

    /**
     * Create a prefixed cache pool with a namespace.
     *
     * @param \Cache\Hierarchy\HierarchicalPoolInterface $hierarchyPool
     * @param string                                     $namespace
     *
     * @return \Cache\Namespaced\NamespacedCachePool
     */
    protected function getNamespacedPool(
        HierarchicalPoolInterface $hierarchyPool,
        string $namespace
    ): NamespacedCachePool {
        return new NamespacedCachePool($hierarchyPool, $namespace);
    }

    /**
     * {@inheritdoc}
     */
    protected static function getConfigName(): string
    {
        return 'cache';
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Profiler;

use Viserio\Component\Contract\Profiler\DataCollector as DataCollectorContract;
use Viserio\Component\Contract\Profiler\Exception\CollectorNotFoundException;

class Profile
{
    /**
     * Profile token.
     *
     * @var string
     */
    private $token;

    /**
     * Client ip.
     *
     * @var string
     */
    private $ip;

    /**
     * Request method.
     *
     * @var string
     */
    private $method;

    /**
     * Request url.
     *
     * @var string
     */
    private $url;

    /**
     * Needed time.
     *
     * @var string
     */
    private $time;

    /**
     * Creation date.
     *
     * @var string
     */
    private $date;

    /**
     * Response status code.
     *
     * @var string
     */
    private $statusCode;

    /**
     * All collected collectors.
     *
     * @var array
     */
    private $collectors = [];

    /**
     * Create new Profiler profile.
     *
     * @param string $token
     */
    public function __construct(string $token)
    {
        $this->token = $token;
    }

    /**
     * @return array
     *
     * @codeCoverageIgnore
     */
    public function __sleep(): array
    {
        return [
            'token',
            'parent',
            'children',
            'collectors',
            'ip',
            'method',
            'url',
            'time',
            'statusCode',
            'date',
        ];
    }

    /**
     * Gets the token.
     *
     * @return string The token
     */
    public function getToken(): string
    {
        return $this->token;
    }

    /**
     * Sets the token.
     *
     * @param string $token The token
     *
     * @return void
     */
    public function setToken(string $token): void
    {
        $this->token = $token;
    }

    /**
     * Returns the IP.
     *
     * @return string The IP
     */
    public function getIp(): string
    {
        return $this->ip;
    }

    /**
     * Sets the IP.
     *
     * @param string $ip
     *
     * @return void
     */
    public function setIp(string $ip): void
    {
        $this->ip = $ip;
    }

    /**
     * Returns the request method.
     *
     * @return string The request method
     */
    public function getMethod(): string
    {
        return $this->method;
    }

    /**
     * Set the request method.
     *
     * @param string $method
     *
     * @return void
     */
    public function setMethod(string $method): void
    {
        $this->method = $method;
    }

    /**
     * Returns the url.
     *
     * @return string
     */
    public function getUrl(): string
    {
        return $this->url;
    }

    /**
     * Set the request url.
     *
     * @param string $url
     *
     * @return void
     */
    public function setUrl(string $url): void
    {
        $this->url = $url;
    }

    /**
     * Returns the time.
     *
     * @return string The time
     */
    public function getTime(): string
    {
        if ($this->time === null) {
            return '0';
        }

        return $this->time;
    }

    /**
     * Set the needed time.
     *
     * @param float|string $time
     *
     * @return void
     */
    public function setTime($time): void
    {
        $this->time = (string) $time;
    }

    /**
     * Returns the date.
     *
     * @return string The date
     */
    public function getDate(): string
    {
        return $this->date;
    }

    /**
     * Set the creation date.
     *
     * @param string $date
     *
     * @return void
     */
    public function setDate(string $date): void
    {
        $this->date = $date;
    }

    /**
     * Get the response status code.
     *
     * @return string
     */
    public function getStatusCode(): string
    {
        return $this->statusCode;
    }

    /**
     * Set the response status code.
     *
     * @param int $statusCode
     *
     * @return void
     */
    public function setStatusCode(int $statusCode): void
    {
        $this->statusCode = (string) $statusCode;
    }

    /**
     * Gets the Collectors associated with this profile.
     *
     * @return \Viserio\Component\Contract\Profiler\DataCollector[]
     */
    public function getCollectors(): array
    {
        return $this->collectors;
    }

    /**
     * Sets the Collectors associated with this profile.
     *
     * @param array $collectors
     *
     * @return void
     */
    public function setCollectors(array $collectors): void
    {
        foreach ($collectors as $collector) {
            $this->addCollector($collector['collector']);
        }
    }

    /**
     * Gets a Collector by name.
     *
     * @param string $name A collector name
     *
     * @throws \Viserio\Component\Contract\Profiler\Exception\CollectorNotFoundException if the collector does not exist
     *
     * @return \Viserio\Component\Contract\Profiler\DataCollector
     */
    public function getCollector(string $name): DataCollectorContract
    {
        if (! isset($this->collectors[$name])) {
            throw new CollectorNotFoundException(\sprintf('Collector [%s] not found.', $name));
        }

        return $this->collectors[$name];
    }

    /**
     * Adds a Collector.
     *
     * @param \Viserio\Component\Contract\Profiler\DataCollector $collector
     *
     * @return void
     */
    public function addCollector(DataCollectorContract $collector): void
    {
        $this->collectors[$collector->getName()] = $collector;
    }

    /**
     * Returns true if a Collector for the given name exists.
     *
     * @param string $name A collector name
     *
     * @return bool
     */
    public function hasCollector(string $name): bool
    {
        return isset($this->collectors[$name]);
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Routing\TreeGenerator;

use Viserio\Component\Contract\Routing\Dispatcher as DispatcherContract;
use Viserio\Component\Routing\TreeGenerator\Optimizer\RouteTreeOptimizer;

final class RouteTreeCompiler
{
    /**
     * RouteTreeBuilder instance.
     *
     * @var \Viserio\Component\Routing\TreeGenerator\RouteTreeBuilder
     */
    private $treeBuilder;

    /**
     * RouteTreeOptimizer instance.
     *
     * @var \Viserio\Component\Routing\TreeGenerator\Optimizer\RouteTreeOptimizer
     */
    private $treeOptimizer;

    /**
     * Create a new tree route compailer instance.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\RouteTreeBuilder             $treeBuilder
     * @param \Viserio\Component\Routing\TreeGenerator\Optimizer\RouteTreeOptimizer $treeOptimizer
     */
    public function __construct(RouteTreeBuilder $treeBuilder, RouteTreeOptimizer $treeOptimizer)
    {
        $this->treeBuilder   = $treeBuilder;
        $this->treeOptimizer = $treeOptimizer;
    }

    /**
     * Complie all added routes to a router handler.
     *
     * @param array $routes
     *
     * @return string
     */
    public function compile(array $routes): string
    {
        $routeTree = $this->treeOptimizer->optimize(
            $this->treeBuilder->build($routes)
        );

        $code         = new PHPCodeCollection();
        $code->indent = 1;

        $this->compileRouteTree($code, $routeTree);

        $rootRouteCode         = new PHPCodeCollection();
        $rootRouteCode->indent = 2;

        $this->compileNotFound($rootRouteCode);

        return $this->createRouterClassTemplate(\substr($rootRouteCode->code, 0, -\strlen(\PHP_EOL)), $code->code);
    }

    /**
     * Creating a template for the router class.
     *
     * @param string $rootRoute
     * @param string $body
     *
     * @return string
     */
    private function createRouterClassTemplate(string $rootRoute, string $body): string
    {
        $template = <<<'PHP'
<?php
return function ($method, $uri) {
    if ($uri === '') {
{root_route}
    } elseif ($uri[0] !== '/') {
        throw new \RuntimeException("Cannot match route: non-empty uri must be prefixed with '/', '{$uri}' given");
    }

    $segments = \explode('/', \substr($uri, 1));
{body}
};
PHP;

        return \strtr($template, ['{root_route}' => $rootRoute, '{body}' => $body]);
    }

    /**
     * Compile the counter for the segments check.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\PHPCodeCollection $code
     * @param array                                                      $routeTree
     */
    private function compileRouteTree(PHPCodeCollection $code, array $routeTree): void
    {
        $code->appendLine('switch (count($segments)) {');

        $code->indent++;

        foreach ($routeTree[1] as $segmentDepth => $nodes) {
            $code->appendLine('case ' . $segmentDepth . ':');

            $code->indent++;

            $segmentVariables = [];

            for ($i = 0; $i < $segmentDepth; $i++) {
                $segmentVariables[$i] = '$s' . $i;
            }

            $code->appendLine('[' . \implode(', ', $segmentVariables) . '] = $segments;');

            $this->compileSegmentNodes($code, $nodes, $segmentVariables);
            $this->compileDisallowedHttpMethodOrNotFound($code);

            $code->appendLine('break;');

            $code->indent--;

            $code->appendLine();
        }

        $code->appendLine('default:');

        $code->indent++;

        $this->compileNotFound($code);

        $code->indent--;
        $code->indent--;

        $code->append('}');
    }

    /**
     * Compile the segemtns nodes to if statements.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\PHPCodeCollection      $code
     * @param \Viserio\Component\Routing\TreeGenerator\ChildrenNodeCollection $nodes
     * @param array                                                           $segmentVariables
     * @param array                                                           $parameters
     */
    private function compileSegmentNodes(
        PHPCodeCollection $code,
        ChildrenNodeCollection $nodes,
        array $segmentVariables,
        array $parameters = []
    ): void {
        $originalParameters = $parameters;

        foreach ($nodes->getChildren() as $node) {
            $parameters       = $originalParameters;
            $segmentMatchers  = $node->getMatchers();
            $conditions       = [];
            $currentParameter = \count($parameters) === 0 ? 0 : \max(\array_keys($parameters)) + 1;
            $count            = $currentParameter;

            foreach ($segmentMatchers as $segmentDepth => $matcher) {
                $conditions[] = $matcher->getConditionExpression($segmentVariables[$segmentDepth], $count++);
            }

            $code->appendLine('if (' . \implode(' && ', $conditions) . ') {');

            $code->indent++;

            $count = $currentParameter;

            foreach ($segmentMatchers as $segmentDepth => $matcher) {
                $matchedParameters = $matcher->getMatchedParameterExpressions(
                    $segmentVariables[$segmentDepth],
                    $count++
                );

                foreach ($matchedParameters as $parameterKey => $matchedParameter) {
                    $parameters[$parameterKey] = $matchedParameter;
                }
            }

            $contents = $node->getContents();

            if ($contents instanceof MatchedRouteDataMap) {
                $this->compiledRouteHttpMethodMatch($code, $contents, $parameters);
            } else {
                $this->compileSegmentNodes($code, $contents, $segmentVariables, $parameters);
            }

            $code->indent--;

            $code->appendLine('}');
        }
    }

    /**
     * Compile the route http method match switch.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\PHPCodeCollection   $code
     * @param \Viserio\Component\Routing\TreeGenerator\MatchedRouteDataMap $routeDataMap
     * @param array                                                        $parameters
     */
    private function compiledRouteHttpMethodMatch(
        PHPCodeCollection $code,
        MatchedRouteDataMap $routeDataMap,
        array $parameters
    ): void {
        $code->appendLine('switch ($method) {');

        $code->indent++;

        foreach ($routeDataMap->getHttpMethodRouteDataMap() as $item) {
            [$httpMethods, $routeData] = $item;

            foreach ($httpMethods as $httpMethod) {
                $code->appendLine('case \'' . $httpMethod . '\':');
            }

            $code->indent++;

            $this->compileFoundRoute($code, $routeData, $parameters);

            $code->indent--;
        }

        $code->appendLine('default:');

        $code->indent++;

        foreach ($routeDataMap->allowedHttpMethods() as $method) {
            $code->appendLine('$allowedHttpMethods[] = \'' . $method . '\';');
        }

        $code->appendLine('break;');

        $code->indent--;
        $code->indent--;

        $code->appendLine('}');
    }

    /**
     * Compile the return data.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\PHPCodeCollection $code
     */
    private function compileNotFound(PHPCodeCollection $code): void
    {
        $code->appendLine('return [' . DispatcherContract::NOT_FOUND . '];');
    }

    /**
     * Compile disallowed http method or not found data check.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\PHPCodeCollection $code
     */
    private function compileDisallowedHttpMethodOrNotFound(PHPCodeCollection $code): void
    {
        $code->appendLine(
            'return ' .
            'isset($allowedHttpMethods) '
            . '? '
            . '['
            . DispatcherContract::HTTP_METHOD_NOT_ALLOWED
            . ', $allowedHttpMethods] '
            . ': '
            . '['
            . DispatcherContract::NOT_FOUND
            . '];'
        );
    }

    /**
     * Compile the found route data.
     *
     * @param \Viserio\Component\Routing\TreeGenerator\PHPCodeCollection $code
     * @param array                                                      $foundRoute
     * @param array                                                      $parameterExpressions
     */
    private function compileFoundRoute(PHPCodeCollection $code, array $foundRoute, array $parameterExpressions): void
    {
        $parameters = '[';

        foreach ($foundRoute[0] as $index => $parameterName) {
            $parameters .= '\'' . $parameterName . '\' => ' . $parameterExpressions[$index] . ', ';
        }

        if (\strlen($parameters) > 2) {
            $parameters = \substr($parameters, 0, -2);
        }

        $parameters .= ']';

        $code->appendLine(
            'return ['
            . DispatcherContract::FOUND
            . ', '
            . '\'' . $foundRoute[1] . '\''
            . ', '
            . $parameters
            . '];'
        );
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Http\Stream;

use Psr\Http\Message\StreamInterface;
use Throwable;
use Viserio\Component\Contract\Http\Exception\BadMethodCallException;
use Viserio\Component\Contract\Http\Exception\LogicException;

/**
 * Compose stream implementations based on a hash of functions.
 *
 * Allows for easy testing and extension of a provided stream without needing
 * to create a concrete class for a simple extension point.
 */
class FnStream implements StreamInterface
{
    /**
     * Methods that must be implemented in the given array.
     *
     * @var array
     */
    private const SLOTS = ['__toString', 'close', 'detach', 'rewind',
        'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write',
        'isReadable', 'read', 'getContents', 'getMetadata', ];

    /**
     * Create a new fn stream instance.
     *
     * @param array $methods
     */
    public function __construct(array $methods)
    {
        // Create the functions on the class
        foreach ($methods as $name => $fn) {
            $this->{'_fn_' . $name} = $fn;
        }
    }

    /**
     * The close method is called on the underlying stream only if possible.
     *
     * @return void
     */
    public function __destruct()
    {
        if (isset($this->_fn_close)) {
            \call_user_func($this->_fn_close);
        }
    }

    /**
     * Lazily determine which methods are not implemented.
     *
     * @param mixed $name
     *
     * @throws \Viserio\Component\Contract\Http\Exception\BadMethodCallException
     *
     * @return void
     */
    public function __get($name): void
    {
        throw new BadMethodCallException(
            \str_replace('_fn_', '', $name) . '() is not implemented in the FnStream'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function __toString(): string
    {
        try {
            return \call_user_func($this->_fn___toString);
        } catch (Throwable $exception) {
            // Really, PHP? https://bugs.php.net/bug.php?id=53648
            \trigger_error(self::class . '::__toString exception: ' . (string) $exception, \E_USER_ERROR);

            return '';
        }
    }

    /**
     * An unserialize would allow the __destruct to run when the unserialized value goes out of scope.
     *
     * @throws \Viserio\Component\Contract\Http\Exception\LogicException
     */
    public function __wakeup(): void
    {
        throw new LogicException('FnStream should never be unserialized.');
    }

    /**
     * Adds custom functionality to an underlying stream by intercepting
     * specific method calls.
     *
     * @param StreamInterface $stream  Stream to decorate
     * @param array           $methods Hash of method name to a closure
     *
     * @return \Viserio\Component\Http\Stream\FnStream
     */
    public static function decorate(StreamInterface $stream, array $methods): self
    {
        // If any of the required methods were not provided, then simply
        // proxy to the decorated stream.
        foreach (\array_diff(self::SLOTS, \array_keys($methods)) as $diff) {
            $methods[$diff] = [$stream, $diff];
        }

        return new self($methods);
    }

    /**
     * {@inheritdoc}
     */
    public function close(): void
    {
        \call_user_func($this->_fn_close);
    }

    /**
     * {@inheritdoc}
     */
    public function detach()
    {
        return \call_user_func($this->_fn_detach);
    }

    /**
     * {@inheritdoc}
     */
    public function getSize(): ?int
    {
        return \call_user_func($this->_fn_getSize);
    }

    /**
     * {@inheritdoc}
     */
    public function tell(): int
    {
        return \call_user_func($this->_fn_tell);
    }

    /**
     * {@inheritdoc}
     */
    public function eof(): bool
    {
        return \call_user_func($this->_fn_eof);
    }

    /**
     * {@inheritdoc}
     */
    public function isSeekable(): bool
    {
        return \call_user_func($this->_fn_isSeekable);
    }

    /**
     * {@inheritdoc}
     */
    public function rewind(): void
    {
        \call_user_func($this->_fn_rewind);
    }

    /**
     * {@inheritdoc}
     */
    public function seek($offset, $whence = \SEEK_SET): void
    {
        \call_user_func($this->_fn_seek, $offset, $whence);
    }

    /**
     * {@inheritdoc}
     */
    public function isWritable(): bool
    {
        return \call_user_func($this->_fn_isWritable);
    }

    /**
     * {@inheritdoc}
     */
    public function write($string): int
    {
        return \call_user_func($this->_fn_write, $string);
    }

    /**
     * {@inheritdoc}
     */
    public function isReadable(): bool
    {
        return \call_user_func($this->_fn_isReadable);
    }

    /**
     * {@inheritdoc}
     */
    public function read($length): string
    {
        return \call_user_func($this->_fn_read, $length);
    }

    /**
     * {@inheritdoc}
     */
    public function getContents(): string
    {
        return \call_user_func($this->_fn_getContents);
    }

    /**
     * {@inheritdoc}
     */
    public function getMetadata($key = null)
    {
        return \call_user_func($this->_fn_getMetadata, $key);
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Console\Helper;

use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper;
use Symfony\Component\Console\Output\OutputInterface;

class DescriptorHelper extends BaseDescriptorHelper
{
    /**
     * {@inheritdoc}
     */
    public function describe(OutputInterface $output, $object, array $options = []): void
    {
        $options = \array_merge([
            'raw_text' => false,
            'format'   => 'txt',
        ], $options);

        if ($options['raw_text'] === false && $options['format'] === 'txt') {
            $this->register('txt', new TextDescriptor());
        }

        parent::describe($output, $object, $options);
    }
}


<?php
declare(strict_types=1);
namespace Viserio\Component\Cron\Bootstrap;

use Closure;
use Viserio\Component\Contract\Cron\CronJob as CronJobContract;
use Viserio\Component\Contract\Cron\Schedule as ScheduleContract;
use Viserio\Component\Contract\Foundation\BootstrapState as BootstrapStateContract;
use Viserio\Component\Contract\Foundation\Kernel as KernelContract;
use Viserio\Component\Foundation\Bootstrap\AbstractLoadFiles;
use Viserio\Component\Foundation\Bootstrap\LoadServiceProvider;

class CronScheduling extends AbstractLoadFiles implements BootstrapStateContract
{
    /**
     * {@inheritdoc}
     */
    public static function getPriority(): int
    {
        return 128;
    }

    /**
     * {@inheritdoc}
     */
    public static function getType(): string
    {
        return BootstrapStateContract::TYPE_AFTER;
    }

    /**
     * {@inheritdoc}
     */
    public static function getBootstrapper(): string
    {
        return LoadServiceProvider::class;
    }

    /**
     * {@inheritdoc}
     */
    public static function bootstrap(KernelContract $kernel): void
    {
        $schedule = $kernel->getContainer()->get(ScheduleContract::class);

        foreach ((array) require $kernel->getConfigPath('cron.php') as $class) {
            if ($class instanceof CronJobContract) {
                $class::register($schedule);
            } elseif ($class instanceof Closure) {
                $class($schedule);
            }
        }
    }
}