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

use Closure;
use InvalidArgumentException;
use Viserio\Component\Contract\Support\Arrayable;
use Viserio\Component\Contract\View\Engine as EngineContract;
use Viserio\Component\Contract\View\EngineResolver as EngineResolverContract;
use Viserio\Component\Contract\View\Factory as FactoryContract;
use Viserio\Component\Contract\View\Finder as FinderContract;
use Viserio\Component\Contract\View\View as ViewContract;
use Viserio\Component\View\Traits\NormalizeNameTrait;

class ViewFactory implements FactoryContract
{
    use NormalizeNameTrait;

    /**
     * Array of registered view name aliases.
     *
     * @var array
     */
    protected $aliases = [];

    /**
     * All of the registered view names.
     *
     * @var array
     */
    protected $names = [];

    /**
     * Debug.
     *
     * @var string
     */
    protected $debug;

    /**
     * Register a view extension.
     *
     * @var array
     */
    protected static $extensions = [
        'php'   => 'php',
        'phtml' => 'php',
        'css'   => 'file',
        'js'    => 'file',
        'md'    => 'markdown',
    ];

    /**
     * Data that should be available to all templates.
     *
     * @var array
     */
    protected $shared = [];

    /**
     * The engines instance.
     *
     * @var \Viserio\Component\Contract\View\EngineResolver
     */
    private $engines;

    /**
     * The view finder implementation.
     *
     * @var \Viserio\Component\Contract\View\Finder
     */
    private $finder;

    /**
     * Create a new factory instance.
     *
     * @param \Viserio\Component\Contract\View\EngineResolver $engines
     * @param \Viserio\Component\Contract\View\Finder         $finder
     */
    public function __construct(EngineResolverContract $engines, FinderContract $finder)
    {
        $this->engines = $engines;
        $this->finder  = $finder;

        $this->share('__env', $this);
    }

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

    /**
     * {@inheritdoc}
     */
    public function getExtensions(): array
    {
        return self::$extensions;
    }

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

    /**
     * {@inheritdoc}
     */
    public function shared(string $key, $default = null)
    {
        return $this->shared[$key] ?? $default;
    }

    /**
     * {@inheritdoc}
     */
    public function getFinder(): FinderContract
    {
        return $this->finder;
    }

    /**
     * {@inheritdoc}
     */
    public function exists(string $view): bool
    {
        try {
            $this->getFinder()->find($view);
        } catch (InvalidArgumentException $exception) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function file(string $path, array $data = [], array $mergeData = []): ViewContract
    {
        $data   = \array_merge($mergeData, $this->parseData($data));
        $engine = $this->getEngineFromPath($path);

        return $this->getView($this, $engine, $path, ['path' => $path], $data);
    }

    /**
     * {@inheritdoc}
     */
    public function create(string $view, array $data = [], array $mergeData = []): ViewContract
    {
        if (isset($this->aliases[$view])) {
            $view = $this->aliases[$view];
        }

        $view     = $this->normalizeName($view);
        $fileInfo = $this->getFinder()->find($view);

        return $this->getView(
            $this,
            $this->getEngineFromPath($fileInfo['path']),
            $view,
            $fileInfo,
            \array_merge($mergeData, $this->parseData($data))
        );
    }

    /**
     * {@inheritdoc}
     */
    public function of(string $view, array $data = []): ViewContract
    {
        return $this->create($this->names[$view], $data);
    }

    /**
     * {@inheritdoc}
     */
    public function name(string $view, string $name): FactoryContract
    {
        $this->names[$name] = $view;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function alias(string $view, string $alias): FactoryContract
    {
        $this->aliases[$alias] = $view;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function renderEach(string $view, array $data, string $iterator, string $empty = 'raw|'): string
    {
        $result = '';

        // If is actually data in the array, we will loop through the data and append
        // an instance of the partial view to the final result HTML passing in the
        // iterated value of this data array, allowing the views to access them.
        if (\count($data) > 0) {
            foreach ($data as $key => $value) {
                $data = ['key' => $key, $iterator => $value];
                $result .= $this->create($view, $data)->render();
            }
        } else {
            // If there is no data in the array, we will render the contents of the empty view.
            $result = $this->create($empty)->render();
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function getEngineFromPath(string $path): EngineContract
    {
        $engine    = \explode('|', $path);
        $path      = $engine[1] ?? $path;
        $extension = $this->getExtension($path);

        if ($extension === null) {
            throw new InvalidArgumentException(\sprintf('Unrecognized extension in file: [%s].', $path));
        }

        return $this->engines->resolve(self::$extensions[$extension]);
    }

    /**
     * {@inheritdoc}
     */
    public function share($key, $value = null)
    {
        $keys = \is_array($key) ? $key : [$key => $value];

        foreach ($keys as $k => $v) {
            $this->shared[$k] = $v;
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     *
     * @codeCoverageIgnore
     */
    public function addLocation(string $location): FactoryContract
    {
        $this->getFinder()->addLocation($location);

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @codeCoverageIgnore
     */
    public function addNamespace(string $namespace, $hints): FactoryContract
    {
        $this->getFinder()->addNamespace($namespace, $hints);

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @codeCoverageIgnore
     */
    public function replaceNamespace(string $namespace, $hints): FactoryContract
    {
        $this->getFinder()->replaceNamespace($namespace, $hints);

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @codeCoverageIgnore
     */
    public function prependNamespace(string $namespace, $hints): FactoryContract
    {
        $this->getFinder()->prependNamespace($namespace, $hints);

        return $this;
    }

    /**
     * Flush the cache of views located by the finder.
     */
    public function flushFinderCache(): void
    {
        $this->getFinder()->reset();
    }

    /**
     * {@inheritdoc}
     */
    public function addExtension(string $extension, string $engine, Closure $resolver = null): FactoryContract
    {
        $this->getFinder()->addExtension($extension);

        if ($resolver !== null) {
            $this->engines->register($engine, $resolver);
        }

        if (isset(self::$extensions[$extension])) {
            unset(self::$extensions[$extension]);
        }

        self::$extensions = \array_merge([$extension => $engine], self::$extensions);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getEngineResolver(): EngineResolverContract
    {
        return $this->engines;
    }

    /**
     * Parse the given data into a raw array.
     *
     * @param mixed $data
     *
     * @return array
     */
    protected function parseData($data): array
    {
        return $data instanceof Arrayable ? $data->toArray() : $data;
    }

    /**
     * Get the extension used by the view file.
     *
     * @param string $path
     *
     * @return null|string
     */
    protected function getExtension(string $path): ?string
    {
        $callback = function ($value) use ($path) {
            return $this->endsWith($path, $value);
        };

        foreach (\array_keys(self::$extensions) as $key => $value) {
            if ($callback($value)) {
                return $value;
            }
        }

        return null;
    }

    /**
     * Get the right view object.
     *
     * @param \Viserio\Component\Contract\View\Factory            $factory
     * @param \Viserio\Component\Contract\View\Engine             $engine
     * @param string                                              $view
     * @param array                                               $fileInfo
     * @param array|\Viserio\Component\Contract\Support\Arrayable $data
     *
     * @return \Viserio\Component\View\View
     */
    protected function getView(
        FactoryContract $factory,
        EngineContract $engine,
        string $view,
        array $fileInfo,
        $data = []
    ): View {
        return new View($factory, $engine, $view, $fileInfo, $data);
    }

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

        if ($length === 0) {
            return true;
        }

        return \mb_substr($haystack, -$length) === $needle;
    }
}


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

use Psr\Http\Message\ServerRequestInterface;
use Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException;

final class RequestCookies extends AbstractCookieCollector
{
    /**
     * Create a new cookie collection instance.
     *
     * @param array $cookies
     *
     * @throws \Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException
     */
    public function __construct(array $cookies = [])
    {
        foreach ($cookies as $cookie) {
            if (! ($cookie instanceof Cookie)) {
                throw new InvalidArgumentException(\sprintf(
                    'The object [%s] must be an instance of [%s].',
                    \get_class($cookie),
                    Cookie::class
                ));
            }

            $this->cookies[$cookie->getName()] = $cookie;
        }
    }

    /**
     * Creates a Cookie instance from a Set-Cookie header value.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request
     *
     * @throws \Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException
     *
     * @return self
     */
    public static function fromRequest(ServerRequestInterface $request): self
    {
        return new static(self::listFromCookieString($request->getHeaderLine('cookie')));
    }

    /**
     * Render Cookies into a Request.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request
     *
     * @return \Psr\Http\Message\ServerRequestInterface
     */
    public function renderIntoCookieHeader(ServerRequestInterface $request): ServerRequestInterface
    {
        $cookieString = \implode('; ', $this->cookies);

        return $request->withHeader('cookie', $cookieString);
    }

    /**
     * Create a list of Cookies from a Cookie header value string.
     *
     * @param string $string
     *
     * @return array
     */
    protected static function listFromCookieString(string $string): array
    {
        $cookies = self::splitOnAttributeDelimiter($string);

        return \array_map(static function ($cookiePair) {
            return self::oneFromCookiePair($cookiePair);
        }, $cookies);
    }

    /**
     * Create one Cookie from a cookie key/value header value string.
     *
     * @param string $string
     *
     * @return \Viserio\Component\Cookie\Cookie
     */
    protected static function oneFromCookiePair(string $string): Cookie
    {
        [$name, $value] = self::splitCookiePair($string);

        $cookie = new Cookie($name);

        if (null !== $value) {
            $cookie = $cookie->withValue($value);
        }

        return $cookie;
    }
}


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

use ReflectionProperty;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use Viserio\Component\Console\ConsoleEvents;

final class ConsoleErrorEvent extends ConsoleEvent
{
    /**
     * Create a new console error event.
     *
     * @param null|\Symfony\Component\Console\Command\Command   $command
     * @param \Symfony\Component\Console\Input\InputInterface   $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     * @param \Throwable                                        $error
     */
    public function __construct(?Command $command, InputInterface $input, OutputInterface $output, Throwable $error)
    {
        $this->name       = ConsoleEvents::ERROR;
        $this->target     = $command;
        $this->parameters = [
            'input'     => $input,
            'output'    => $output,
            'error'     => $error,
            'exit_code' => $error->getCode() ?: 1,
        ];
    }

    /**
     * Returns the thrown exception.
     *
     * @return \Throwable The thrown exception
     */
    public function getError(): Throwable
    {
        return $this->parameters['error'];
    }

    /**
     * Replaces the thrown exception.
     *
     * This exception will be thrown if no response is set in the event.
     *
     * @param \Throwable $error
     *
     * @return void
     */
    public function setError(Throwable $error): void
    {
        $this->parameters['error']     = $error;
        $this->parameters['exit_code'] = $error->getCode() ?: 1;
    }

    /**
     * Sets the exit code.
     *
     * @param int $exitCode The command exit code
     *
     * @throws \ReflectionException
     *
     * @return void
     */
    public function setExitCode(int $exitCode): void
    {
        $this->parameters['exit_code'] = $exitCode;

        $r = new ReflectionProperty($this->parameters['error'], 'code');
        $r->setAccessible(true);
        $r->setValue($this->parameters['error'], $this->parameters['exit_code']);
    }

    /**
     * Gets the exit code.
     *
     * @return int The command exit code
     */
    public function getExitCode(): int
    {
        return $this->parameters['exit_code'];
    }
}


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

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Macroable\Macroable;
use Viserio\Component\Contract\Routing\Exception\RouteNotFoundException;
use Viserio\Component\Contract\Routing\Exception\UrlGenerationException;
use Viserio\Component\Contract\Routing\Route as RouteContract;
use Viserio\Component\Contract\Routing\RouteCollection as RouteCollectionContract;
use Viserio\Component\Contract\Routing\UrlGenerator as UrlGeneratorContract;

class UrlGenerator implements UrlGeneratorContract
{
    use Macroable;

    /**
     * The named parameter defaults.
     *
     * @var array
     */
    public $defaultParameters = [];

    /**
     * The route collection.
     *
     * @var \Viserio\Component\Contract\Routing\RouteCollection
     */
    protected $routes;

    /**
     * The request instance.
     *
     * @var \Psr\Http\Message\ServerRequestInterface
     */
    protected $request;

    /**
     * The request instance.
     *
     * @var \Psr\Http\Message\UriFactoryInterface
     */
    protected $uriFactory;

    /**
     * This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
     *
     * PHP's rawurlencode() encodes all chars except "a-zA-Z0-9-._~" according to RFC 3986. But we want to allow some chars
     * to be used in their literal form (reasons below). Other chars inside the path must of course be encoded, e.g.
     * "?" and "#" (would be interpreted wrongly as query and fragment identifier),
     * "'" and """ (are used as delimiters in HTML).
     *
     * @var array
     */
    protected static $dontEncode = [
        // the slash can be used to designate a hierarchical structure and we want allow using it with this meaning
        // some webservers don't allow the slash in encoded form in the path for security reasons anyway
        // see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss
        '%2F' => '/',
        // the following chars are general delimiters in the URI specification but have only special meaning in the authority component
        // so they can safely be used in the path in unencoded form
        '%40' => '@',
        '%3A' => ':',
        // these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally
        // so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability
        '%3B' => ';',
        '%2C' => ',',
        '%3D' => '=',
        '%2B' => '+',
        '%21' => '!',
        '%2A' => '*',
        '%7C' => '|',
        '%3F' => '?',
        '%26' => '&',
        '%23' => '#',
        '%25' => '%',
    ];

    /**
     * Create a new URL Generator instance.
     *
     * @param \Viserio\Component\Contract\Routing\RouteCollection $routes
     * @param \Psr\Http\Message\ServerRequestInterface            $request
     * @param \Psr\Http\Message\UriFactoryInterface               $uriFactory
     */
    public function __construct(
        RouteCollectionContract $routes,
        ServerRequestInterface $request,
        UriFactoryInterface $uriFactory
    ) {
        $this->routes     = $routes;
        $this->request    = $request;
        $this->uriFactory = $uriFactory;
    }

    /**
     * Returns the target path as relative reference from the base path.
     *
     * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
     * Both paths must be absolute and not contain relative parts.
     * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
     * Furthermore, they can be used to reduce the link size in documents.
     *
     * Example target paths, given a base path of "/a/b/c/d":
     * - "/a/b/c/d"     -> ""
     * - "/a/b/c/"      -> "./"
     * - "/a/b/"        -> "../"
     * - "/a/b/c/other" -> "other"
     * - "/a/x/y"       -> "../../x/y"
     *
     * @param string $basePath   The base path
     * @param string $targetPath The target path
     *
     * @return string The relative target path
     */
    public static function getRelativePath(string $basePath, string $targetPath): string
    {
        if ($basePath === $targetPath) {
            return '';
        }

        $sourceDirs = \explode('/', isset($basePath[0]) && '/' === $basePath[0] ? \substr($basePath, 1) : $basePath);
        $targetDirs = \explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? \substr($targetPath, 1) : $targetPath);

        \array_pop($sourceDirs);

        $targetFile = \array_pop($targetDirs);

        foreach ($sourceDirs as $i => $dir) {
            if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
                unset($sourceDirs[$i], $targetDirs[$i]);
            } else {
                break;
            }
        }

        $targetDirs[] = $targetFile;
        $path         = \str_repeat('../', \count($sourceDirs)) . \implode('/', $targetDirs);

        // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
        // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
        // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
        // (see http://tools.ietf.org/html/rfc3986#section-4.2).
        return $path === ''  ||
            $path[0] === '/' ||
            (false !== ($colonPos = \strpos($path, ':')) && ($colonPos < ($slashPos = \strpos($path, '/')) || false === $slashPos))
            ? "./${path}" : $path;
    }

    /**
     * {@inheritdoc}
     */
    public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
    {
        $routes = $this->routes;

        if (($route = $routes->getByName($name)) !== null || ($route = $routes->getByAction($name)) !== null) {
            return $this->toRoute($route, $parameters, $referenceType);
        }

        throw new RouteNotFoundException(\sprintf('Unable to generate a URL for the named/action route [%s] as such route does not exist.', $name));
    }

    /**
     * Get the URL for a given route instance.
     *
     * @param \Viserio\Component\Contract\Routing\Route $route
     * @param array                                     $parameters
     * @param int                                       $referenceType
     *
     * @return string
     */
    protected function toRoute(RouteContract $route, array $parameters, int $referenceType): string
    {
        $path = $this->prepareRoutePath($route, $parameters);
        $uri  = $this->uriFactory->createUri('/' . \ltrim($path, '/'));

        if (($host = $route->getDomain()) !== null) {
            $uri = $uri->withHost($host);
        } else {
            $uri = $uri->withHost($this->request->getUri()->getHost());
        }

        $requiredSchemes = $this->isSchemeRequired($route);

        if ($referenceType === self::ABSOLUTE_URL || $requiredSchemes || $referenceType === self::NETWORK_PATH) {
            $uri = $this->addPortAndSchemeToUri($uri, $route);
        }

        if ($referenceType === self::ABSOLUTE_URL || $requiredSchemes) {
            return (string) $uri;
        }

        if ($referenceType === self::NETWORK_PATH) {
            $uri = $uri->withScheme('');

            return (string) $uri;
        }

        return '/' . self::getRelativePath('//' . $uri->getHost() . '/', (string) $uri);
    }

    /**
     * Prepare route patch with all parameters and encode the path.
     *
     * @param \Viserio\Component\Contract\Routing\Route $route
     * @param array                                     $parameters
     *
     * @return string
     */
    protected function prepareRoutePath(RouteContract $route, array $parameters): string
    {
        $parameters = \array_replace($route->getParameters(), $parameters);

        // First we will construct the entire URI including the root and query string. Once it
        // has been constructed, we'll make sure we don't have any missing parameters or we
        // will need to throw the exception to let the developers know one was not given.
        $path = $this->addQueryString(
            $this->replaceRouteParameters($route->getUri(), $parameters),
            $parameters
        );

        \preg_match('/\\{(.*?)\\}/', $path, $matches);

        if (isset($matches[1]) && $matches[1] !== '') {
            throw new UrlGenerationException($route);
        }

        $path = $this->replacePathSegments($path);

        // Once we have ensured that there are no missing parameters in the URI we will encode
        // the URI and prepare it for returning to the developer.
        return \strtr(\rawurlencode($path), self::$dontEncode);
    }

    /**
     * Check if a scheme is required.
     *
     * @param \Viserio\Component\Contract\Routing\Route $route
     *
     * @return bool
     */
    protected function isSchemeRequired(RouteContract $route): bool
    {
        $requiredSchemes = false;
        $requestScheme   = $this->request->getUri()->getScheme();

        if ($route->isHttpOnly()) {
            $requiredSchemes = $requestScheme !== 'http';
        } elseif ($route->isHttpsOnly()) {
            $requiredSchemes = $requestScheme !== 'https';
        }

        return $requiredSchemes;
    }

    /**
     * The path segments "." and ".." are interpreted as relative reference when resolving a URI;
     * see http://tools.ietf.org/html/rfc3986#section-3.3 so we need to encode them as they are not used for this purpose here
     * otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route.
     *
     * @param string $path
     *
     * @return string
     */
    protected function replacePathSegments(string $path): string
    {
        $path = \strtr($path, ['/../' => '/%2E%2E/', '/./' => '/%2E/']);

        if ('/..' === \substr($path, -3)) {
            $path = \substr($path, 0, -2) . '%2E%2E';
        } elseif ('/.' === \substr($path, -2)) {
            $path = \substr($path, 0, -1) . '%2E';
        }

        return $path;
    }

    /**
     * Add the port and scheme to the uri if necessary.
     *
     * @param \Psr\Http\Message\UriInterface            $uri
     * @param \Viserio\Component\Contract\Routing\Route $route
     *
     * @return \Psr\Http\Message\UriInterface
     */
    protected function addPortAndSchemeToUri(UriInterface $uri, RouteContract $route): UriInterface
    {
        if ($route->isHttpOnly()) {
            $secure = 'http';
            $port   = 80;
        } elseif ($route->isHttpsOnly()) {
            $secure = 'https';
            $port   = 443;
        } else {
            $requestUri = $this->request->getUri();
            $secure     = $requestUri->getScheme();
            $port       = $requestUri->getPort();
        }

        $uri = $uri->withScheme($secure);

        return $uri->withPort($port);
    }

    /**
     * Replace all of the wildcard parameters for a route path.
     *
     * @param string $path
     * @param array  $parameters
     *
     * @return string
     */
    protected function replaceRouteParameters(string $path, array &$parameters): string
    {
        $path = $this->replaceNamedParameters($path, $parameters);

        $path = \preg_replace_callback('/\{.*?\}/', static function ($match) use (&$parameters) {
            if (\count($parameters) === 0 && ! (\substr($match[0], -\strlen('?}')) === '?}')) {
                return $match[0];
            }

            return \array_shift($parameters);
        }, $path);

        return \trim(\preg_replace('/\{.*?\?\}/', '', $path), '/');
    }

    /**
     * Replace all of the named parameters in the path.
     *
     * @param string $path
     * @param array  $parameters
     *
     * @return string
     */
    protected function replaceNamedParameters(string $path, array &$parameters): string
    {
        return \preg_replace_callback('/\{(.*?)\??\}/', static function ($m) use (&$parameters) {
            if (isset($parameters[$m[1]]) && $parameters[$m[1]] !== '') {
                $parameter = $parameters[$m[1]];

                unset($parameters[$m[1]]);

                return $parameter;
            }

            return $m[0];
        }, $path);
    }

    /**
     * Add a query string to the URI.
     *
     * @param string $uri
     * @param array  $parameters
     *
     * @return string
     */
    protected function addQueryString(string $uri, array $parameters): string
    {
        // If the URI has a fragment we will move it to the end of this URI since it will
        // need to come after any query string that may be added to the URL else it is
        // not going to be available. We will remove it then append it back on here.
        if (null !== ($fragment = \parse_url($uri, \PHP_URL_FRAGMENT))) {
            $uri = \preg_replace('/#.*/', '', $uri);
        }

        $uri .= $this->getRouteQueryString($parameters);

        if (null === $fragment) {
            return $uri;
        }

        return $uri . '#' . \strtr(\rawurlencode($fragment), ['%2F' => '/', '%3F' => '?']);
    }

    /**
     * Get the query string for a given route.
     *
     * @param array $parameters
     *
     * @return string
     */
    protected function getRouteQueryString(array $parameters): string
    {
        // First we will get all of the string parameters that are remaining after we
        // have replaced the route wildcards. We'll then build a query string from
        // these string parameters then use it as a starting point for the rest.
        if (\count($parameters) === 0 || \in_array(null, $parameters, true)) {
            return '';
        }

        $query = \http_build_query(
            $keyed = $this->getStringParameters($parameters),
            '',
            '&',
            \PHP_QUERY_RFC3986
        );

        // Lastly, if there are still parameters remaining, we will fetch the numeric
        // parameters that are in the array and add them to the query string or we
        // will make the initial query string if it wasn't started with strings.
        if (\count($keyed) < \count($parameters)) {
            $query .= '&' . \implode(
                '&',
                $this->getNumericParameters($parameters)
            );
        }

        // "/" and "?" can be left decoded for better user experience, see
        // http://tools.ietf.org/html/rfc3986#section-3.4
        return '?' . \trim(\strtr($query, ['%2F' => '/']), '&');
    }

    /**
     * Get the string parameters from a given list.
     *
     * @param array $parameters
     *
     * @return array
     */
    protected function getStringParameters(array $parameters): array
    {
        return \array_filter($parameters, 'is_string', \ARRAY_FILTER_USE_KEY);
    }

    /**
     * Get the numeric parameters from a given list.
     *
     * @param array $parameters
     *
     * @return array
     */
    protected function getNumericParameters(array $parameters): array
    {
        return \array_filter($parameters, 'is_numeric', \ARRAY_FILTER_USE_KEY);
    }
}


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

use Invoker\InvokerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Viserio\Component\Contract\Container\Factory as FactoryContract;
use Viserio\Component\Contract\Container\Traits\ContainerAwareTrait;
use Viserio\Component\Contract\Routing\Route as RouteContract;
use Viserio\Component\Routing\Route\Action as RouteAction;
use Viserio\Component\Routing\Route\Parser as RouteParser;
use Viserio\Component\Routing\Traits\MiddlewareAwareTrait;

class Route implements RouteContract
{
    use ContainerAwareTrait;
    use MiddlewareAwareTrait;

    /**
     * The URI pattern the route responds to.
     *
     * @var string
     */
    protected $uri;

    /**
     * The HTTP methods the route responds to.
     *
     * @var array
     */
    protected $httpMethods = [];

    /**
     * The route action array.
     *
     * @var array
     */
    protected $action;

    /**
     * The controller instance.
     *
     * @var mixed
     */
    protected $controller;

    /**
     * The array of matched parameters.
     *
     * @var array
     */
    protected $parameters = [];

    /**
     * The regular expression requirements.
     *
     * @var array
     */
    protected $wheres = [];

    /**
     * Route identifier.
     *
     * @var string
     */
    protected $identifier;

    /**
     * The invoker instance.
     *
     * @var \Invoker\InvokerInterface
     */
    protected $invoker;

    /**
     * Create a new Route instance.
     *
     * @param array|string        $methods
     * @param string              $uri
     * @param null|array|\Closure $action
     */
    public function __construct($methods, string $uri, $action)
    {
        $this->uri = $uri;
        // According to RFC methods are defined in uppercase (See RFC 7231)
        $this->httpMethods = \array_map('strtoupper', (array) $methods);
        $this->action      = RouteAction::parse($uri, $action);

        if (\in_array('GET', $this->httpMethods, true) && ! \in_array('HEAD', $this->httpMethods, true)) {
            $this->httpMethods[] = 'HEAD';
        }

        if (isset($this->action['prefix'])) {
            $this->addPrefix($this->action['prefix']);
        }

        if (isset($this->action['suffix'])) {
            $this->addSuffix($this->action['suffix']);
        }
    }

    /**
     * Dynamically access route parameters.
     *
     * @param string $key
     *
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getParameter($key);
    }

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

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

    /**
     * {@inheritdoc}
     */
    public function setAction(array $action): RouteContract
    {
        $this->action = $action;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getController()
    {
        [$class] = \explode('@', $this->action['uses']);

        if ($this->controller === null) {
            if ($this->container !== null) {
                $container = $this->container;

                if ($container->has($class)) {
                    $this->controller = $container->get($class);
                } elseif ($container instanceof FactoryContract) {
                    $this->controller = $container->resolve($class);
                }
            } else {
                $this->controller = new $class();
            }
        }

        return $this->controller;
    }

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

    /**
     * Get route identifier.
     *
     * @return string
     */
    public function getIdentifier(): string
    {
        return \implode('|', $this->httpMethods) . $this->getDomain() . $this->uri;
    }

    /**
     * Set a Invoker instance.
     *
     * @param \Invoker\InvokerInterface $invoker
     *
     * @return void
     */
    public function setInvoker(InvokerInterface $invoker): void
    {
        $this->invoker = $invoker;
    }

    /**
     * {@inheritdoc}
     */
    public function getDomain(): ?string
    {
        if (isset($this->action['domain'])) {
            return \str_replace(['http://', 'https://'], '', $this->action['domain']);
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getName(): ?string
    {
        return $this->action['as'] ?? null;
    }

    /**
     * {@inheritdoc}
     */
    public function setName(string $name): RouteContract
    {
        $this->action['as'] = isset($this->action['as']) ? $this->action['as'] . $name : $name;

        return $this;
    }

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

    /**
     * {@inheritdoc}
     */
    public function where($name, ?string $expression = null): RouteContract
    {
        foreach ($this->parseWhere($name, $expression) as $n => $exp) {
            $this->wheres[$n] = $exp;
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function gatherMiddleware(): array
    {
        if (isset($this->action['middleware'])) {
            $this->withMiddleware($this->action['middleware']);
        }

        return \array_unique(
            \array_merge(
                $this->middleware,
                $this->getControllerMiddleware()
            ),
            \SORT_REGULAR
        );
    }

    /**
     * {@inheritdoc}
     */
    public function gatherDisabledMiddleware(): array
    {
        if (isset($this->action['bypass'])) {
            $this->withoutMiddleware($this->action['bypass']);
        }

        return \array_merge(
            $this->bypassedMiddleware,
            $this->getControllerDisabledMiddleware()
        );
    }

    /**
     * {@inheritdoc}
     */
    public function isHttpOnly(): bool
    {
        return \in_array('http', $this->action, true);
    }

    /**
     * {@inheritdoc}
     */
    public function isHttpsOnly(): bool
    {
        return \in_array('https', $this->action, true);
    }

    /**
     * {@inheritdoc}
     */
    public function getActionName(): string
    {
        return $this->action['controller'] ?? 'Closure';
    }

    /**
     * {@inheritdoc}
     */
    public function addPrefix(string $prefix): RouteContract
    {
        $uri = \rtrim($prefix, '/') . '/' . \ltrim($this->uri, '/');

        $this->uri = \trim($uri, '/');

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getPrefix(): string
    {
        return $this->action['prefix'] ?? '';
    }

    /**
     * {@inheritdoc}
     */
    public function addSuffix(string $suffix): RouteContract
    {
        $uri = \rtrim($this->uri) . \ltrim($suffix);

        $this->uri = \trim($uri);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getSuffix(): ?string
    {
        return $this->action['suffix'] ?? null;
    }

    /**
     * {@inheritdoc}
     */
    public function addParameter($name, $value): RouteContract
    {
        $this->parameters[$name] = $value;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameter(string $name, $default = null)
    {
        if (isset($this->parameters[$name])) {
            return $this->parameters[$name];
        }

        return $default;
    }

    /**
     * {@inheritdoc}
     */
    public function hasParameter(string $name): bool
    {
        return isset($this->parameters[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function forgetParameter(string $name): void
    {
        $this->parameters;

        unset($this->parameters[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function getSegments(): array
    {
        return RouteParser::parse($this->uri, $this->wheres);
    }

    /**
     * {@inheritdoc}
     */
    public function run(ServerRequestInterface $serverRequest): ResponseInterface
    {
        $parameters = \array_merge([$serverRequest], $this->parameters);

        if ($this->isControllerAction()) {
            return $this->invoker->call([$this->getController(), $this->getControllerMethod()], $parameters);
        }

        return $this->invoker->call($this->action['uses'], $parameters);
    }

    /**
     * Parse arguments to the where method into an array.
     *
     * @param array|string $name
     * @param null|string  $expression
     *
     * @return array
     */
    protected function parseWhere($name, ?string $expression): array
    {
        if (\is_string($name)) {
            return [$name => $expression];
        }

        $arr = [];

        foreach ($name as $paramName) {
            $arr[$paramName] = $expression;
        }

        return $arr;
    }

    /**
     * Get the bound route controller middleware.
     *
     * @return array
     */
    protected function getControllerMiddleware(): array
    {
        if (! $this->isControllerAction()) {
            return [];
        }

        $controller = $this->getController();

        if (\method_exists($controller, 'gatherMiddleware')) {
            return $controller->gatherMiddleware();
        }

        return [];
    }

    /**
     * Get the bound route controller disabled middleware.
     *
     * @return array
     */
    protected function getControllerDisabledMiddleware(): array
    {
        if (! $this->isControllerAction()) {
            return [];
        }

        $controller = $this->getController();

        if (\method_exists($controller, 'gatherDisabledMiddleware')) {
            return $controller->gatherDisabledMiddleware();
        }

        return [];
    }

    /**
     * Checks whether the route's action is a controller.
     *
     * @return bool
     */
    protected function isControllerAction(): bool
    {
        return \is_string($this->action['uses']);
    }

    /**
     * Get the controller method used for the route.
     *
     * @return string
     */
    protected function getControllerMethod(): string
    {
        return \explode('@', $this->action['uses'])[1];
    }
}


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

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

abstract class AbstractCookieCollector
{
    /**
     * All stored cookies.
     *
     * @var array
     */
    protected $cookies = [];

    /**
     * Checking if request cookie exist.
     *
     * @param string $name
     *
     * @return bool
     */
    public function has(string $name): bool
    {
        return isset($this->cookies[$name]);
    }

    /**
     * Get a request cookie.
     *
     * @param string $name
     *
     * @return null|\Viserio\Component\Contract\Cookie\Cookie|\Viserio\Component\Cookie\Cookie
     */
    public function get(string $name)
    {
        if (! $this->has($name)) {
            return null;
        }

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

    /**
     * Get all request cookies.
     *
     * @return array
     */
    public function getAll(): array
    {
        return \array_values($this->cookies);
    }

    /**
     * Add a request cookie to the stack.
     *
     * @param \Viserio\Component\Contract\Cookie\Cookie|\Viserio\Component\Cookie\Cookie $cookie
     *
     * @throws \Viserio\Component\Contract\Cookie\Exception\InvalidArgumentException
     *
     * @return $this
     */
    public function add($cookie): self
    {
        if ($cookie instanceof Cookie || $cookie instanceof SetCookie) {
            $clone                              = clone $this;
            $clone->cookies[$cookie->getName()] = $cookie;

            return $clone;
        }

        throw new InvalidArgumentException(\sprintf(
            'The object [%s] must be an instance of [%s] or [%s].',
            \get_class($cookie),
            Cookie::class,
            SetCookie::class
        ));
    }

    /**
     * Forget a request cookie.
     *
     * @param string $name
     *
     * @return $this
     */
    public function forget(string $name): self
    {
        $clone = clone $this;

        if (! $clone->has($name)) {
            return $clone;
        }

        unset($clone->cookies[$name]);

        return $clone;
    }

    /**
     * Split string on attributes delimiter to array.
     *
     * @param string $string
     *
     * @return array
     */
    protected static function splitOnAttributeDelimiter(string $string): array
    {
        return \array_filter(\preg_split('@\s*[;]\s*@', $string));
    }

    /**
     * Split a string to array.
     *
     * @param string $string
     *
     * @return array
     */
    protected static function splitCookiePair(string $string): array
    {
        $pairParts = \explode('=', $string, 2);

        if (\count($pairParts) === 1) {
            $pairParts[1] = '';
        }

        return \array_map(static function ($part) {
            if ($part === null) {
                return '';
            }

            return \urldecode($part);
        }, $pairParts);
    }
}


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

trait InjectContentTypeTrait
{
    /**
     * Inject the provided Content-Type, if none is already present.
     *
     * @param string $contentType
     * @param array  $headers
     *
     * @return array Headers with injected Content-Type
     */
    private function injectContentType(string $contentType, array $headers): array
    {
        $hasContentType = \array_reduce(\array_keys($headers), static function ($carry, $item) {
            return $carry ?: (\strtolower($item) === 'content-type');
        }, false);

        if (! $hasContentType) {
            $headers['Content-Type'] = [$contentType];
        }

        return $headers;
    }
}


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

use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Twig\Environment;
use Viserio\Bridge\Twig\Command\DebugCommand;
use Viserio\Bridge\Twig\Command\LintCommand;
use Viserio\Component\Console\Application;
use Viserio\Component\Contract\OptionsResolver\ProvidesDefaultOptions as ProvidesDefaultOptionsContract;
use Viserio\Component\Contract\OptionsResolver\RequiresComponentConfig as RequiresComponentConfigContract;

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' => [
                'twig:debug' => DebugCommand::class,
                'lint:twig'  => LintCommand::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 && $container->has(Environment::class)) {
            $twig = $container->get(Environment::class);

            $console->addCommands([
                new DebugCommand($twig),
                new LintCommand($twig),
            ]);
        }

        return $console;
    }
}


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

use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Viserio\Component\Contract\Exception\ConsoleHandler as ConsoleHandlerContract;
use Viserio\Component\Exception\Console\Handler;

class ConsoleExceptionServiceProvider implements ServiceProviderInterface
{
    /**
     * {@inheritdoc}
     */
    public function getFactories(): array
    {
        return [
            ConsoleHandlerContract::class => [self::class, 'createExceptionHandler'],
            Handler::class                => static function (ContainerInterface $container) {
                return $container->get(ConsoleHandlerContract::class);
            },
        ];
    }

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

    /**
     * Create a new Handler instance.
     *
     * @param \Psr\Container\ContainerInterface $container
     *
     * @return \Viserio\Component\Contract\Exception\ConsoleHandler
     */
    public static function createExceptionHandler(ContainerInterface $container): ConsoleHandlerContract
    {
        $logger = null;

        if ($container->has(LoggerInterface::class)) {
            $logger = $container->get(LoggerInterface::class);
        }

        return new Handler($container->get('config'), $logger);
    }
}


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

use BadMethodCallException;
use Spatie\Macroable\Macroable;
use Stringy\StaticStringy;
use Viserio\Component\Contract\Support\CharacterType;

/**
 * @mixin \Stringy\StaticStringy
 */
class Str
{
    use Macroable {
        __callStatic as macroableCallStatic;
    }

    /**
     * The cache of snake-cased words.
     *
     * @var array
     */
    protected static $snakeCache = [];

    /**
     * The cache of studly-cased words.
     *
     * @var array
     */
    protected static $studlyCache = [];

    /**
     * Creates an instance of Stringy and invokes the given method with the
     * rest of the passed arguments. The optional encoding is expected to be
     * the last argument. For example, the following:
     * StaticStringy::slice('fòôbàř', 0, 3, 'UTF-8'); translates to
     * Stringy::create('fòôbàř', 'UTF-8')->slice(0, 3);
     * The result is not cast, so the return value may be of type Stringy,
     * integer, boolean, etc.
     *
     * @param string  $name
     * @param mixed[] $arguments
     *
     * @return mixed
     */
    public static function __callStatic(string $name, array $arguments)
    {
        try {
            return self::macroableCallStatic($name, $arguments);
        } catch (BadMethodCallException $exception) {
            return \forward_static_call_array([StaticStringy::class, $name], $arguments);
        }
    }

    /**
     * Cap a string with a single instance of a given value.
     *
     * @param string $value
     * @param string $cap
     *
     * @return string
     */
    public static function finish(string $value, string $cap): string
    {
        $quoted = \preg_quote($cap, '/');

        return \preg_replace('/(?:' . $quoted . ')+$/', '', $value) . $cap;
    }

    /**
     * Limit the number of characters in a string.
     *
     * @param string $value
     * @param int    $limit
     * @param string $end
     *
     * @return string
     */
    public static function limit(string $value, int $limit = 100, string $end = '...'): string
    {
        if (\mb_strwidth($value, 'UTF-8') <= $limit) {
            return $value;
        }

        return \rtrim(\mb_strimwidth($value, 0, $limit, '', 'UTF-8')) . $end;
    }

    /**
     * Limit the number of words in a string.
     *
     * @param string $value
     * @param int    $words
     * @param string $end
     *
     * @return string
     */
    public static function words(string $value, int $words = 100, string $end = '...'): string
    {
        \preg_match('/^\s*+(?:\S++\s*+){1,' . $words . '}/u', $value, $matches);

        if (! isset($matches[0]) || \mb_strlen($value) === \mb_strlen($matches[0])) {
            return $value;
        }

        return \rtrim($matches[0]) . $end;
    }

    /**
     * Parse a Class@method style callback into class and method.
     *
     * @param string $callback
     * @param string $default
     *
     * @return array
     */
    public static function parseCallback(string $callback, string $default): array
    {
        return static::contains($callback, '@') ? \explode('@', $callback, 2) : [$callback, $default];
    }

    /**
     * Generate a random string of a given length and character set.
     *
     * @param int    $length     How many characters do you want?
     * @param string $characters Which characters to choose from
     *
     * @return string
     */
    public static function random(int $length = 64, string $characters = CharacterType::PRINTABLE_ASCII): string
    {
        $str = '';
        $l   = self::length($characters) - 1;

        for ($i = 0; $i < $length; $i++) {
            $r = \random_int(0, $l);
            $str .= $characters[$r];
        }

        return $str;
    }

    /**
     * Convert a string to kebab case.
     *
     * @param string $value
     *
     * @return string
     */
    public static function kebab(string $value): string
    {
        return static::snake($value, '-');
    }

    /**
     * Convert a string to snake case.
     *
     * @see https://en.wikipedia.org/wiki/Snake_case
     *
     * @param string $value
     * @param string $delimiter
     *
     * @return string
     */
    public static function snake(string $value, string $delimiter = '_'): string
    {
        $key = $value;

        if (isset(static::$snakeCache[$key][$delimiter])) {
            return static::$snakeCache[$key][$delimiter];
        }

        if (! \ctype_lower($value)) {
            $value = self::normalizeScreamingCase($value);
            $value = \trim($value);
            $value = \mb_strtolower(\preg_replace('/(.)(?=[A-Z0-9])/u', '$1' . $delimiter, $value));
            $value = \preg_replace('/[_\s-]+/', $delimiter, $value);
        }

        return static::$snakeCache[$key][$delimiter] = $value;
    }

    /**
     * Convert a value to studly caps case.
     *
     * @param string $value
     *
     * @return string
     */
    public static function studly(string $value): string
    {
        $key = $value;

        if (isset(static::$studlyCache[$key])) {
            return static::$studlyCache[$key];
        }

        $value = self::normalizeScreamingCase($value);
        $value = \ucwords(\str_replace(['-', '_'], ' ', $value));

        return static::$studlyCache[$key] = \str_replace(' ', '', $value);
    }

    /**
     * Get the plural form of an English word.
     *
     * @param string $value
     * @param int    $count
     *
     * @return string
     */
    public static function plural(string $value, int $count = 2): string
    {
        return Pluralizer::plural($value, $count);
    }

    /**
     * Get the singular form of an English word.
     *
     * @param string $value
     *
     * @return string
     */
    public static function singular(string $value): string
    {
        return Pluralizer::singular($value);
    }

    /**
     * Replace the first occurrence of a given value in the string.
     *
     * @param string $search
     * @param string $replace
     * @param string $subject
     *
     * @return string
     */
    public static function replaceFirst(string $search, string $replace, string $subject): string
    {
        if ($search === '') {
            return $subject;
        }

        $position = \mb_strpos($subject, $search);

        return self::replaceByPosition($subject, $replace, $position, $search);
    }

    /**
     * Replace the last occurrence of a given value in the string.
     *
     * @param string $search
     * @param string $replace
     * @param string $subject
     *
     * @return string
     */
    public static function replaceLast(string $search, string $replace, string $subject): string
    {
        $position = \mb_strrpos($subject, $search);

        return self::replaceByPosition($subject, $replace, $position, $search);
    }

    /**
     * Helper function for replaceLast and replaceFirst.
     *
     * @param string   $subject
     * @param string   $replace
     * @param bool|int $position
     * @param string   $search
     *
     * @return string
     */
    private static function replaceByPosition(string $subject, string $replace, $position, string $search): string
    {
        if ($position !== false) {
            return \substr_replace($subject, $replace, $position, \mb_strlen($search));
        }

        return $subject;
    }

    /**
     * Normalize screaming snake/kebab case value to regular snake/kebab case.
     *
     * @param string $value
     *
     * @return string
     */
    private static function normalizeScreamingCase(string $value): string
    {
        if (\ctype_upper(\str_replace(['_', '-'], '', $value))) {
            return \mb_strtolower($value);
        }

        return $value;
    }
}


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

use Invoker\InvokerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Viserio\Component\Console\Application;
use Viserio\Component\Contract\Console\Exception\LogicException;
use Viserio\Component\Contract\Container\Traits\ContainerAwareTrait;
use Viserio\Component\Contract\Support\Arrayable;

abstract class AbstractCommand extends BaseCommand
{
    use ContainerAwareTrait;

    /**
     * The console command input.
     *
     * @var \Symfony\Component\Console\Input\InputInterface
     */
    protected $input;

    /**
     * The console command output.
     *
     * @var \Symfony\Component\Console\Style\SymfonyStyle
     */
    protected $output;

    /**
     * Indicates whether the command should be shown in the Artisan command list.
     *
     * @var bool
     */
    protected $hidden = false;

    /**
     * The mapping between human readable verbosity levels and Symfony's
     * OutputInterface.
     *
     * @var array
     */
    protected $verbosityMap = [
        'v'      => OutputInterface::VERBOSITY_VERBOSE,
        'vv'     => OutputInterface::VERBOSITY_VERY_VERBOSE,
        'vvv'    => OutputInterface::VERBOSITY_DEBUG,
        'quiet'  => OutputInterface::VERBOSITY_QUIET,
        'normal' => OutputInterface::VERBOSITY_NORMAL,
    ];

    /**
     * The default verbosity of output commands.
     *
     * @var int
     */
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description;

    /**
     * The name and signature of the console command.
     *
     * @var null|string
     */
    protected $signature;

    /**
     * The invoker instance.
     *
     * @var \Invoker\InvokerInterface
     */
    protected $invoker;

    /**
     * Create a new console command instance.
     *
     * @throws \Symfony\Component\Console\Exception\LogicException When the command name is empty
     */
    public function __construct()
    {
        // We will go ahead and set the name, description, and parameters on console
        // commands just to make things a little easier.
        if ($this->signature !== null) {
            $this->configureUsingFluentDefinition();
        } else {
            parent::__construct();
        }

        $this->setDescription($this->description);

        $this->setHidden($this->hidden);

        if ($this->signature === null) {
            $this->specifyParameters();
        }
    }

    /**
     * Get the output implementation.
     *
     * @return \Symfony\Component\Console\Style\SymfonyStyle
     *
     * @codeCoverageIgnore
     */
    public function getOutput(): SymfonyStyle
    {
        return $this->output;
    }

    /**
     * Get the verbosity level in terms of Symfony's OutputInterface level.
     *
     * @param null|int|string $level
     *
     * @return int
     */
    public function getVerbosity($level = null): int
    {
        if ($level === null) {
            return $this->verbosity;
        }

        if (isset($this->verbosityMap[$level])) {
            return $this->verbosityMap[$level];
        }

        return (int) $level;
    }

    /**
     * Set the verbosity level.
     *
     * @param int|string $level
     *
     * @return void
     */
    public function setVerbosity($level): void
    {
        $this->verbosity = $this->getVerbosity($level);
    }

    /**
     * Set a Invoker instance.
     *
     * @param \Invoker\InvokerInterface $invoker
     *
     * @return void
     */
    public function setInvoker(InvokerInterface $invoker): void
    {
        $this->invoker = $invoker;
    }

    /**
     * Gets the application instance for this command.
     *
     * @return null|\Viserio\Component\Console\Application
     */
    public function getApplication(): ?Application
    {
        return parent::getApplication();
    }

    /**
     * Run the console command.
     *
     * @param \Symfony\Component\Console\Input\InputInterface   $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     *
     * @return int
     */
    public function run(InputInterface $input, OutputInterface $output): int
    {
        $this->input  = $input;
        $this->output = new SymfonyStyle(
            $input,
            $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output
        );

        return parent::run($input, $output);
    }

    /**
     * Call another console command.
     *
     * @param string $command
     * @param array  $arguments
     *
     * @return int
     */
    public function call(string $command, array $arguments = []): int
    {
        return $this->getApplication()->call($command, $arguments, $this->getOutput());
    }

    /**
     * Call another console command silently.
     *
     * @param string $command
     * @param array  $arguments
     *
     * @return int
     */
    public function callSilent(string $command, array $arguments = []): int
    {
        return $this->getApplication()->call($command, $arguments, new NullOutput());
    }

    /**
     * Get the value of a command argument.
     *
     * @param null|string $key
     *
     * @return null|array|string
     */
    public function argument(?string $key = null)
    {
        if ($key === null) {
            return $this->input->getArguments();
        }

        return $this->input->getArgument($key);
    }

    /**
     * Get the value of a command option.
     *
     * @param null|string $key
     *
     * @return null|array|string
     */
    public function option(?string $key = null)
    {
        if ($key === null) {
            return $this->input->getOptions();
        }

        return $this->input->getOption($key);
    }

    /**
     * Check if a command option is set.
     *
     * @param string $key
     *
     * @return bool
     */
    public function hasOption(string $key): bool
    {
        return $this->input->hasParameterOption('--' . $key);
    }

    /**
     * Confirm a question with the user.
     *
     * @param string $question
     * @param bool   $default
     *
     * @return bool|string
     */
    public function confirm(string $question, bool $default = false)
    {
        return $this->getOutput()->confirm($question, $default);
    }

    /**
     * Prompt the user for input.
     *
     * @param string      $question
     * @param null|string $default
     *
     * @return null|string
     */
    public function ask(string $question, ?string $default = null): ?string
    {
        return $this->getOutput()->ask($question, $default);
    }

    /**
     * Prompt the user for input with auto completion.
     *
     * @param string $question
     * @param array  $choices
     * @param string $default
     *
     * @return null|string
     */
    public function anticipate(string $question, array $choices, string $default = null): ?string
    {
        return $this->askWithCompletion($question, $choices, $default);
    }

    /**
     * Prompt the user for input with auto completion.
     *
     * @param string      $question
     * @param array       $choices
     * @param null|string $default
     *
     * @return null|string
     */
    public function askWithCompletion(string $question, array $choices, ?string $default = null): ?string
    {
        $question = new Question($question, $default);

        $question->setAutocompleterValues($choices);

        return $this->getOutput()->askQuestion($question);
    }

    /**
     * Prompt the user for input but hide the answer from the console.
     *
     * @param string $question
     * @param bool   $fallback
     *
     * @return string
     */
    public function secret(string $question, bool $fallback = true): string
    {
        $question = new Question($question);

        $question->setHidden(true)->setHiddenFallback($fallback);

        return $this->getOutput()->askQuestion($question);
    }

    /**
     * Give the user a single choice from an array of answers.
     *
     * @param string      $question
     * @param array       $choices
     * @param null|string $default
     * @param mixed       $attempts
     * @param bool        $multiple
     *
     * @return null|string
     */
    public function choice(
        string $question,
        array $choices,
        ?string $default = null,
        $attempts        = null,
        bool $multiple   = false
    ): ?string {
        $question = new ChoiceQuestion($question, $choices, $default);

        $question->setMaxAttempts($attempts)->setMultiselect($multiple);

        return $this->getOutput()->askQuestion($question);
    }

    /**
     * Format input to textual table.
     *
     * @param array                                               $headers
     * @param array|\Viserio\Component\Contract\Support\Arrayable $rows
     * @param string                                              $style
     * @param array                                               $columnStyles
     *
     * @return void
     */
    public function table(array $headers, $rows, string $style = 'default', array $columnStyles = []): void
    {
        $table = new Table($this->output);

        if ($rows instanceof Arrayable) {
            $rows = $rows->toArray();
        }

        $table->setHeaders($headers)->setRows($rows)->setStyle($style);

        foreach ($columnStyles as $columnIndex => $columnStyle) {
            $table->setColumnStyle($columnIndex, $columnStyle);
        }

        $table->render();
    }

    /**
     * Write a string as standard output.
     *
     * @param string          $string
     * @param null|string     $style          The output style of the string
     * @param null|int|string $verbosityLevel
     *
     * @return void
     */
    public function line(string $string, ?string $style = null, $verbosityLevel = null): void
    {
        $styledString = $style ? "<${style}>${string}</${style}>" : $string;
        $this->getOutput()->writeln($styledString, $this->getVerbosity($verbosityLevel));
    }

    /**
     * Write a string as information output.
     *
     * @param string          $string
     * @param null|int|string $verbosityLevel
     *
     * @return void
     */
    public function info(string $string, $verbosityLevel = null): void
    {
        $this->line($string, 'info', $verbosityLevel);
    }

    /**
     * Write a string as comment output.
     *
     * @param string          $string
     * @param null|int|string $verbosityLevel
     *
     * @return void
     */
    public function comment(string $string, $verbosityLevel = null): void
    {
        $this->line($string, 'comment', $verbosityLevel);
    }

    /**
     * Write a string as question output.
     *
     * @param string          $string
     * @param null|int|string $verbosityLevel
     *
     * @return void
     */
    public function question(string $string, $verbosityLevel = null): void
    {
        $this->line($string, 'question', $verbosityLevel);
    }

    /**
     * Write a string as error output.
     *
     * @param string          $string
     * @param null|int|string $verbosityLevel
     *
     * @return void
     */
    public function error(string $string, $verbosityLevel = null): void
    {
        $this->line($string, 'error', $verbosityLevel);
    }

    /**
     * Write a string as warning output.
     *
     * @param string          $string
     * @param null|int|string $verbosityLevel
     *
     * @return void
     */
    public function warn(string $string, $verbosityLevel = null): void
    {
        if (! $this->getOutput()->getFormatter()->hasStyle('warning')) {
            $style = new OutputFormatterStyle('yellow');
            $this->getOutput()->getFormatter()->setStyle('warning', $style);
        }

        $this->line($string, 'warning', $verbosityLevel);
    }

    /**
     * Write a string in an alert box.
     *
     * @param string $string
     *
     * @return void
     */
    public function alert(string $string): void
    {
        $length = \mb_strlen(\strip_tags($string)) + 12;

        $this->comment(\str_repeat('*', $length));
        $this->comment('*     ' . $string . '     *');
        $this->comment(\str_repeat('*', $length));

        $this->getOutput()->newLine();
    }

    /**
     * Write a string as task output.
     *
     * @param string   $string
     * @param callable $callable
     *
     * @return void
     */
    public function task($string, callable $callable): void
    {
        $result = $callable() ? '<info>✔</info>' : '<error>fail</error>';

        $this->line($string . ':' . $result);
    }

    /**
     * Get the container instance.
     *
     * @throws \Viserio\Component\Contract\Console\Exception\LogicException
     *
     * @return \Psr\Container\ContainerInterface
     */
    protected function getContainer(): ContainerInterface
    {
        if (! $this->container) {
            throw new LogicException('Container is not set up.');
        }

        return $this->container;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): ?int
    {
        if ($this->invoker === null) {
            throw new LogicException('Your forgot to call the setInvoker function.');
        }

        /** @var callable $callback */
        $callback = [$this, 'handle'];

        return $this->invoker->call($callback);
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments(): array
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions(): array
    {
        return [];
    }

    /**
     * Configure the console command using a fluent definition.
     *
     * @throws \Symfony\Component\Console\Exception\LogicException
     * @throws \Viserio\Component\Contract\Console\Exception\InvalidCommandExpression
     *
     * @return void
     */
    private function configureUsingFluentDefinition(): void
    {
        $data = ExpressionParser::parse($this->signature);

        parent::__construct($data['name']);

        foreach ($data['arguments'] as $argument) {
            $this->getDefinition()->addArgument($argument);
        }

        foreach ($data['options'] as $option) {
            $this->getDefinition()->addOption($option);
        }
    }

    /**
     * Specify the arguments and options on the command.
     *
     * @return void
     */
    private function specifyParameters(): void
    {
        // We will loop through all of the arguments and options for the command and
        // set them all on the base command instance. This specifies what can get
        // passed into these commands as "parameters" to control the execution.
        foreach ($this->getArguments() as $arguments) {
            $this->addArgument(...$arguments);
        }

        foreach ($this->getOptions() as $options) {
            $this->addOption(...$options);
        }
    }
}


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

use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use Swift_Mime_SimpleMessage;

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

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

    /**
     * Transmission options.
     *
     * @var array
     */
    protected $options = [];

    /**
     * Spark api endpoint.
     *
     * @see https://developers.sparkpost.com/api/
     *
     * @var string
     */
    protected $endpoint;

    /**
     * Create a new SparkPost transport instance.
     *
     * @param \GuzzleHttp\Client $client
     * @param string             $key
     * @param array              $options
     * @param null|string        $endpoint
     */
    public function __construct(Client $client, string $key, array $options = [], ?string $endpoint = null)
    {
        $this->key      = $key;
        $this->client   = $client;
        $this->options  = $options;
        $this->endpoint = $endpoint ?? 'https://api.sparkpost.com/api/v1/transmissions';
    }

    /**
     * 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;
    }

    /**
     * Get the transmission options being used by the transport.
     *
     * @return array
     */
    public function getOptions(): array
    {
        return $this->options;
    }

    /**
     * Set the transmission options being used by the transport.
     *
     * @param array $options
     *
     * @return $this
     */
    public function setOptions(array $options): self
    {
        $this->options = $options;

        return $this;
    }

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

        $recipients = $this->getRecipients($message);

        $message->setBcc([]);

        $options = [
            'headers' => [
                'Authorization' => $this->key,
            ],
            'json' => [
                'recipients' => $recipients,
                'content'    => [
                    'email_rfc822' => $message->toString(),
                ],
            ],
        ];

        if (\count($this->options) !== 0) {
            $options['json']['options'] = $this->options;
        }

        $response = $this->client->post($this->endpoint, $options);

        $message->getHeaders()->addTextHeader('X-SparkPost-Transmission-ID', $this->getTransmissionId($response));

        $this->sendPerformed($message);

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

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

    /**
     * Get the transmission ID from the response.
     *
     * @param \Psr\Http\Message\ResponseInterface $response
     *
     * @return null|string
     */
    protected function getTransmissionId(ResponseInterface $response): ?string
    {
        $object = \json_decode($response->getBody()->getContents());

        foreach (['results', 'id'] as $segment) {
            if (! \is_object($object) || ! isset($object->{$segment})) {
                return null;
            }

            $object = $object->{$segment};
        }

        return $object;
    }

    /**
     * Get all the addresses this message should be sent to.
     *
     * Note that SparkPost still respects CC, BCC headers in raw message itself.
     *
     * @param \Swift_Mime_SimpleMessage $message
     *
     * @return array
     */
    protected function getRecipients(Swift_Mime_SimpleMessage $message): array
    {
        $recipients = [];

        if ($message->getTo() !== null) {
            foreach ($message->getTo() as $email => $name) {
                $recipients[] = ['address' => \compact('name', 'email')];
            }
        }

        if ($message->getCc() !== null) {
            foreach ($message->getCc() as $email => $name) {
                $recipients[] = ['address' => \compact('name', 'email')];
            }
        }

        if ($message->getBcc() !== null) {
            foreach ($message->getBcc() as $email => $name) {
                $recipients[] = ['address' => \compact('name', 'email')];
            }
        }

        return $recipients;
    }
}


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

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Viserio\Component\Contract\Exception\Displayer as DisplayerContract;
use Viserio\Component\Contract\HttpFactory\Traits\ResponseFactoryAwareTrait;
use Viserio\Component\Contract\OptionsResolver\ProvidesDefaultOptions as ProvidesDefaultOptionsContract;
use Viserio\Component\Contract\OptionsResolver\RequiresComponentConfig as RequiresComponentConfigContract;
use Viserio\Component\Exception\ExceptionInfo;
use Viserio\Component\OptionsResolver\Traits\OptionsResolverTrait;

class HtmlDisplayer implements DisplayerContract, RequiresComponentConfigContract, ProvidesDefaultOptionsContract
{
    use OptionsResolverTrait;
    use ResponseFactoryAwareTrait;

    /**
     * The html template path.
     *
     * @var string
     */
    protected $path;

    /**
     * Resolved options.
     *
     * @var array
     */
    protected $resolvedOptions = [];

    /**
     * Create a new html displayer instance.
     *
     * @param \Psr\Http\Message\ResponseFactoryInterface $responseFactory
     * @param array|\ArrayAccess                         $config
     */
    public function __construct(ResponseFactoryInterface $responseFactory, $config = [])
    {
        $this->responseFactory = $responseFactory;
        $this->resolvedOptions = self::resolveOptions($config);
    }

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

    /**
     * {@inheritdoc}
     */
    public static function getDefaultOptions(): array
    {
        return [
            'template_path' => \dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'Resource' . \DIRECTORY_SEPARATOR . 'error.html',
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function display(Throwable $exception, string $id, int $code, array $headers): ResponseInterface
    {
        $response = $this->responseFactory->createResponse($code);

        foreach (\array_merge($headers, ['Content-Type' => $this->getContentType()]) as $header => $value) {
            $response = $response->withAddedHeader($header, $value);
        }

        $body = $response->getBody();
        $body->write($this->render(ExceptionInfo::generate($id, $code)));
        $body->rewind();

        return $response->withBody($body);
    }

    /**
     * {@inheritdoc}
     */
    public function getContentType(): string
    {
        return 'text/html';
    }

    /**
     * {@inheritdoc}
     */
    public function canDisplay(Throwable $original, Throwable $transformed, int $code): bool
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isVerbose(): bool
    {
        return false;
    }

    /**
     * Render the page with given info.
     *
     * @param array $info
     *
     * @return string
     */
    protected function render(array $info): string
    {
        $content = \file_get_contents($this->resolvedOptions['template_path']);

        foreach ($info as $key => $val) {
            $content = \str_replace("{{ $${key} }}", $val, $content);
        }

        return $content;
    }
}


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

use Error;
use ErrorException;
use Exception;
use Narrowspark\HttpStatus\Exception\AbstractClientErrorException;
use Narrowspark\HttpStatus\Exception\AbstractServerErrorException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\Debug\Exception\OutOfMemoryException;
use Throwable;
use Viserio\Component\Contract\Exception\Handler as HandlerContract;
use Viserio\Component\Contract\Exception\Transformer as TransformerContract;
use Viserio\Component\Contract\OptionsResolver\ProvidesDefaultOptions as ProvidesDefaultOptionsContract;
use Viserio\Component\Contract\OptionsResolver\RequiresComponentConfig as RequiresComponentConfigContract;
use Viserio\Component\Exception\Traits\DetermineErrorLevelTrait;
use Viserio\Component\Exception\Transformer\ClassNotFoundFatalErrorTransformer;
use Viserio\Component\Exception\Transformer\UndefinedFunctionFatalErrorTransformer;
use Viserio\Component\Exception\Transformer\UndefinedMethodFatalErrorTransformer;
use Viserio\Component\OptionsResolver\Traits\OptionsResolverTrait;

class ErrorHandler implements
    HandlerContract,
    RequiresComponentConfigContract,
    ProvidesDefaultOptionsContract,
    LoggerAwareInterface
{
    use OptionsResolverTrait;
    use LoggerAwareTrait;
    use DetermineErrorLevelTrait;

    /**
     * Exception transformers.
     *
     * @var array
     */
    protected $transformers = [];

    /**
     * A list of the exception types that should not be reported.
     *
     * @var array
     */
    protected $dontReport = [];

    /**
     * Resolved options.
     *
     * @var array
     */
    protected $resolvedOptions = [];

    /**
     * Some reserved memory.
     *
     * @var null|string
     */
    private $reservedMemory;

    /**
     * List of int errors to string.
     *
     * @var array
     */
    private static $levels = [
        \E_DEPRECATED        => 'Deprecated',
        \E_USER_DEPRECATED   => 'User Deprecated',
        \E_NOTICE            => 'Notice',
        \E_USER_NOTICE       => 'User Notice',
        \E_STRICT            => 'Runtime Notice',
        \E_WARNING           => 'Warning',
        \E_USER_WARNING      => 'User Warning',
        \E_COMPILE_WARNING   => 'Compile Warning',
        \E_CORE_WARNING      => 'Core Warning',
        \E_USER_ERROR        => 'User Error',
        \E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
        \E_COMPILE_ERROR     => 'Compile Error',
        \E_PARSE             => 'Parse Error',
        \E_ERROR             => 'Error',
        \E_CORE_ERROR        => 'Core Error',
    ];

    /**
     * Transform's php errors to logger level errors.
     *
     * @var array
     */
    private static $loggers = [
        \E_DEPRECATED        => LogLevel::INFO,
        \E_USER_DEPRECATED   => LogLevel::INFO,
        \E_NOTICE            => LogLevel::WARNING,
        \E_USER_NOTICE       => LogLevel::WARNING,
        \E_STRICT            => LogLevel::WARNING,
        \E_WARNING           => LogLevel::WARNING,
        \E_USER_WARNING      => LogLevel::WARNING,
        \E_COMPILE_WARNING   => LogLevel::WARNING,
        \E_CORE_WARNING      => LogLevel::WARNING,
        \E_USER_ERROR        => LogLevel::CRITICAL,
        \E_RECOVERABLE_ERROR => LogLevel::CRITICAL,
        \E_COMPILE_ERROR     => LogLevel::CRITICAL,
        \E_PARSE             => LogLevel::CRITICAL,
        \E_ERROR             => LogLevel::CRITICAL,
        \E_CORE_ERROR        => LogLevel::CRITICAL,
    ];

    /**
     * Create a new error handler instance.
     *
     * @param array|\ArrayAccess            $config
     * @param null|\Psr\Log\LoggerInterface $logger
     */
    public function __construct($config, ?LoggerInterface $logger = null)
    {
        $this->resolvedOptions = self::resolveOptions($config);
        $this->transformers    = \array_merge(
            $this->getErrorTransformer(),
            $this->transformArray($this->resolvedOptions['transformers'])
        );

        $this->dontReport = $this->resolvedOptions['dont_report'];
        $this->logger     = $logger ?? new NullLogger();
    }

    /**
     * Get the transformer exceptions.
     *
     * @return array
     */
    public function getTransformers(): array
    {
        return $this->transformers;
    }

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

    /**
     * {@inheritdoc}
     */
    public static function getDefaultOptions(): array
    {
        return [
            // A list of the exception types that should not be reported.
            'dont_report' => [],
            'levels'      => [
                FatalThrowableError::class          => LogLevel::CRITICAL,
                FatalErrorException::class          => LogLevel::ERROR,
                Throwable::class                    => LogLevel::ERROR,
                Exception::class                    => LogLevel::ERROR,
                AbstractClientErrorException::class => LogLevel::NOTICE,
                AbstractServerErrorException::class => LogLevel::ERROR,
            ],
            // Exception transformers.
            'transformers' => [],
        ];
    }

    /**
     * Determine if the exception shouldn't be reported.
     *
     * @param \Throwable $exception
     *
     * @return $this
     */
    public function addShouldntReport(Throwable $exception): HandlerContract
    {
        $this->dontReport[\get_class($exception)] = $exception;

        return $this;
    }

    /**
     * Report or log an exception.
     *
     * @param \Throwable $exception
     *
     * @return void
     */
    public function report(Throwable $exception): void
    {
        if ($this->shouldntReport($exception)) {
            return;
        }

        $level = $this->getLevel($exception);
        $id    = ExceptionIdentifier::identify($exception);

        if ($exception instanceof FatalErrorException) {
            if ($exception instanceof FatalThrowableError) {
                $message = $exception->getMessage();
            } else {
                $message = 'Fatal ' . $exception->getMessage();
            }
        } elseif ($exception instanceof ErrorException) {
            $message = 'Uncaught ' . $exception->getMessage();
        } else {
            $message = 'Uncaught Exception: ' . $exception->getMessage();
        }

        $this->logger->{$level}(
            $message,
            ['exception' => $exception, 'identification' => ['id' => $id]]
        );
    }

    /**
     * Add the transformed instance.
     *
     * @param \Viserio\Component\Contract\Exception\Transformer $transformer
     *
     * @return $this
     */
    public function addTransformer(TransformerContract $transformer): HandlerContract
    {
        $this->transformers[\get_class($transformer)] = $transformer;

        return $this;
    }

    /**
     * Convert errors into ErrorException objects.
     *
     * This method catches PHP errors and converts them into ErrorException objects;
     * these ErrorException objects are then thrown and caught by Viserio's
     * built-in or custom error handlers.
     *
     * @param int    $type    The numeric type of the Error
     * @param string $message The error message
     * @param string $file    The absolute path to the affected file
     * @param int    $line    The line number of the error in the affected file
     *
     * @throws \Symfony\Component\Debug\Exception\FatalErrorException
     *
     * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself
     *
     * @internal
     */
    public function handleError(int $type, string $message, string $file = '', int $line = 0): bool
    {
        if (\error_reporting() === 0) {
            return false;
        }

        // Level is the current error reporting level to manage silent error.
        // Strong errors are not authorized to be silenced.
        $severity = \error_reporting() | \E_RECOVERABLE_ERROR | \E_USER_ERROR | \E_DEPRECATED | \E_USER_DEPRECATED;

        if ($severity) {
            throw new FatalErrorException($message, 0, $severity, $file, $line);
        }

        return true;
    }

    /**
     * Handle an uncaught exception from the application.
     *
     * Note: Fatal error exceptions must
     * be handled differently since they are not normal exceptions.
     *
     * @param \Throwable $exception
     *
     * @throws \Throwable
     *
     * @return void
     *
     * @internal
     *
     * @see https://secure.php.net/manual/en/function.set-exception-handler.php
     */
    public function handleException(Throwable $exception): void
    {
        $exception = $this->prepareException($exception);

        try {
            $this->report($exception);
        } catch (Throwable $e) {
            // If handler can't report exception just throw it
        }

        throw $this->getTransformed($exception);
    }

    /**
     * Shutdown registered function for handling PHP fatal errors.
     *
     * @throws \Throwable
     *
     * @return void
     *
     * @internal
     */
    public function handleShutdown(): void
    {
        if ($this->reservedMemory === null) {
            return;
        }

        $this->reservedMemory = null;

        // If an error has occurred that has not been displayed, we will create a fatal
        // error exception instance and pass it into the regular exception handling
        // code so it can be displayed back out to the developer for information.
        $error     = \error_get_last();
        $exception = null;

        if ($error !== null && self::isLevelFatal($error['type'])) {
            $trace = $error['backtrace'] ?? null;

            if (\strpos($error['message'], 'Allowed memory') === 0 || \strpos($error['message'], 'Out of memory') === 0) {
                $exception = new OutOfMemoryException(self::$levels[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace);
            } else {
                // Create a new fatal exception instance from an error array.
                $exception = new FatalErrorException(self::$levels[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace);
            }
        }

        if ($exception !== null) {
            $this->handleException($exception);
        }
    }

    /**
     * Register the PHP error handler.
     *
     * @return void
     */
    protected function registerErrorHandler(): void
    {
        \set_error_handler([$this, 'handleError']);
    }

    /**
     * Register the PHP exception handler.
     *
     * @return void
     */
    protected function registerExceptionHandler(): void
    {
        if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
            \ini_set('display_errors', '0');
        } elseif (! \filter_var(\ini_get('log_errors'), \FILTER_VALIDATE_BOOLEAN) || \filter_var(\ini_get('error_log'), \FILTER_VALIDATE_BOOLEAN)) {
            // CLI - display errors only if they're not already logged to STDERR
            \ini_set('display_errors', '1');
        }

        \set_exception_handler([$this, 'handleException']);
    }

    /**
     * Register the PHP shutdown handler.
     *
     * @return void
     */
    protected function registerShutdownHandler(): void
    {
        if ($this->reservedMemory === null) {
            $this->reservedMemory = \str_repeat('x', 10240);
            \register_shutdown_function([$this, 'handleShutdown']);
        }
    }

    /**
     * Prepare exception in a fatal error handler.
     *
     * @param \Error|\Exception|\Throwable $exception
     *
     * @return \Error|\Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface|\Throwable
     */
    protected function prepareException($exception)
    {
        if (! $exception instanceof Exception && ! $exception instanceof Error) {
            $exception = new FatalThrowableError($exception);
        } elseif ($exception instanceof Error) {
            $trace = $exception->getTrace();

            $exception = new FatalErrorException(
                $exception->getMessage(),
                $exception->getCode(),
                \E_ERROR,
                $exception->getFile(),
                $exception->getLine(),
                \count($trace),
                \count($trace) !== 0,
                $trace
            );
        }

        return $exception;
    }

    /**
     * Get the transformed exception.
     *
     * @param \Throwable $exception
     *
     * @return \Throwable
     */
    protected function getTransformed(Throwable $exception): Throwable
    {
        if (! $exception instanceof OutOfMemoryException || \count($this->transformers) === 0) {
            return $exception;
        }

        foreach ($this->transformers as $transformer) {
            /** @var TransformerContract $transformer */
            $exception = $transformer->transform($exception);
        }

        return $exception;
    }

    /**
     * Transform's the given array to a key (class name) value (object/class name) array.
     *
     * @param array $data
     *
     * @return array
     */
    protected function transformArray(array $data): array
    {
        $array = [];

        foreach ($data as $key => $value) {
            if (\is_numeric($key)) {
                $key = \is_string($value) ? $value : \get_class($value);
            }

            $array[$key] = $value;
        }

        return $array;
    }

    /**
     * The default error transformers.
     *
     * @return array
     */
    protected function getErrorTransformer(): array
    {
        return [
            ClassNotFoundFatalErrorTransformer::class     => new ClassNotFoundFatalErrorTransformer(),
            UndefinedFunctionFatalErrorTransformer::class => new UndefinedFunctionFatalErrorTransformer(),
            UndefinedMethodFatalErrorTransformer::class   => new UndefinedMethodFatalErrorTransformer(),
        ];
    }

    /**
     * Get the exception level.
     *
     * @param \Throwable $exception
     *
     * @return string
     */
    private function getLevel(Throwable $exception): string
    {
        foreach ($this->resolvedOptions['levels'] as $class => $level) {
            if ($exception instanceof $class) {
                return $level;
            }
        }

        if ($exception instanceof FatalErrorException) {
            return self::$loggers[$exception->getSeverity()];
        }

        return LogLevel::ERROR;
    }

    /**
     * Determine if the exception is in the "do not report" list.
     *
     * @param \Throwable $exception
     *
     * @return bool
     */
    private function shouldntReport(Throwable $exception): bool
    {
        foreach ($this->dontReport as $type) {
            if ($exception instanceof $type) {
                return true;
            }
        }

        return false;
    }
}


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

use Viserio\Component\Contract\Parser\Dumper as DumperContract;
use Viserio\Component\Contract\Parser\Exception\NotSupportedException;
use Viserio\Component\Parser\Dumper\IniDumper;
use Viserio\Component\Parser\Dumper\JsonDumper;
use Viserio\Component\Parser\Dumper\PhpArrayDumper;
use Viserio\Component\Parser\Dumper\QtDumper;
use Viserio\Component\Parser\Dumper\QueryStrDumper;
use Viserio\Component\Parser\Dumper\SerializeDumper;
use Viserio\Component\Parser\Dumper\XliffDumper;
use Viserio\Component\Parser\Dumper\XmlDumper;
use Viserio\Component\Parser\Dumper\YamlDumper;

class Dumper
{
    /**
     * Supported mime type formats.
     *
     * @var array
     */
    private static $supportedMimeTypes = [
        // XML
        'application/xml' => 'xml',
        'text/xml'        => 'xml',
        // Xliff
        'application/x-xliff+xml' => 'xlf',
        // JSON
        'application/json'         => 'json',
        'application/x-javascript' => 'json',
        'text/javascript'          => 'json',
        'text/x-javascript'        => 'json',
        'text/x-json'              => 'json',
        // YAML
        'text/yaml'          => 'yaml',
        'text/x-yaml'        => 'yaml',
        'application/yaml'   => 'yaml',
        'application/x-yaml' => 'yaml',
        // MISC
        'application/vnd.php.serialized'    => 'serialize',
        'application/x-www-form-urlencoded' => 'querystr',
    ];

    /**
     * All supported dumper.
     *
     * @var array
     */
    private static $supportedDumper = [
        'ini'       => IniDumper::class,
        'json'      => JsonDumper::class,
        'php'       => PhpArrayDumper::class,
        'querystr'  => QueryStrDumper::class,
        'serialize' => SerializeDumper::class,
        'ts'        => QtDumper::class,
        'xml'       => XmlDumper::class,
        'xlf'       => XliffDumper::class,
        'yaml'      => YamlDumper::class,
    ];

    /**
     * Add a new mime type with extension.
     *
     * @param string $mimeType
     * @param string $extension
     *
     * @return void
     */
    public function addMimeType(string $mimeType, string $extension): void
    {
        self::$supportedMimeTypes[$mimeType] = $extension;
    }

    /**
     * Add a new dumper.
     *
     * @param \Viserio\Component\Contract\Parser\Dumper $dumper
     * @param string                                    $extension
     *
     * @return void
     */
    public function addDumper(DumperContract $dumper, string $extension): void
    {
        self::$supportedDumper[$extension] = $dumper;
    }

    /**
     * Dump data in your choosing format.
     *
     * @param array  $data
     * @param string $format
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\DumpException
     * @throws \Viserio\Component\Contract\Parser\Exception\NotSupportedException
     *
     * @return string
     */
    public function dump(array $data, string $format): string
    {
        $dumper = $this->getDumper($format);

        return $dumper->dump($data);
    }

    /**
     * Get supported dumper on extension or mime type.
     *
     * @param string $type
     *
     * @throws \Viserio\Component\Contract\Parser\Exception\NotSupportedException
     *
     * @return \Viserio\Component\Contract\Parser\Dumper
     */
    public function getDumper(string $type): DumperContract
    {
        if (isset(self::$supportedDumper[$type])) {
            return new self::$supportedDumper[$type]();
        }

        if (isset(self::$supportedMimeTypes[$type])) {
            $class = self::$supportedDumper[self::$supportedMimeTypes[$type]];

            if (\is_object($class) && $class instanceof DumperContract) {
                return $class;
            }

            return new $class();
        }

        throw new NotSupportedException(\sprintf('Given extension or mime type [%s] is not supported.', $type));
    }
}