+ * @author Nicolas Grekas
+ */
+class CompiledUrlGeneratorDumper extends GeneratorDumper
+{
+ public function getCompiledRoutes(): array
+ {
+ $compiledRoutes = [];
+ foreach ($this->getRoutes()->all() as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ $compiledRoutes[$name] = [
+ $compiledRoute->getVariables(),
+ $route->getDefaults(),
+ $route->getRequirements(),
+ $compiledRoute->getTokens(),
+ $compiledRoute->getHostTokens(),
+ $route->getSchemes(),
+ [],
+ ];
+ }
+
+ return $compiledRoutes;
+ }
+
+ public function getCompiledAliases(): array
+ {
+ $routes = $this->getRoutes();
+ $compiledAliases = [];
+ foreach ($routes->getAliases() as $name => $alias) {
+ $deprecations = $alias->isDeprecated() ? [$alias->getDeprecation($name)] : [];
+ $currentId = $alias->getId();
+ $visited = [];
+ while (null !== $alias = $routes->getAlias($currentId) ?? null) {
+ if (false !== $searchKey = array_search($currentId, $visited)) {
+ $visited[] = $currentId;
+
+ throw new RouteCircularReferenceException($currentId, \array_slice($visited, $searchKey));
+ }
+
+ if ($alias->isDeprecated()) {
+ $deprecations[] = $deprecation = $alias->getDeprecation($currentId);
+ trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
+ }
+
+ $visited[] = $currentId;
+ $currentId = $alias->getId();
+ }
+
+ if (null === $target = $routes->get($currentId)) {
+ throw new RouteNotFoundException(sprintf('Target route "%s" for alias "%s" does not exist.', $currentId, $name));
+ }
+
+ $compiledTarget = $target->compile();
+
+ $compiledAliases[$name] = [
+ $compiledTarget->getVariables(),
+ $target->getDefaults(),
+ $target->getRequirements(),
+ $compiledTarget->getTokens(),
+ $compiledTarget->getHostTokens(),
+ $target->getSchemes(),
+ $deprecations,
+ ];
+ }
+
+ return $compiledAliases;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dump(array $options = [])
+ {
+ return <<generateDeclaredRoutes()}
+];
+
+EOF;
+ }
+
+ /**
+ * Generates PHP code representing an array of defined routes
+ * together with the routes properties (e.g. requirements).
+ */
+ private function generateDeclaredRoutes(): string
+ {
+ $routes = '';
+ foreach ($this->getCompiledRoutes() as $name => $properties) {
+ $routes .= sprintf("\n '%s' => %s,", $name, CompiledUrlMatcherDumper::export($properties));
+ }
+
+ foreach ($this->getCompiledAliases() as $alias => $properties) {
+ $routes .= sprintf("\n '%s' => %s,", $alias, CompiledUrlMatcherDumper::export($properties));
+ }
+
+ return $routes;
+ }
+}
diff --git a/src/vendor/symfony/routing/Generator/Dumper/GeneratorDumper.php b/src/vendor/symfony/routing/Generator/Dumper/GeneratorDumper.php
new file mode 100644
index 000000000..659c5ba1c
--- /dev/null
+++ b/src/vendor/symfony/routing/Generator/Dumper/GeneratorDumper.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GeneratorDumper is the base class for all built-in generator dumpers.
+ *
+ * @author Fabien Potencier
+ */
+abstract class GeneratorDumper implements GeneratorDumperInterface
+{
+ private $routes;
+
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+}
diff --git a/src/vendor/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php b/src/vendor/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php
new file mode 100644
index 000000000..d4a248a5b
--- /dev/null
+++ b/src/vendor/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GeneratorDumperInterface is the interface that all generator dumper classes must implement.
+ *
+ * @author Fabien Potencier
+ */
+interface GeneratorDumperInterface
+{
+ /**
+ * Dumps a set of routes to a string representation of executable code
+ * that can then be used to generate a URL of such a route.
+ *
+ * @return string
+ */
+ public function dump(array $options = []);
+
+ /**
+ * Gets the routes to dump.
+ *
+ * @return RouteCollection
+ */
+ public function getRoutes();
+}
diff --git a/src/vendor/symfony/routing/Generator/UrlGenerator.php b/src/vendor/symfony/routing/Generator/UrlGenerator.php
new file mode 100644
index 000000000..4419e9efd
--- /dev/null
+++ b/src/vendor/symfony/routing/Generator/UrlGenerator.php
@@ -0,0 +1,378 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * UrlGenerator can generate a URL or a path for any route in the RouteCollection
+ * based on the passed parameters.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface
+{
+ private const QUERY_FRAGMENT_DECODED = [
+ // RFC 3986 explicitly allows those in the query/fragment to reference other URIs unencoded
+ '%2F' => '/',
+ '%3F' => '?',
+ // reserved chars that have no special meaning for HTTP URIs in a query or fragment
+ // this excludes esp. "&", "=" and also "+" because PHP would treat it as a space (form-encoded)
+ '%40' => '@',
+ '%3A' => ':',
+ '%21' => '!',
+ '%3B' => ';',
+ '%2C' => ',',
+ '%2A' => '*',
+ ];
+
+ protected $routes;
+ protected $context;
+
+ /**
+ * @var bool|null
+ */
+ protected $strictRequirements = true;
+
+ protected $logger;
+
+ private $defaultLocale;
+
+ /**
+ * 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).
+ */
+ protected $decodedChars = [
+ // 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' => '/',
+ '%252F' => '%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' => '|',
+ ];
+
+ public function __construct(RouteCollection $routes, RequestContext $context, ?LoggerInterface $logger = null, ?string $defaultLocale = null)
+ {
+ $this->routes = $routes;
+ $this->context = $context;
+ $this->logger = $logger;
+ $this->defaultLocale = $defaultLocale;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setStrictRequirements(?bool $enabled)
+ {
+ $this->strictRequirements = $enabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isStrictRequirements()
+ {
+ return $this->strictRequirements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH)
+ {
+ $route = null;
+ $locale = $parameters['_locale']
+ ?? $this->context->getParameter('_locale')
+ ?: $this->defaultLocale;
+
+ if (null !== $locale) {
+ do {
+ if (null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
+ break;
+ }
+ } while (false !== $locale = strstr($locale, '_', true));
+ }
+
+ if (null === $route = $route ?? $this->routes->get($name)) {
+ throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
+ }
+
+ // the Route has a cache of its own and is not recompiled as long as it does not get modified
+ $compiledRoute = $route->compile();
+
+ $defaults = $route->getDefaults();
+ $variables = $compiledRoute->getVariables();
+
+ if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
+ if (!\in_array('_locale', $variables, true)) {
+ unset($parameters['_locale']);
+ } elseif (!isset($parameters['_locale'])) {
+ $parameters['_locale'] = $defaults['_locale'];
+ }
+ }
+
+ return $this->doGenerate($variables, $defaults, $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes());
+ }
+
+ /**
+ * @return string
+ *
+ * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
+ * @throws InvalidParameterException When a parameter value for a placeholder is not correct because
+ * it does not match the requirement
+ */
+ protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = [])
+ {
+ $variables = array_flip($variables);
+ $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
+
+ // all params must be given
+ if ($diff = array_diff_key($variables, $mergedParams)) {
+ throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', array_keys($diff)), $name));
+ }
+
+ $url = '';
+ $optional = true;
+ $message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.';
+ foreach ($tokens as $token) {
+ if ('variable' === $token[0]) {
+ $varName = $token[3];
+ // variable is not important by default
+ $important = $token[5] ?? false;
+
+ if (!$optional || $important || !\array_key_exists($varName, $defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) {
+ // check requirement (while ignoring look-around patterns)
+ if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|strictRequirements) {
+ throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName]]));
+ }
+
+ if ($this->logger) {
+ $this->logger->error($message, ['parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]]);
+ }
+
+ return '';
+ }
+
+ $url = $token[1].$mergedParams[$varName].$url;
+ $optional = false;
+ }
+ } else {
+ // static text
+ $url = $token[1].$url;
+ $optional = false;
+ }
+ }
+
+ if ('' === $url) {
+ $url = '/';
+ }
+
+ // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request)
+ $url = strtr(rawurlencode($url), $this->decodedChars);
+
+ // 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
+ $url = strtr($url, ['/../' => '/%2E%2E/', '/./' => '/%2E/']);
+ if (str_ends_with($url, '/..')) {
+ $url = substr($url, 0, -2).'%2E%2E';
+ } elseif (str_ends_with($url, '/.')) {
+ $url = substr($url, 0, -1).'%2E';
+ }
+
+ $schemeAuthority = '';
+ $host = $this->context->getHost();
+ $scheme = $this->context->getScheme();
+
+ if ($requiredSchemes) {
+ if (!\in_array($scheme, $requiredSchemes, true)) {
+ $referenceType = self::ABSOLUTE_URL;
+ $scheme = current($requiredSchemes);
+ }
+ }
+
+ if ($hostTokens) {
+ $routeHost = '';
+ foreach ($hostTokens as $token) {
+ if ('variable' === $token[0]) {
+ // check requirement (while ignoring look-around patterns)
+ if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|strictRequirements) {
+ throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]]));
+ }
+
+ if ($this->logger) {
+ $this->logger->error($message, ['parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]]);
+ }
+
+ return '';
+ }
+
+ $routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
+ } else {
+ $routeHost = $token[1].$routeHost;
+ }
+ }
+
+ if ($routeHost !== $host) {
+ $host = $routeHost;
+ if (self::ABSOLUTE_URL !== $referenceType) {
+ $referenceType = self::NETWORK_PATH;
+ }
+ }
+ }
+
+ if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
+ if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
+ $port = '';
+ if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
+ $port = ':'.$this->context->getHttpPort();
+ } elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
+ $port = ':'.$this->context->getHttpsPort();
+ }
+
+ $schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://";
+ $schemeAuthority .= $host.$port;
+ }
+ }
+
+ if (self::RELATIVE_PATH === $referenceType) {
+ $url = self::getRelativePath($this->context->getPathInfo(), $url);
+ } else {
+ $url = $schemeAuthority.$this->context->getBaseUrl().$url;
+ }
+
+ // add a query string if needed
+ $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, function ($a, $b) {
+ return $a == $b ? 0 : 1;
+ });
+
+ array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) {
+ if (\is_object($v)) {
+ if ($vars = get_object_vars($v)) {
+ array_walk_recursive($vars, $caster);
+ $v = $vars;
+ } elseif (method_exists($v, '__toString')) {
+ $v = (string) $v;
+ }
+ }
+ });
+
+ // extract fragment
+ $fragment = $defaults['_fragment'] ?? '';
+
+ if (isset($extra['_fragment'])) {
+ $fragment = $extra['_fragment'];
+ unset($extra['_fragment']);
+ }
+
+ if ($extra && $query = http_build_query($extra, '', '&', \PHP_QUERY_RFC3986)) {
+ $url .= '?'.strtr($query, self::QUERY_FRAGMENT_DECODED);
+ }
+
+ if ('' !== $fragment) {
+ $url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED);
+ }
+
+ return $url;
+ }
+
+ /**
+ * 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
+ */
+ public static function getRelativePath(string $basePath, string $targetPath)
+ {
+ 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;
+ }
+}
diff --git a/src/vendor/symfony/routing/Generator/UrlGeneratorInterface.php b/src/vendor/symfony/routing/Generator/UrlGeneratorInterface.php
new file mode 100644
index 000000000..c6d5005f9
--- /dev/null
+++ b/src/vendor/symfony/routing/Generator/UrlGeneratorInterface.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator;
+
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\RequestContextAwareInterface;
+
+/**
+ * UrlGeneratorInterface is the interface that all URL generator classes must implement.
+ *
+ * The constants in this interface define the different types of resource references that
+ * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986
+ * We are using the term "URL" instead of "URI" as this is more common in web applications
+ * and we do not need to distinguish them as the difference is mostly semantical and
+ * less technical. Generating URIs, i.e. representation-independent resource identifiers,
+ * is also possible.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+interface UrlGeneratorInterface extends RequestContextAwareInterface
+{
+ /**
+ * Generates an absolute URL, e.g. "http://example.com/dir/file".
+ */
+ public const ABSOLUTE_URL = 0;
+
+ /**
+ * Generates an absolute path, e.g. "/dir/file".
+ */
+ public const ABSOLUTE_PATH = 1;
+
+ /**
+ * Generates a relative path based on the current request path, e.g. "../parent-file".
+ *
+ * @see UrlGenerator::getRelativePath()
+ */
+ public const RELATIVE_PATH = 2;
+
+ /**
+ * Generates a network path, e.g. "//example.com/dir/file".
+ * Such reference reuses the current scheme but specifies the host.
+ */
+ public const NETWORK_PATH = 3;
+
+ /**
+ * Generates a URL or path for a specific route based on the given parameters.
+ *
+ * Parameters that reference placeholders in the route pattern will substitute them in the
+ * path or host. Extra params are added as query string to the URL.
+ *
+ * When the passed reference type cannot be generated for the route because it requires a different
+ * host or scheme than the current one, the method will return a more comprehensive reference
+ * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH
+ * but the route requires the https scheme whereas the current scheme is http, it will instead return an
+ * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches
+ * the route in any case.
+ *
+ * If there is no route with the given name, the generator must throw the RouteNotFoundException.
+ *
+ * The special parameter _fragment will be used as the document fragment suffixed to the final URL.
+ *
+ * @return string
+ *
+ * @throws RouteNotFoundException If the named route doesn't exist
+ * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
+ * @throws InvalidParameterException When a parameter value for a placeholder is not correct because
+ * it does not match the requirement
+ */
+ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH);
+}
diff --git a/src/vendor/symfony/routing/LICENSE b/src/vendor/symfony/routing/LICENSE
new file mode 100644
index 000000000..0138f8f07
--- /dev/null
+++ b/src/vendor/symfony/routing/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-present Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/vendor/symfony/routing/Loader/AnnotationClassLoader.php b/src/vendor/symfony/routing/Loader/AnnotationClassLoader.php
new file mode 100644
index 000000000..c0bcb4713
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/AnnotationClassLoader.php
@@ -0,0 +1,394 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Doctrine\Common\Annotations\Reader;
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\Loader\LoaderResolverInterface;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\Annotation\Route as RouteAnnotation;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * AnnotationClassLoader loads routing information from a PHP class and its methods.
+ *
+ * You need to define an implementation for the configureRoute() method. Most of the
+ * time, this method should define some PHP callable to be called for the route
+ * (a controller in MVC speak).
+ *
+ * The @Route annotation can be set on the class (for global parameters),
+ * and on each method.
+ *
+ * The @Route annotation main value is the route path. The annotation also
+ * recognizes several parameters: requirements, options, defaults, schemes,
+ * methods, host, and name. The name parameter is mandatory.
+ * Here is an example of how you should be able to use it:
+ * /**
+ * * @Route("/Blog")
+ * * /
+ * class Blog
+ * {
+ * /**
+ * * @Route("/", name="blog_index")
+ * * /
+ * public function index()
+ * {
+ * }
+ * /**
+ * * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
+ * * /
+ * public function show()
+ * {
+ * }
+ * }
+ *
+ * On PHP 8, the annotation class can be used as an attribute as well:
+ * #[Route('/Blog')]
+ * class Blog
+ * {
+ * #[Route('/', name: 'blog_index')]
+ * public function index()
+ * {
+ * }
+ * #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])]
+ * public function show()
+ * {
+ * }
+ * }
+
+ *
+ * @author Fabien Potencier
+ * @author Alexander M. Turek
+ */
+abstract class AnnotationClassLoader implements LoaderInterface
+{
+ protected $reader;
+ protected $env;
+
+ /**
+ * @var string
+ */
+ protected $routeAnnotationClass = RouteAnnotation::class;
+
+ /**
+ * @var int
+ */
+ protected $defaultRouteIndex = 0;
+
+ public function __construct(?Reader $reader = null, ?string $env = null)
+ {
+ $this->reader = $reader;
+ $this->env = $env;
+ }
+
+ /**
+ * Sets the annotation class to read route properties from.
+ */
+ public function setRouteAnnotationClass(string $class)
+ {
+ $this->routeAnnotationClass = $class;
+ }
+
+ /**
+ * Loads from annotations from a class.
+ *
+ * @param string $class A class name
+ *
+ * @return RouteCollection
+ *
+ * @throws \InvalidArgumentException When route can't be parsed
+ */
+ public function load($class, ?string $type = null)
+ {
+ if (!class_exists($class)) {
+ throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
+ }
+
+ $class = new \ReflectionClass($class);
+ if ($class->isAbstract()) {
+ throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName()));
+ }
+
+ $globals = $this->getGlobals($class);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($class->getFileName()));
+
+ if ($globals['env'] && $this->env !== $globals['env']) {
+ return $collection;
+ }
+
+ foreach ($class->getMethods() as $method) {
+ $this->defaultRouteIndex = 0;
+ foreach ($this->getAnnotations($method) as $annot) {
+ $this->addRoute($collection, $annot, $globals, $class, $method);
+ }
+ }
+
+ if (0 === $collection->count() && $class->hasMethod('__invoke')) {
+ $globals = $this->resetGlobals();
+ foreach ($this->getAnnotations($class) as $annot) {
+ $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * @param RouteAnnotation $annot or an object that exposes a similar interface
+ */
+ protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
+ {
+ if ($annot->getEnv() && $annot->getEnv() !== $this->env) {
+ return;
+ }
+
+ $name = $annot->getName();
+ if (null === $name) {
+ $name = $this->getDefaultRouteName($class, $method);
+ }
+ $name = $globals['name'].$name;
+
+ $requirements = $annot->getRequirements();
+
+ foreach ($requirements as $placeholder => $requirement) {
+ if (\is_int($placeholder)) {
+ throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?', $placeholder, $requirement, $name, $class->getName(), $method->getName()));
+ }
+ }
+
+ $defaults = array_replace($globals['defaults'], $annot->getDefaults());
+ $requirements = array_replace($globals['requirements'], $requirements);
+ $options = array_replace($globals['options'], $annot->getOptions());
+ $schemes = array_merge($globals['schemes'], $annot->getSchemes());
+ $methods = array_merge($globals['methods'], $annot->getMethods());
+
+ $host = $annot->getHost();
+ if (null === $host) {
+ $host = $globals['host'];
+ }
+
+ $condition = $annot->getCondition() ?? $globals['condition'];
+ $priority = $annot->getPriority() ?? $globals['priority'];
+
+ $path = $annot->getLocalizedPaths() ?: $annot->getPath();
+ $prefix = $globals['localized_paths'] ?: $globals['path'];
+ $paths = [];
+
+ if (\is_array($path)) {
+ if (!\is_array($prefix)) {
+ foreach ($path as $locale => $localePath) {
+ $paths[$locale] = $prefix.$localePath;
+ }
+ } elseif ($missing = array_diff_key($prefix, $path)) {
+ throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing))));
+ } else {
+ foreach ($path as $locale => $localePath) {
+ if (!isset($prefix[$locale])) {
+ throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name));
+ }
+
+ $paths[$locale] = $prefix[$locale].$localePath;
+ }
+ }
+ } elseif (\is_array($prefix)) {
+ foreach ($prefix as $locale => $localePrefix) {
+ $paths[$locale] = $localePrefix.$path;
+ }
+ } else {
+ $paths[] = $prefix.$path;
+ }
+
+ foreach ($method->getParameters() as $param) {
+ if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) {
+ continue;
+ }
+ foreach ($paths as $locale => $path) {
+ if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/', preg_quote($param->name)), $path)) {
+ $defaults[$param->name] = $param->getDefaultValue();
+ break;
+ }
+ }
+ }
+
+ foreach ($paths as $locale => $path) {
+ $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
+ $this->configureRoute($route, $class, $method, $annot);
+ if (0 !== $locale) {
+ $route->setDefault('_locale', $locale);
+ $route->setRequirement('_locale', preg_quote($locale));
+ $route->setDefault('_canonical_route', $name);
+ $collection->add($name.'.'.$locale, $route, $priority);
+ } else {
+ $collection->add($name, $route, $priority);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResolver(LoaderResolverInterface $resolver)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResolver()
+ {
+ }
+
+ /**
+ * Gets the default route name for a class method.
+ *
+ * @return string
+ */
+ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
+ {
+ $name = str_replace('\\', '_', $class->name).'_'.$method->name;
+ $name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name);
+ if ($this->defaultRouteIndex > 0) {
+ $name .= '_'.$this->defaultRouteIndex;
+ }
+ ++$this->defaultRouteIndex;
+
+ return $name;
+ }
+
+ protected function getGlobals(\ReflectionClass $class)
+ {
+ $globals = $this->resetGlobals();
+
+ $annot = null;
+ if (\PHP_VERSION_ID >= 80000 && ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)) {
+ $annot = $attribute->newInstance();
+ }
+ if (!$annot && $this->reader) {
+ $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
+ }
+
+ if ($annot) {
+ if (null !== $annot->getName()) {
+ $globals['name'] = $annot->getName();
+ }
+
+ if (null !== $annot->getPath()) {
+ $globals['path'] = $annot->getPath();
+ }
+
+ $globals['localized_paths'] = $annot->getLocalizedPaths();
+
+ if (null !== $annot->getRequirements()) {
+ $globals['requirements'] = $annot->getRequirements();
+ }
+
+ if (null !== $annot->getOptions()) {
+ $globals['options'] = $annot->getOptions();
+ }
+
+ if (null !== $annot->getDefaults()) {
+ $globals['defaults'] = $annot->getDefaults();
+ }
+
+ if (null !== $annot->getSchemes()) {
+ $globals['schemes'] = $annot->getSchemes();
+ }
+
+ if (null !== $annot->getMethods()) {
+ $globals['methods'] = $annot->getMethods();
+ }
+
+ if (null !== $annot->getHost()) {
+ $globals['host'] = $annot->getHost();
+ }
+
+ if (null !== $annot->getCondition()) {
+ $globals['condition'] = $annot->getCondition();
+ }
+
+ $globals['priority'] = $annot->getPriority() ?? 0;
+ $globals['env'] = $annot->getEnv();
+
+ foreach ($globals['requirements'] as $placeholder => $requirement) {
+ if (\is_int($placeholder)) {
+ throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?', $placeholder, $requirement, $class->getName()));
+ }
+ }
+ }
+
+ return $globals;
+ }
+
+ private function resetGlobals(): array
+ {
+ return [
+ 'path' => null,
+ 'localized_paths' => [],
+ 'requirements' => [],
+ 'options' => [],
+ 'defaults' => [],
+ 'schemes' => [],
+ 'methods' => [],
+ 'host' => '',
+ 'condition' => '',
+ 'name' => '',
+ 'priority' => 0,
+ 'env' => null,
+ ];
+ }
+
+ protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition)
+ {
+ return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
+ }
+
+ abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot);
+
+ /**
+ * @param \ReflectionClass|\ReflectionMethod $reflection
+ *
+ * @return iterable
+ */
+ private function getAnnotations(object $reflection): iterable
+ {
+ if (\PHP_VERSION_ID >= 80000) {
+ foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ yield $attribute->newInstance();
+ }
+ }
+
+ if (!$this->reader) {
+ return;
+ }
+
+ $anntotations = $reflection instanceof \ReflectionClass
+ ? $this->reader->getClassAnnotations($reflection)
+ : $this->reader->getMethodAnnotations($reflection);
+
+ foreach ($anntotations as $annotation) {
+ if ($annotation instanceof $this->routeAnnotationClass) {
+ yield $annotation;
+ }
+ }
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/AnnotationDirectoryLoader.php b/src/vendor/symfony/routing/Loader/AnnotationDirectoryLoader.php
new file mode 100644
index 000000000..8cd60f827
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/AnnotationDirectoryLoader.php
@@ -0,0 +1,93 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Resource\DirectoryResource;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * AnnotationDirectoryLoader loads routing information from annotations set
+ * on PHP classes and methods.
+ *
+ * @author Fabien Potencier
+ */
+class AnnotationDirectoryLoader extends AnnotationFileLoader
+{
+ /**
+ * Loads from annotations from a directory.
+ *
+ * @param string $path A directory path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection
+ *
+ * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed
+ */
+ public function load($path, ?string $type = null)
+ {
+ if (!is_dir($dir = $this->locator->locate($path))) {
+ return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection();
+ }
+
+ $collection = new RouteCollection();
+ $collection->addResource(new DirectoryResource($dir, '/\.php$/'));
+ $files = iterator_to_array(new \RecursiveIteratorIterator(
+ new \RecursiveCallbackFilterIterator(
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
+ function (\SplFileInfo $current) {
+ return '.' !== substr($current->getBasename(), 0, 1);
+ }
+ ),
+ \RecursiveIteratorIterator::LEAVES_ONLY
+ ));
+ usort($files, function (\SplFileInfo $a, \SplFileInfo $b) {
+ return (string) $a > (string) $b ? 1 : -1;
+ });
+
+ foreach ($files as $file) {
+ if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) {
+ continue;
+ }
+
+ if ($class = $this->findClass($file)) {
+ $refl = new \ReflectionClass($class);
+ if ($refl->isAbstract()) {
+ continue;
+ }
+
+ $collection->addCollection($this->loader->load($class, $type));
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ if ('annotation' === $type) {
+ return true;
+ }
+
+ if ($type || !\is_string($resource)) {
+ return false;
+ }
+
+ try {
+ return is_dir($this->locator->locate($resource));
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/AnnotationFileLoader.php b/src/vendor/symfony/routing/Loader/AnnotationFileLoader.php
new file mode 100644
index 000000000..e75eac11c
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/AnnotationFileLoader.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\FileLocatorInterface;
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * AnnotationFileLoader loads routing information from annotations set
+ * on a PHP class and its methods.
+ *
+ * @author Fabien Potencier
+ */
+class AnnotationFileLoader extends FileLoader
+{
+ protected $loader;
+
+ public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader)
+ {
+ if (!\function_exists('token_get_all')) {
+ throw new \LogicException('The Tokenizer extension is required for the routing annotation loaders.');
+ }
+
+ parent::__construct($locator);
+
+ $this->loader = $loader;
+ }
+
+ /**
+ * Loads from annotations from a file.
+ *
+ * @param string $file A PHP file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection|null
+ *
+ * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed
+ */
+ public function load($file, ?string $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $collection = new RouteCollection();
+ if ($class = $this->findClass($path)) {
+ $refl = new \ReflectionClass($class);
+ if ($refl->isAbstract()) {
+ return null;
+ }
+
+ $collection->addResource(new FileResource($path));
+ $collection->addCollection($this->loader->load($class, $type));
+ }
+
+ gc_mem_caches();
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'annotation' === $type);
+ }
+
+ /**
+ * Returns the full class name for the first class in the file.
+ *
+ * @return string|false
+ */
+ protected function findClass(string $file)
+ {
+ $class = false;
+ $namespace = false;
+ $tokens = token_get_all(file_get_contents($file));
+
+ if (1 === \count($tokens) && \T_INLINE_HTML === $tokens[0][0]) {
+ throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forget to add the " true, \T_STRING => true];
+ if (\defined('T_NAME_QUALIFIED')) {
+ $nsTokens[\T_NAME_QUALIFIED] = true;
+ }
+ for ($i = 0; isset($tokens[$i]); ++$i) {
+ $token = $tokens[$i];
+ if (!isset($token[1])) {
+ continue;
+ }
+
+ if (true === $class && \T_STRING === $token[0]) {
+ return $namespace.'\\'.$token[1];
+ }
+
+ if (true === $namespace && isset($nsTokens[$token[0]])) {
+ $namespace = $token[1];
+ while (isset($tokens[++$i][1], $nsTokens[$tokens[$i][0]])) {
+ $namespace .= $tokens[$i][1];
+ }
+ $token = $tokens[$i];
+ }
+
+ if (\T_CLASS === $token[0]) {
+ // Skip usage of ::class constant and anonymous classes
+ $skipClassToken = false;
+ for ($j = $i - 1; $j > 0; --$j) {
+ if (!isset($tokens[$j][1])) {
+ if ('(' === $tokens[$j] || ',' === $tokens[$j]) {
+ $skipClassToken = true;
+ }
+ break;
+ }
+
+ if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) {
+ $skipClassToken = true;
+ break;
+ } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) {
+ break;
+ }
+ }
+
+ if (!$skipClassToken) {
+ $class = true;
+ }
+ }
+
+ if (\T_NAMESPACE === $token[0]) {
+ $namespace = true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/ClosureLoader.php b/src/vendor/symfony/routing/Loader/ClosureLoader.php
new file mode 100644
index 000000000..a5081ca28
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/ClosureLoader.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\Loader;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * ClosureLoader loads routes from a PHP closure.
+ *
+ * The Closure must return a RouteCollection instance.
+ *
+ * @author Fabien Potencier
+ */
+class ClosureLoader extends Loader
+{
+ /**
+ * Loads a Closure.
+ *
+ * @param \Closure $closure A Closure
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection
+ */
+ public function load($closure, ?string $type = null)
+ {
+ return $closure($this->env);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return $resource instanceof \Closure && (!$type || 'closure' === $type);
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/AliasConfigurator.php b/src/vendor/symfony/routing/Loader/Configurator/AliasConfigurator.php
new file mode 100644
index 000000000..4b2206e68
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/AliasConfigurator.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator;
+
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
+use Symfony\Component\Routing\Alias;
+
+class AliasConfigurator
+{
+ private $alias;
+
+ public function __construct(Alias $alias)
+ {
+ $this->alias = $alias;
+ }
+
+ /**
+ * Whether this alias is deprecated, that means it should not be called anymore.
+ *
+ * @param string $package The name of the composer package that is triggering the deprecation
+ * @param string $version The version of the package that introduced the deprecation
+ * @param string $message The deprecation message to use
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException when the message template is invalid
+ */
+ public function deprecate(string $package, string $version, string $message): self
+ {
+ $this->alias->setDeprecated($package, $version, $message);
+
+ return $this;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/CollectionConfigurator.php b/src/vendor/symfony/routing/Loader/Configurator/CollectionConfigurator.php
new file mode 100644
index 000000000..ec59f7ee9
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/CollectionConfigurator.php
@@ -0,0 +1,125 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @author Nicolas Grekas
+ */
+class CollectionConfigurator
+{
+ use Traits\AddTrait;
+ use Traits\HostTrait;
+ use Traits\RouteTrait;
+
+ private $parent;
+ private $parentConfigurator;
+ private $parentPrefixes;
+ private $host;
+
+ public function __construct(RouteCollection $parent, string $name, ?self $parentConfigurator = null, ?array $parentPrefixes = null)
+ {
+ $this->parent = $parent;
+ $this->name = $name;
+ $this->collection = new RouteCollection();
+ $this->route = new Route('');
+ $this->parentConfigurator = $parentConfigurator; // for GC control
+ $this->parentPrefixes = $parentPrefixes;
+ }
+
+ /**
+ * @return array
+ */
+ public function __sleep()
+ {
+ throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ public function __destruct()
+ {
+ if (null === $this->prefixes) {
+ $this->collection->addPrefix($this->route->getPath());
+ }
+ if (null !== $this->host) {
+ $this->addHost($this->collection, $this->host);
+ }
+
+ $this->parent->addCollection($this->collection);
+ }
+
+ /**
+ * Creates a sub-collection.
+ */
+ final public function collection(string $name = ''): self
+ {
+ return new self($this->collection, $this->name.$name, $this, $this->prefixes);
+ }
+
+ /**
+ * Sets the prefix to add to the path of all child routes.
+ *
+ * @param string|array $prefix the prefix, or the localized prefixes
+ *
+ * @return $this
+ */
+ final public function prefix($prefix): self
+ {
+ if (\is_array($prefix)) {
+ if (null === $this->parentPrefixes) {
+ // no-op
+ } elseif ($missing = array_diff_key($this->parentPrefixes, $prefix)) {
+ throw new \LogicException(sprintf('Collection "%s" is missing prefixes for locale(s) "%s".', $this->name, implode('", "', array_keys($missing))));
+ } else {
+ foreach ($prefix as $locale => $localePrefix) {
+ if (!isset($this->parentPrefixes[$locale])) {
+ throw new \LogicException(sprintf('Collection "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $this->name, $locale));
+ }
+
+ $prefix[$locale] = $this->parentPrefixes[$locale].$localePrefix;
+ }
+ }
+ $this->prefixes = $prefix;
+ $this->route->setPath('/');
+ } else {
+ $this->prefixes = null;
+ $this->route->setPath($prefix);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the host to use for all child routes.
+ *
+ * @param string|array $host the host, or the localized hosts
+ *
+ * @return $this
+ */
+ final public function host($host): self
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ private function createRoute(string $path): Route
+ {
+ return (clone $this->route)->setPath($path);
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php b/src/vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php
new file mode 100644
index 000000000..32f3efe3a
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @author Nicolas Grekas
+ */
+class ImportConfigurator
+{
+ use Traits\HostTrait;
+ use Traits\PrefixTrait;
+ use Traits\RouteTrait;
+
+ private $parent;
+
+ public function __construct(RouteCollection $parent, RouteCollection $route)
+ {
+ $this->parent = $parent;
+ $this->route = $route;
+ }
+
+ /**
+ * @return array
+ */
+ public function __sleep()
+ {
+ throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ public function __destruct()
+ {
+ $this->parent->addCollection($this->route);
+ }
+
+ /**
+ * Sets the prefix to add to the path of all child routes.
+ *
+ * @param string|array $prefix the prefix, or the localized prefixes
+ *
+ * @return $this
+ */
+ final public function prefix($prefix, bool $trailingSlashOnRoot = true): self
+ {
+ $this->addPrefix($this->route, $prefix, $trailingSlashOnRoot);
+
+ return $this;
+ }
+
+ /**
+ * Sets the prefix to add to the name of all child routes.
+ *
+ * @return $this
+ */
+ final public function namePrefix(string $namePrefix): self
+ {
+ $this->route->addNamePrefix($namePrefix);
+
+ return $this;
+ }
+
+ /**
+ * Sets the host to use for all child routes.
+ *
+ * @param string|array $host the host, or the localized hosts
+ *
+ * @return $this
+ */
+ final public function host($host): self
+ {
+ $this->addHost($this->route, $host);
+
+ return $this;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/RouteConfigurator.php b/src/vendor/symfony/routing/Loader/Configurator/RouteConfigurator.php
new file mode 100644
index 000000000..fcd1c2157
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/RouteConfigurator.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @author Nicolas Grekas
+ */
+class RouteConfigurator
+{
+ use Traits\AddTrait;
+ use Traits\HostTrait;
+ use Traits\RouteTrait;
+
+ protected $parentConfigurator;
+
+ public function __construct(RouteCollection $collection, RouteCollection $route, string $name = '', ?CollectionConfigurator $parentConfigurator = null, ?array $prefixes = null)
+ {
+ $this->collection = $collection;
+ $this->route = $route;
+ $this->name = $name;
+ $this->parentConfigurator = $parentConfigurator; // for GC control
+ $this->prefixes = $prefixes;
+ }
+
+ /**
+ * Sets the host to use for all child routes.
+ *
+ * @param string|array $host the host, or the localized hosts
+ *
+ * @return $this
+ */
+ final public function host($host): self
+ {
+ $this->addHost($this->route, $host);
+
+ return $this;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/RoutingConfigurator.php b/src/vendor/symfony/routing/Loader/Configurator/RoutingConfigurator.php
new file mode 100644
index 000000000..620b2d586
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/RoutingConfigurator.php
@@ -0,0 +1,81 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator;
+
+use Symfony\Component\Routing\Loader\PhpFileLoader;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @author Nicolas Grekas
+ */
+class RoutingConfigurator
+{
+ use Traits\AddTrait;
+
+ private $loader;
+ private $path;
+ private $file;
+ private $env;
+
+ public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, ?string $env = null)
+ {
+ $this->collection = $collection;
+ $this->loader = $loader;
+ $this->path = $path;
+ $this->file = $file;
+ $this->env = $env;
+ }
+
+ /**
+ * @param string|string[]|null $exclude Glob patterns to exclude from the import
+ */
+ final public function import($resource, ?string $type = null, bool $ignoreErrors = false, $exclude = null): ImportConfigurator
+ {
+ $this->loader->setCurrentDir(\dirname($this->path));
+
+ $imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file, $exclude) ?: [];
+ if (!\is_array($imported)) {
+ return new ImportConfigurator($this->collection, $imported);
+ }
+
+ $mergedCollection = new RouteCollection();
+ foreach ($imported as $subCollection) {
+ $mergedCollection->addCollection($subCollection);
+ }
+
+ return new ImportConfigurator($this->collection, $mergedCollection);
+ }
+
+ final public function collection(string $name = ''): CollectionConfigurator
+ {
+ return new CollectionConfigurator($this->collection, $name);
+ }
+
+ /**
+ * Get the current environment to be able to write conditional configuration.
+ */
+ final public function env(): ?string
+ {
+ return $this->env;
+ }
+
+ /**
+ * @return static
+ */
+ final public function withPath(string $path): self
+ {
+ $clone = clone $this;
+ $clone->path = $clone->file = $path;
+
+ return $clone;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/Traits/AddTrait.php b/src/vendor/symfony/routing/Loader/Configurator/Traits/AddTrait.php
new file mode 100644
index 000000000..92b1bd0ea
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/Traits/AddTrait.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator\Traits;
+
+use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
+use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
+use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @author Nicolas Grekas
+ */
+trait AddTrait
+{
+ use LocalizedRouteTrait;
+
+ /**
+ * @var RouteCollection
+ */
+ protected $collection;
+ protected $name = '';
+ protected $prefixes;
+
+ /**
+ * Adds a route.
+ *
+ * @param string|array $path the path, or the localized paths of the route
+ */
+ public function add(string $name, $path): RouteConfigurator
+ {
+ $parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null);
+ $route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes);
+
+ return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
+ }
+
+ public function alias(string $name, string $alias): AliasConfigurator
+ {
+ return new AliasConfigurator($this->collection->addAlias($name, $alias));
+ }
+
+ /**
+ * Adds a route.
+ *
+ * @param string|array $path the path, or the localized paths of the route
+ */
+ public function __invoke(string $name, $path): RouteConfigurator
+ {
+ return $this->add($name, $path);
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/Traits/HostTrait.php b/src/vendor/symfony/routing/Loader/Configurator/Traits/HostTrait.php
new file mode 100644
index 000000000..168bbb4f9
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/Traits/HostTrait.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator\Traits;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @internal
+ */
+trait HostTrait
+{
+ final protected function addHost(RouteCollection $routes, $hosts)
+ {
+ if (!$hosts || !\is_array($hosts)) {
+ $routes->setHost($hosts ?: '');
+
+ return;
+ }
+
+ foreach ($routes->all() as $name => $route) {
+ if (null === $locale = $route->getDefault('_locale')) {
+ $priority = $routes->getPriority($name) ?? 0;
+ $routes->remove($name);
+ foreach ($hosts as $locale => $host) {
+ $localizedRoute = clone $route;
+ $localizedRoute->setDefault('_locale', $locale);
+ $localizedRoute->setRequirement('_locale', preg_quote($locale));
+ $localizedRoute->setDefault('_canonical_route', $name);
+ $localizedRoute->setHost($host);
+ $routes->add($name.'.'.$locale, $localizedRoute, $priority);
+ }
+ } elseif (!isset($hosts[$locale])) {
+ throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding host in its parent collection.', $name, $locale));
+ } else {
+ $route->setHost($hosts[$locale]);
+ $route->setRequirement('_locale', preg_quote($locale));
+ $routes->add($name, $route, $routes->getPriority($name) ?? 0);
+ }
+ }
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php b/src/vendor/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php
new file mode 100644
index 000000000..44fb047a9
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator\Traits;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @internal
+ *
+ * @author Nicolas Grekas
+ * @author Jules Pietri
+ */
+trait LocalizedRouteTrait
+{
+ /**
+ * Creates one or many routes.
+ *
+ * @param string|array $path the path, or the localized paths of the route
+ */
+ final protected function createLocalizedRoute(RouteCollection $collection, string $name, $path, string $namePrefix = '', ?array $prefixes = null): RouteCollection
+ {
+ $paths = [];
+
+ $routes = new RouteCollection();
+
+ if (\is_array($path)) {
+ if (null === $prefixes) {
+ $paths = $path;
+ } elseif ($missing = array_diff_key($prefixes, $path)) {
+ throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing))));
+ } else {
+ foreach ($path as $locale => $localePath) {
+ if (!isset($prefixes[$locale])) {
+ throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
+ }
+
+ $paths[$locale] = $prefixes[$locale].$localePath;
+ }
+ }
+ } elseif (null !== $prefixes) {
+ foreach ($prefixes as $locale => $prefix) {
+ $paths[$locale] = $prefix.$path;
+ }
+ } else {
+ $routes->add($namePrefix.$name, $route = $this->createRoute($path));
+ $collection->add($namePrefix.$name, $route);
+
+ return $routes;
+ }
+
+ foreach ($paths as $locale => $path) {
+ $routes->add($name.'.'.$locale, $route = $this->createRoute($path));
+ $collection->add($namePrefix.$name.'.'.$locale, $route);
+ $route->setDefault('_locale', $locale);
+ $route->setRequirement('_locale', preg_quote($locale));
+ $route->setDefault('_canonical_route', $namePrefix.$name);
+ }
+
+ return $routes;
+ }
+
+ private function createRoute(string $path): Route
+ {
+ return new Route($path);
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php b/src/vendor/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php
new file mode 100644
index 000000000..0b19573ec
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator\Traits;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @internal
+ *
+ * @author Nicolas Grekas
+ */
+trait PrefixTrait
+{
+ final protected function addPrefix(RouteCollection $routes, $prefix, bool $trailingSlashOnRoot)
+ {
+ if (\is_array($prefix)) {
+ foreach ($prefix as $locale => $localePrefix) {
+ $prefix[$locale] = trim(trim($localePrefix), '/');
+ }
+ foreach ($routes->all() as $name => $route) {
+ if (null === $locale = $route->getDefault('_locale')) {
+ $priority = $routes->getPriority($name) ?? 0;
+ $routes->remove($name);
+ foreach ($prefix as $locale => $localePrefix) {
+ $localizedRoute = clone $route;
+ $localizedRoute->setDefault('_locale', $locale);
+ $localizedRoute->setRequirement('_locale', preg_quote($locale));
+ $localizedRoute->setDefault('_canonical_route', $name);
+ $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
+ $routes->add($name.'.'.$locale, $localizedRoute, $priority);
+ }
+ } elseif (!isset($prefix[$locale])) {
+ throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
+ } else {
+ $route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
+ $routes->add($name, $route, $routes->getPriority($name) ?? 0);
+ }
+ }
+
+ return;
+ }
+
+ $routes->addPrefix($prefix);
+ if (!$trailingSlashOnRoot) {
+ $rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
+ foreach ($routes->all() as $route) {
+ if ($route->getPath() === $rootPath) {
+ $route->setPath(rtrim($rootPath, '/'));
+ }
+ }
+ }
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/Configurator/Traits/RouteTrait.php b/src/vendor/symfony/routing/Loader/Configurator/Traits/RouteTrait.php
new file mode 100644
index 000000000..ac05d10e5
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/Configurator/Traits/RouteTrait.php
@@ -0,0 +1,175 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader\Configurator\Traits;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+trait RouteTrait
+{
+ /**
+ * @var RouteCollection|Route
+ */
+ protected $route;
+
+ /**
+ * Adds defaults.
+ *
+ * @return $this
+ */
+ final public function defaults(array $defaults): self
+ {
+ $this->route->addDefaults($defaults);
+
+ return $this;
+ }
+
+ /**
+ * Adds requirements.
+ *
+ * @return $this
+ */
+ final public function requirements(array $requirements): self
+ {
+ $this->route->addRequirements($requirements);
+
+ return $this;
+ }
+
+ /**
+ * Adds options.
+ *
+ * @return $this
+ */
+ final public function options(array $options): self
+ {
+ $this->route->addOptions($options);
+
+ return $this;
+ }
+
+ /**
+ * Whether paths should accept utf8 encoding.
+ *
+ * @return $this
+ */
+ final public function utf8(bool $utf8 = true): self
+ {
+ $this->route->addOptions(['utf8' => $utf8]);
+
+ return $this;
+ }
+
+ /**
+ * Sets the condition.
+ *
+ * @return $this
+ */
+ final public function condition(string $condition): self
+ {
+ $this->route->setCondition($condition);
+
+ return $this;
+ }
+
+ /**
+ * Sets the pattern for the host.
+ *
+ * @return $this
+ */
+ final public function host(string $pattern): self
+ {
+ $this->route->setHost($pattern);
+
+ return $this;
+ }
+
+ /**
+ * Sets the schemes (e.g. 'https') this route is restricted to.
+ * So an empty array means that any scheme is allowed.
+ *
+ * @param string[] $schemes
+ *
+ * @return $this
+ */
+ final public function schemes(array $schemes): self
+ {
+ $this->route->setSchemes($schemes);
+
+ return $this;
+ }
+
+ /**
+ * Sets the HTTP methods (e.g. 'POST') this route is restricted to.
+ * So an empty array means that any method is allowed.
+ *
+ * @param string[] $methods
+ *
+ * @return $this
+ */
+ final public function methods(array $methods): self
+ {
+ $this->route->setMethods($methods);
+
+ return $this;
+ }
+
+ /**
+ * Adds the "_controller" entry to defaults.
+ *
+ * @param callable|string|array $controller a callable or parseable pseudo-callable
+ *
+ * @return $this
+ */
+ final public function controller($controller): self
+ {
+ $this->route->addDefaults(['_controller' => $controller]);
+
+ return $this;
+ }
+
+ /**
+ * Adds the "_locale" entry to defaults.
+ *
+ * @return $this
+ */
+ final public function locale(string $locale): self
+ {
+ $this->route->addDefaults(['_locale' => $locale]);
+
+ return $this;
+ }
+
+ /**
+ * Adds the "_format" entry to defaults.
+ *
+ * @return $this
+ */
+ final public function format(string $format): self
+ {
+ $this->route->addDefaults(['_format' => $format]);
+
+ return $this;
+ }
+
+ /**
+ * Adds the "_stateless" entry to defaults.
+ *
+ * @return $this
+ */
+ final public function stateless(bool $stateless = true): self
+ {
+ $this->route->addDefaults(['_stateless' => $stateless]);
+
+ return $this;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/ContainerLoader.php b/src/vendor/symfony/routing/Loader/ContainerLoader.php
new file mode 100644
index 000000000..a03d46524
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/ContainerLoader.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Psr\Container\ContainerInterface;
+
+/**
+ * A route loader that executes a service from a PSR-11 container to load the routes.
+ *
+ * @author Ryan Weaver
+ */
+class ContainerLoader extends ObjectLoader
+{
+ private $container;
+
+ public function __construct(ContainerInterface $container, ?string $env = null)
+ {
+ $this->container = $container;
+ parent::__construct($env);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return 'service' === $type && \is_string($resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getObject(string $id)
+ {
+ return $this->container->get($id);
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/DirectoryLoader.php b/src/vendor/symfony/routing/Loader/DirectoryLoader.php
new file mode 100644
index 000000000..24cf185d6
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/DirectoryLoader.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Resource\DirectoryResource;
+use Symfony\Component\Routing\RouteCollection;
+
+class DirectoryLoader extends FileLoader
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load($file, ?string $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new DirectoryResource($path));
+
+ foreach (scandir($path) as $dir) {
+ if ('.' !== $dir[0]) {
+ $this->setCurrentDir($path);
+ $subPath = $path.'/'.$dir;
+ $subType = null;
+
+ if (is_dir($subPath)) {
+ $subPath .= '/';
+ $subType = 'directory';
+ }
+
+ $subCollection = $this->import($subPath, $subType, false, $path);
+ $collection->addCollection($subCollection);
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ // only when type is forced to directory, not to conflict with AnnotationLoader
+
+ return 'directory' === $type;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/GlobFileLoader.php b/src/vendor/symfony/routing/Loader/GlobFileLoader.php
new file mode 100644
index 000000000..9c2f4ed4f
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/GlobFileLoader.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GlobFileLoader loads files from a glob pattern.
+ *
+ * @author Nicolas Grekas
+ */
+class GlobFileLoader extends FileLoader
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load($resource, ?string $type = null)
+ {
+ $collection = new RouteCollection();
+
+ foreach ($this->glob($resource, false, $globResource) as $path => $info) {
+ $collection->addCollection($this->import($path));
+ }
+
+ $collection->addResource($globResource);
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return 'glob' === $type;
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/ObjectLoader.php b/src/vendor/symfony/routing/Loader/ObjectLoader.php
new file mode 100644
index 000000000..d212f8e8b
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/ObjectLoader.php
@@ -0,0 +1,84 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\Loader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * A route loader that calls a method on an object to load the routes.
+ *
+ * @author Ryan Weaver
+ */
+abstract class ObjectLoader extends Loader
+{
+ /**
+ * Returns the object that the method will be called on to load routes.
+ *
+ * For example, if your application uses a service container,
+ * the $id may be a service id.
+ *
+ * @return object
+ */
+ abstract protected function getObject(string $id);
+
+ /**
+ * Calls the object method that will load the routes.
+ *
+ * @param string $resource object_id::method
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection
+ */
+ public function load($resource, ?string $type = null)
+ {
+ if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) {
+ throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object'));
+ }
+
+ $parts = explode('::', $resource);
+ $method = $parts[1] ?? '__invoke';
+
+ $loaderObject = $this->getObject($parts[0]);
+
+ if (!\is_object($loaderObject)) {
+ throw new \TypeError(sprintf('"%s:getObject()" must return an object: "%s" returned.', static::class, get_debug_type($loaderObject)));
+ }
+
+ if (!\is_callable([$loaderObject, $method])) {
+ throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s".', $method, get_debug_type($loaderObject), $resource));
+ }
+
+ $routeCollection = $loaderObject->$method($this, $this->env);
+
+ if (!$routeCollection instanceof RouteCollection) {
+ $type = get_debug_type($routeCollection);
+
+ throw new \LogicException(sprintf('The "%s::%s()" method must return a RouteCollection: "%s" returned.', get_debug_type($loaderObject), $method, $type));
+ }
+
+ // make the object file tracked so that if it changes, the cache rebuilds
+ $this->addClassResource(new \ReflectionClass($loaderObject), $routeCollection);
+
+ return $routeCollection;
+ }
+
+ private function addClassResource(\ReflectionClass $class, RouteCollection $collection)
+ {
+ do {
+ if (is_file($class->getFileName())) {
+ $collection->addResource(new FileResource($class->getFileName()));
+ }
+ } while ($class = $class->getParentClass());
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/PhpFileLoader.php b/src/vendor/symfony/routing/Loader/PhpFileLoader.php
new file mode 100644
index 000000000..3f1cf9cd1
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/PhpFileLoader.php
@@ -0,0 +1,85 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * PhpFileLoader loads routes from a PHP file.
+ *
+ * The file must return a RouteCollection instance.
+ *
+ * @author Fabien Potencier
+ * @author Nicolas grekas
+ * @author Jules Pietri
+ */
+class PhpFileLoader extends FileLoader
+{
+ /**
+ * Loads a PHP file.
+ *
+ * @param string $file A PHP file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection
+ */
+ public function load($file, ?string $type = null)
+ {
+ $path = $this->locator->locate($file);
+ $this->setCurrentDir(\dirname($path));
+
+ // the closure forbids access to the private scope in the included file
+ $loader = $this;
+ $load = \Closure::bind(static function ($file) use ($loader) {
+ return include $file;
+ }, null, ProtectedPhpFileLoader::class);
+
+ $result = $load($path);
+
+ if (\is_object($result) && \is_callable($result)) {
+ $collection = $this->callConfigurator($result, $path, $file);
+ } else {
+ $collection = $result;
+ }
+
+ $collection->addResource(new FileResource($path));
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type);
+ }
+
+ protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
+ {
+ $collection = new RouteCollection();
+
+ $result(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
+
+ return $collection;
+ }
+}
+
+/**
+ * @internal
+ */
+final class ProtectedPhpFileLoader extends PhpFileLoader
+{
+}
diff --git a/src/vendor/symfony/routing/Loader/XmlFileLoader.php b/src/vendor/symfony/routing/Loader/XmlFileLoader.php
new file mode 100644
index 000000000..85bb0ee8c
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/XmlFileLoader.php
@@ -0,0 +1,469 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Config\Util\XmlUtils;
+use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
+use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
+use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * XmlFileLoader loads XML routing files.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+class XmlFileLoader extends FileLoader
+{
+ use HostTrait;
+ use LocalizedRouteTrait;
+ use PrefixTrait;
+
+ public const NAMESPACE_URI = 'http://symfony.com/schema/routing';
+ public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
+
+ /**
+ * Loads an XML file.
+ *
+ * @param string $file An XML file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection
+ *
+ * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be
+ * parsed because it does not validate against the scheme
+ */
+ public function load($file, ?string $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $xml = $this->loadFile($path);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($path));
+
+ // process routes and imports
+ foreach ($xml->documentElement->childNodes as $node) {
+ if (!$node instanceof \DOMElement) {
+ continue;
+ }
+
+ $this->parseNode($collection, $node, $path, $file);
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Parses a node from a loaded XML file.
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file)
+ {
+ if (self::NAMESPACE_URI !== $node->namespaceURI) {
+ return;
+ }
+
+ switch ($node->localName) {
+ case 'route':
+ $this->parseRoute($collection, $node, $path);
+ break;
+ case 'import':
+ $this->parseImport($collection, $node, $path, $file);
+ break;
+ case 'when':
+ if (!$this->env || $node->getAttribute('env') !== $this->env) {
+ break;
+ }
+ foreach ($node->childNodes as $node) {
+ if ($node instanceof \DOMElement) {
+ $this->parseNode($collection, $node, $path, $file);
+ }
+ }
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
+ }
+
+ /**
+ * Parses a route and adds it to the RouteCollection.
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path)
+ {
+ if ('' === $id = $node->getAttribute('id')) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" attribute.', $path));
+ }
+
+ if ('' !== $alias = $node->getAttribute('alias')) {
+ $alias = $collection->addAlias($id, $alias);
+
+ if ($deprecationInfo = $this->parseDeprecation($node, $path)) {
+ $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']);
+ }
+
+ return;
+ }
+
+ $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY);
+ $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY);
+
+ [$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path);
+
+ if (!$paths && '' === $node->getAttribute('path')) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "path" attribute or child nodes.', $path));
+ }
+
+ if ($paths && '' !== $node->getAttribute('path')) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "path" attribute and child nodes.', $path));
+ }
+
+ $routes = $this->createLocalizedRoute($collection, $id, $paths ?: $node->getAttribute('path'));
+ $routes->addDefaults($defaults);
+ $routes->addRequirements($requirements);
+ $routes->addOptions($options);
+ $routes->setSchemes($schemes);
+ $routes->setMethods($methods);
+ $routes->setCondition($condition);
+
+ if (null !== $hosts) {
+ $this->addHost($routes, $hosts);
+ }
+ }
+
+ /**
+ * Parses an import and adds the routes in the resource to the RouteCollection.
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file)
+ {
+ if ('' === $resource = $node->getAttribute('resource')) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "resource" attribute.', $path));
+ }
+
+ $type = $node->getAttribute('type');
+ $prefix = $node->getAttribute('prefix');
+ $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null;
+ $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null;
+ $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true;
+ $namePrefix = $node->getAttribute('name-prefix') ?: null;
+
+ [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path);
+
+ if ('' !== $prefix && $prefixes) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "prefix" attribute and child nodes.', $path));
+ }
+
+ $exclude = [];
+ foreach ($node->childNodes as $child) {
+ if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
+ $exclude[] = $child->nodeValue;
+ }
+ }
+
+ if ($node->hasAttribute('exclude')) {
+ if ($exclude) {
+ throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and tags at the same time.');
+ }
+ $exclude = [$node->getAttribute('exclude')];
+ }
+
+ $this->setCurrentDir(\dirname($path));
+
+ /** @var RouteCollection[] $imported */
+ $imported = $this->import($resource, '' !== $type ? $type : null, false, $file, $exclude) ?: [];
+
+ if (!\is_array($imported)) {
+ $imported = [$imported];
+ }
+
+ foreach ($imported as $subCollection) {
+ $this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot);
+
+ if (null !== $hosts) {
+ $this->addHost($subCollection, $hosts);
+ }
+
+ if (null !== $condition) {
+ $subCollection->setCondition($condition);
+ }
+ if (null !== $schemes) {
+ $subCollection->setSchemes($schemes);
+ }
+ if (null !== $methods) {
+ $subCollection->setMethods($methods);
+ }
+ if (null !== $namePrefix) {
+ $subCollection->addNamePrefix($namePrefix);
+ }
+ $subCollection->addDefaults($defaults);
+ $subCollection->addRequirements($requirements);
+ $subCollection->addOptions($options);
+
+ $collection->addCollection($subCollection);
+ }
+ }
+
+ /**
+ * @return \DOMDocument
+ *
+ * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
+ * or when the XML structure is not as expected by the scheme -
+ * see validate()
+ */
+ protected function loadFile(string $file)
+ {
+ return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
+ }
+
+ /**
+ * Parses the config elements (default, requirement, option).
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ private function parseConfigs(\DOMElement $node, string $path): array
+ {
+ $defaults = [];
+ $requirements = [];
+ $options = [];
+ $condition = null;
+ $prefixes = [];
+ $paths = [];
+ $hosts = [];
+
+ /** @var \DOMElement $n */
+ foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
+ if ($node !== $n->parentNode) {
+ continue;
+ }
+
+ switch ($n->localName) {
+ case 'path':
+ $paths[$n->getAttribute('locale')] = trim($n->textContent);
+ break;
+ case 'host':
+ $hosts[$n->getAttribute('locale')] = trim($n->textContent);
+ break;
+ case 'prefix':
+ $prefixes[$n->getAttribute('locale')] = trim($n->textContent);
+ break;
+ case 'default':
+ if ($this->isElementValueNull($n)) {
+ $defaults[$n->getAttribute('key')] = null;
+ } else {
+ $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path);
+ }
+
+ break;
+ case 'requirement':
+ $requirements[$n->getAttribute('key')] = trim($n->textContent);
+ break;
+ case 'option':
+ $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent));
+ break;
+ case 'condition':
+ $condition = trim($n->textContent);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path));
+ }
+ }
+
+ if ($controller = $node->getAttribute('controller')) {
+ if (isset($defaults['_controller'])) {
+ $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
+
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ', $path).$name);
+ }
+
+ $defaults['_controller'] = $controller;
+ }
+ if ($node->hasAttribute('locale')) {
+ $defaults['_locale'] = $node->getAttribute('locale');
+ }
+ if ($node->hasAttribute('format')) {
+ $defaults['_format'] = $node->getAttribute('format');
+ }
+ if ($node->hasAttribute('utf8')) {
+ $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8'));
+ }
+ if ($stateless = $node->getAttribute('stateless')) {
+ if (isset($defaults['_stateless'])) {
+ $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
+
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name);
+ }
+
+ $defaults['_stateless'] = XmlUtils::phpize($stateless);
+ }
+
+ if (!$hosts) {
+ $hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
+ }
+
+ return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts];
+ }
+
+ /**
+ * Parses the "default" elements.
+ *
+ * @return array|bool|float|int|string|null
+ */
+ private function parseDefaultsConfig(\DOMElement $element, string $path)
+ {
+ if ($this->isElementValueNull($element)) {
+ return null;
+ }
+
+ // Check for existing element nodes in the default element. There can
+ // only be a single element inside a default element. So this element
+ // (if one was found) can safely be returned.
+ foreach ($element->childNodes as $child) {
+ if (!$child instanceof \DOMElement) {
+ continue;
+ }
+
+ if (self::NAMESPACE_URI !== $child->namespaceURI) {
+ continue;
+ }
+
+ return $this->parseDefaultNode($child, $path);
+ }
+
+ // If the default element doesn't contain a nested "bool", "int", "float",
+ // "string", "list", or "map" element, the element contents will be treated
+ // as the string value of the associated default option.
+ return trim($element->textContent);
+ }
+
+ /**
+ * Recursively parses the value of a "default" element.
+ *
+ * @return array|bool|float|int|string|null
+ *
+ * @throws \InvalidArgumentException when the XML is invalid
+ */
+ private function parseDefaultNode(\DOMElement $node, string $path)
+ {
+ if ($this->isElementValueNull($node)) {
+ return null;
+ }
+
+ switch ($node->localName) {
+ case 'bool':
+ return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue);
+ case 'int':
+ return (int) trim($node->nodeValue);
+ case 'float':
+ return (float) trim($node->nodeValue);
+ case 'string':
+ return trim($node->nodeValue);
+ case 'list':
+ $list = [];
+
+ foreach ($node->childNodes as $element) {
+ if (!$element instanceof \DOMElement) {
+ continue;
+ }
+
+ if (self::NAMESPACE_URI !== $element->namespaceURI) {
+ continue;
+ }
+
+ $list[] = $this->parseDefaultNode($element, $path);
+ }
+
+ return $list;
+ case 'map':
+ $map = [];
+
+ foreach ($node->childNodes as $element) {
+ if (!$element instanceof \DOMElement) {
+ continue;
+ }
+
+ if (self::NAMESPACE_URI !== $element->namespaceURI) {
+ continue;
+ }
+
+ $map[$element->getAttribute('key')] = $this->parseDefaultNode($element, $path);
+ }
+
+ return $map;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".', $node->localName, $path));
+ }
+ }
+
+ private function isElementValueNull(\DOMElement $element): bool
+ {
+ $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
+
+ if (!$element->hasAttributeNS($namespaceUri, 'nil')) {
+ return false;
+ }
+
+ return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
+ }
+
+ /**
+ * Parses the deprecation elements.
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ private function parseDeprecation(\DOMElement $node, string $path): array
+ {
+ $deprecatedNode = null;
+ foreach ($node->childNodes as $child) {
+ if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) {
+ continue;
+ }
+ if ('deprecated' !== $child->localName) {
+ throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path));
+ }
+
+ $deprecatedNode = $child;
+ }
+
+ if (null === $deprecatedNode) {
+ return [];
+ }
+
+ if (!$deprecatedNode->hasAttribute('package')) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "package" attribute.', $path));
+ }
+ if (!$deprecatedNode->hasAttribute('version')) {
+ throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "version" attribute.', $path));
+ }
+
+ return [
+ 'package' => $deprecatedNode->getAttribute('package'),
+ 'version' => $deprecatedNode->getAttribute('version'),
+ 'message' => trim($deprecatedNode->nodeValue),
+ ];
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/YamlFileLoader.php b/src/vendor/symfony/routing/Loader/YamlFileLoader.php
new file mode 100644
index 000000000..1087817bb
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/YamlFileLoader.php
@@ -0,0 +1,314 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
+use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
+use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Yaml\Exception\ParseException;
+use Symfony\Component\Yaml\Parser as YamlParser;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * YamlFileLoader loads Yaml routing files.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+class YamlFileLoader extends FileLoader
+{
+ use HostTrait;
+ use LocalizedRouteTrait;
+ use PrefixTrait;
+
+ private const AVAILABLE_KEYS = [
+ 'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude', 'stateless',
+ ];
+ private $yamlParser;
+
+ /**
+ * Loads a Yaml file.
+ *
+ * @param string $file A Yaml file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection
+ *
+ * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid
+ */
+ public function load($file, ?string $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ if (!stream_is_local($path)) {
+ throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path));
+ }
+
+ if (!file_exists($path)) {
+ throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path));
+ }
+
+ if (null === $this->yamlParser) {
+ $this->yamlParser = new YamlParser();
+ }
+
+ try {
+ $parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT);
+ } catch (ParseException $e) {
+ throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: ', $path).$e->getMessage(), 0, $e);
+ }
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($path));
+
+ // empty file
+ if (null === $parsedConfig) {
+ return $collection;
+ }
+
+ // not an array
+ if (!\is_array($parsedConfig)) {
+ throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path));
+ }
+
+ foreach ($parsedConfig as $name => $config) {
+ if (0 === strpos($name, 'when@')) {
+ if (!$this->env || 'when@'.$this->env !== $name) {
+ continue;
+ }
+
+ foreach ($config as $name => $config) {
+ $this->validate($config, $name.'" when "@'.$this->env, $path);
+
+ if (isset($config['resource'])) {
+ $this->parseImport($collection, $config, $path, $file);
+ } else {
+ $this->parseRoute($collection, $name, $config, $path);
+ }
+ }
+
+ continue;
+ }
+
+ $this->validate($config, $name, $path);
+
+ if (isset($config['resource'])) {
+ $this->parseImport($collection, $config, $path, $file);
+ } else {
+ $this->parseRoute($collection, $name, $config, $path);
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, ?string $type = null)
+ {
+ return \is_string($resource) && \in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['yml', 'yaml'], true) && (!$type || 'yaml' === $type);
+ }
+
+ /**
+ * Parses a route and adds it to the RouteCollection.
+ */
+ protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path)
+ {
+ if (isset($config['alias'])) {
+ $alias = $collection->addAlias($name, $config['alias']);
+ $deprecation = $config['deprecated'] ?? null;
+ if (null !== $deprecation) {
+ $alias->setDeprecated(
+ $deprecation['package'],
+ $deprecation['version'],
+ $deprecation['message'] ?? ''
+ );
+ }
+
+ return;
+ }
+
+ $defaults = $config['defaults'] ?? [];
+ $requirements = $config['requirements'] ?? [];
+ $options = $config['options'] ?? [];
+
+ foreach ($requirements as $placeholder => $requirement) {
+ if (\is_int($placeholder)) {
+ throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s"?', $placeholder, $requirement, $name, $path));
+ }
+ }
+
+ if (isset($config['controller'])) {
+ $defaults['_controller'] = $config['controller'];
+ }
+ if (isset($config['locale'])) {
+ $defaults['_locale'] = $config['locale'];
+ }
+ if (isset($config['format'])) {
+ $defaults['_format'] = $config['format'];
+ }
+ if (isset($config['utf8'])) {
+ $options['utf8'] = $config['utf8'];
+ }
+ if (isset($config['stateless'])) {
+ $defaults['_stateless'] = $config['stateless'];
+ }
+
+ $routes = $this->createLocalizedRoute($collection, $name, $config['path']);
+ $routes->addDefaults($defaults);
+ $routes->addRequirements($requirements);
+ $routes->addOptions($options);
+ $routes->setSchemes($config['schemes'] ?? []);
+ $routes->setMethods($config['methods'] ?? []);
+ $routes->setCondition($config['condition'] ?? null);
+
+ if (isset($config['host'])) {
+ $this->addHost($routes, $config['host']);
+ }
+ }
+
+ /**
+ * Parses an import and adds the routes in the resource to the RouteCollection.
+ */
+ protected function parseImport(RouteCollection $collection, array $config, string $path, string $file)
+ {
+ $type = $config['type'] ?? null;
+ $prefix = $config['prefix'] ?? '';
+ $defaults = $config['defaults'] ?? [];
+ $requirements = $config['requirements'] ?? [];
+ $options = $config['options'] ?? [];
+ $host = $config['host'] ?? null;
+ $condition = $config['condition'] ?? null;
+ $schemes = $config['schemes'] ?? null;
+ $methods = $config['methods'] ?? null;
+ $trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true;
+ $namePrefix = $config['name_prefix'] ?? null;
+ $exclude = $config['exclude'] ?? null;
+
+ if (isset($config['controller'])) {
+ $defaults['_controller'] = $config['controller'];
+ }
+ if (isset($config['locale'])) {
+ $defaults['_locale'] = $config['locale'];
+ }
+ if (isset($config['format'])) {
+ $defaults['_format'] = $config['format'];
+ }
+ if (isset($config['utf8'])) {
+ $options['utf8'] = $config['utf8'];
+ }
+ if (isset($config['stateless'])) {
+ $defaults['_stateless'] = $config['stateless'];
+ }
+
+ $this->setCurrentDir(\dirname($path));
+
+ /** @var RouteCollection[] $imported */
+ $imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: [];
+
+ if (!\is_array($imported)) {
+ $imported = [$imported];
+ }
+
+ foreach ($imported as $subCollection) {
+ $this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot);
+
+ if (null !== $host) {
+ $this->addHost($subCollection, $host);
+ }
+ if (null !== $condition) {
+ $subCollection->setCondition($condition);
+ }
+ if (null !== $schemes) {
+ $subCollection->setSchemes($schemes);
+ }
+ if (null !== $methods) {
+ $subCollection->setMethods($methods);
+ }
+ if (null !== $namePrefix) {
+ $subCollection->addNamePrefix($namePrefix);
+ }
+ $subCollection->addDefaults($defaults);
+ $subCollection->addRequirements($requirements);
+ $subCollection->addOptions($options);
+
+ $collection->addCollection($subCollection);
+ }
+ }
+
+ /**
+ * Validates the route configuration.
+ *
+ * @param array $config A resource config
+ * @param string $name The config key
+ * @param string $path The loaded file path
+ *
+ * @throws \InvalidArgumentException If one of the provided config keys is not supported,
+ * something is missing or the combination is nonsense
+ */
+ protected function validate($config, string $name, string $path)
+ {
+ if (!\is_array($config)) {
+ throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
+ }
+ if (isset($config['alias'])) {
+ $this->validateAlias($config, $name, $path);
+
+ return;
+ }
+ if ($extraKeys = array_diff(array_keys($config), self::AVAILABLE_KEYS)) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', $path, $name, implode('", "', $extraKeys), implode('", "', self::AVAILABLE_KEYS)));
+ }
+ if (isset($config['resource']) && isset($config['path'])) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.', $path, $name));
+ }
+ if (!isset($config['resource']) && isset($config['type'])) {
+ throw new \InvalidArgumentException(sprintf('The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.', $name, $path));
+ }
+ if (!isset($config['resource']) && !isset($config['path'])) {
+ throw new \InvalidArgumentException(sprintf('You must define a "path" for the route "%s" in file "%s".', $name, $path));
+ }
+ if (isset($config['controller']) && isset($config['defaults']['_controller'])) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name));
+ }
+ if (isset($config['stateless']) && isset($config['defaults']['_stateless'])) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" key and the defaults key "_stateless" for "%s".', $path, $name));
+ }
+ }
+
+ /**
+ * @throws \InvalidArgumentException If one of the provided config keys is not supported,
+ * something is missing or the combination is nonsense
+ */
+ private function validateAlias(array $config, string $name, string $path): void
+ {
+ foreach ($config as $key => $value) {
+ if (!\in_array($key, ['alias', 'deprecated'], true)) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify other keys than "alias" and "deprecated" for "%s".', $path, $name));
+ }
+
+ if ('deprecated' === $key) {
+ if (!isset($value['package'])) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "package" of the "deprecated" option for "%s".', $path, $name));
+ }
+
+ if (!isset($value['version'])) {
+ throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "version" of the "deprecated" option for "%s".', $path, $name));
+ }
+ }
+ }
+ }
+}
diff --git a/src/vendor/symfony/routing/Loader/schema/routing/routing-1.0.xsd b/src/vendor/symfony/routing/Loader/schema/routing/routing-1.0.xsd
new file mode 100644
index 000000000..66c40a0d8
--- /dev/null
+++ b/src/vendor/symfony/routing/Loader/schema/routing/routing-1.0.xsd
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/vendor/symfony/routing/Matcher/CompiledUrlMatcher.php b/src/vendor/symfony/routing/Matcher/CompiledUrlMatcher.php
new file mode 100644
index 000000000..ae13fd701
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/CompiledUrlMatcher.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherTrait;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * Matches URLs based on rules dumped by CompiledUrlMatcherDumper.
+ *
+ * @author Nicolas Grekas
+ */
+class CompiledUrlMatcher extends UrlMatcher
+{
+ use CompiledUrlMatcherTrait;
+
+ public function __construct(array $compiledRoutes, RequestContext $context)
+ {
+ $this->context = $context;
+ [$this->matchHost, $this->staticRoutes, $this->regexpList, $this->dynamicRoutes, $this->checkCondition] = $compiledRoutes;
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/src/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php
new file mode 100644
index 000000000..ddf231f05
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php
@@ -0,0 +1,501 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * CompiledUrlMatcherDumper creates PHP arrays to be used with CompiledUrlMatcher.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ * @author Arnaud Le Blanc
+ * @author Nicolas Grekas
+ */
+class CompiledUrlMatcherDumper extends MatcherDumper
+{
+ private $expressionLanguage;
+ private $signalingException;
+
+ /**
+ * @var ExpressionFunctionProviderInterface[]
+ */
+ private $expressionLanguageProviders = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dump(array $options = [])
+ {
+ return <<generateCompiledRoutes()}];
+
+EOF;
+ }
+
+ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
+ {
+ $this->expressionLanguageProviders[] = $provider;
+ }
+
+ /**
+ * Generates the arrays for CompiledUrlMatcher's constructor.
+ */
+ public function getCompiledRoutes(bool $forDump = false): array
+ {
+ // Group hosts by same-suffix, re-order when possible
+ $matchHost = false;
+ $routes = new StaticPrefixCollection();
+ foreach ($this->getRoutes()->all() as $name => $route) {
+ if ($host = $route->getHost()) {
+ $matchHost = true;
+ $host = '/'.strtr(strrev($host), '}.{', '(/)');
+ }
+
+ $routes->addRoute($host ?: '/(.*)', [$name, $route]);
+ }
+
+ if ($matchHost) {
+ $compiledRoutes = [true];
+ $routes = $routes->populateCollection(new RouteCollection());
+ } else {
+ $compiledRoutes = [false];
+ $routes = $this->getRoutes();
+ }
+
+ [$staticRoutes, $dynamicRoutes] = $this->groupStaticRoutes($routes);
+
+ $conditions = [null];
+ $compiledRoutes[] = $this->compileStaticRoutes($staticRoutes, $conditions);
+ $chunkLimit = \count($dynamicRoutes);
+
+ while (true) {
+ try {
+ $this->signalingException = new \RuntimeException('Compilation failed: regular expression is too large');
+ $compiledRoutes = array_merge($compiledRoutes, $this->compileDynamicRoutes($dynamicRoutes, $matchHost, $chunkLimit, $conditions));
+
+ break;
+ } catch (\Exception $e) {
+ if (1 < $chunkLimit && $this->signalingException === $e) {
+ $chunkLimit = 1 + ($chunkLimit >> 1);
+ continue;
+ }
+ throw $e;
+ }
+ }
+
+ if ($forDump) {
+ $compiledRoutes[2] = $compiledRoutes[4];
+ }
+ unset($conditions[0]);
+
+ if ($conditions) {
+ foreach ($conditions as $expression => $condition) {
+ $conditions[$expression] = "case {$condition}: return {$expression};";
+ }
+
+ $checkConditionCode = <<indent(implode("\n", $conditions), 3)}
+ }
+ }
+EOF;
+ $compiledRoutes[4] = $forDump ? $checkConditionCode.",\n" : eval('return '.$checkConditionCode.';');
+ } else {
+ $compiledRoutes[4] = $forDump ? " null, // \$checkCondition\n" : null;
+ }
+
+ return $compiledRoutes;
+ }
+
+ private function generateCompiledRoutes(): string
+ {
+ [$matchHost, $staticRoutes, $regexpCode, $dynamicRoutes, $checkConditionCode] = $this->getCompiledRoutes(true);
+
+ $code = self::export($matchHost).', // $matchHost'."\n";
+
+ $code .= '[ // $staticRoutes'."\n";
+ foreach ($staticRoutes as $path => $routes) {
+ $code .= sprintf(" %s => [\n", self::export($path));
+ foreach ($routes as $route) {
+ $code .= vsprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", array_map([__CLASS__, 'export'], $route));
+ }
+ $code .= " ],\n";
+ }
+ $code .= "],\n";
+
+ $code .= sprintf("[ // \$regexpList%s\n],\n", $regexpCode);
+
+ $code .= '[ // $dynamicRoutes'."\n";
+ foreach ($dynamicRoutes as $path => $routes) {
+ $code .= sprintf(" %s => [\n", self::export($path));
+ foreach ($routes as $route) {
+ $code .= vsprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", array_map([__CLASS__, 'export'], $route));
+ }
+ $code .= " ],\n";
+ }
+ $code .= "],\n";
+ $code = preg_replace('/ => \[\n (\[.+?),\n \],/', ' => [$1],', $code);
+
+ return $this->indent($code, 1).$checkConditionCode;
+ }
+
+ /**
+ * Splits static routes from dynamic routes, so that they can be matched first, using a simple switch.
+ */
+ private function groupStaticRoutes(RouteCollection $collection): array
+ {
+ $staticRoutes = $dynamicRegex = [];
+ $dynamicRoutes = new RouteCollection();
+
+ foreach ($collection->all() as $name => $route) {
+ $compiledRoute = $route->compile();
+ $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
+ $hostRegex = $compiledRoute->getHostRegex();
+ $regex = $compiledRoute->getRegex();
+ if ($hasTrailingSlash = '/' !== $route->getPath()) {
+ $pos = strrpos($regex, '$');
+ $hasTrailingSlash = '/' === $regex[$pos - 1];
+ $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
+ }
+
+ if (!$compiledRoute->getPathVariables()) {
+ $host = !$compiledRoute->getHostVariables() ? $route->getHost() : '';
+ $url = $route->getPath();
+ if ($hasTrailingSlash) {
+ $url = substr($url, 0, -1);
+ }
+ foreach ($dynamicRegex as [$hostRx, $rx, $prefix]) {
+ if (('' === $prefix || str_starts_with($url, $prefix)) && (preg_match($rx, $url) || preg_match($rx, $url.'/')) && (!$host || !$hostRx || preg_match($hostRx, $host))) {
+ $dynamicRegex[] = [$hostRegex, $regex, $staticPrefix];
+ $dynamicRoutes->add($name, $route);
+ continue 2;
+ }
+ }
+
+ $staticRoutes[$url][$name] = [$route, $hasTrailingSlash];
+ } else {
+ $dynamicRegex[] = [$hostRegex, $regex, $staticPrefix];
+ $dynamicRoutes->add($name, $route);
+ }
+ }
+
+ return [$staticRoutes, $dynamicRoutes];
+ }
+
+ /**
+ * Compiles static routes in a switch statement.
+ *
+ * Condition-less paths are put in a static array in the switch's default, with generic matching logic.
+ * Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases.
+ *
+ * @throws \LogicException
+ */
+ private function compileStaticRoutes(array $staticRoutes, array &$conditions): array
+ {
+ if (!$staticRoutes) {
+ return [];
+ }
+ $compiledRoutes = [];
+
+ foreach ($staticRoutes as $url => $routes) {
+ $compiledRoutes[$url] = [];
+ foreach ($routes as $name => [$route, $hasTrailingSlash]) {
+ $compiledRoutes[$url][] = $this->compileRoute($route, $name, (!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex()) ?: null, $hasTrailingSlash, false, $conditions);
+ }
+ }
+
+ return $compiledRoutes;
+ }
+
+ /**
+ * Compiles a regular expression followed by a switch statement to match dynamic routes.
+ *
+ * The regular expression matches both the host and the pathinfo at the same time. For stellar performance,
+ * it is built as a tree of patterns, with re-ordering logic to group same-prefix routes together when possible.
+ *
+ * Patterns are named so that we know which one matched (https://pcre.org/current/doc/html/pcre2syntax.html#SEC23).
+ * This name is used to "switch" to the additional logic required to match the final route.
+ *
+ * Condition-less paths are put in a static array in the switch's default, with generic matching logic.
+ * Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases.
+ *
+ * Last but not least:
+ * - Because it is not possible to mix unicode/non-unicode patterns in a single regexp, several of them can be generated.
+ * - The same regexp can be used several times when the logic in the switch rejects the match. When this happens, the
+ * matching-but-failing subpattern is excluded by replacing its name by "(*F)", which forces a failure-to-match.
+ * To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur.
+ */
+ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHost, int $chunkLimit, array &$conditions): array
+ {
+ if (!$collection->all()) {
+ return [[], [], ''];
+ }
+ $regexpList = [];
+ $code = '';
+ $state = (object) [
+ 'regexMark' => 0,
+ 'regex' => [],
+ 'routes' => [],
+ 'mark' => 0,
+ 'markTail' => 0,
+ 'hostVars' => [],
+ 'vars' => [],
+ ];
+ $state->getVars = static function ($m) use ($state) {
+ if ('_route' === $m[1]) {
+ return '?:';
+ }
+
+ $state->vars[] = $m[1];
+
+ return '';
+ };
+
+ $chunkSize = 0;
+ $prev = null;
+ $perModifiers = [];
+ foreach ($collection->all() as $name => $route) {
+ preg_match('#[a-zA-Z]*$#', $route->compile()->getRegex(), $rx);
+ if ($chunkLimit < ++$chunkSize || $prev !== $rx[0] && $route->compile()->getPathVariables()) {
+ $chunkSize = 1;
+ $routes = new RouteCollection();
+ $perModifiers[] = [$rx[0], $routes];
+ $prev = $rx[0];
+ }
+ $routes->add($name, $route);
+ }
+
+ foreach ($perModifiers as [$modifiers, $routes]) {
+ $prev = false;
+ $perHost = [];
+ foreach ($routes->all() as $name => $route) {
+ $regex = $route->compile()->getHostRegex();
+ if ($prev !== $regex) {
+ $routes = new RouteCollection();
+ $perHost[] = [$regex, $routes];
+ $prev = $regex;
+ }
+ $routes->add($name, $route);
+ }
+ $prev = false;
+ $rx = '{^(?';
+ $code .= "\n {$state->mark} => ".self::export($rx);
+ $startingMark = $state->mark;
+ $state->mark += \strlen($rx);
+ $state->regex = $rx;
+
+ foreach ($perHost as [$hostRegex, $routes]) {
+ if ($matchHost) {
+ if ($hostRegex) {
+ preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $hostRegex, $rx);
+ $state->vars = [];
+ $hostRegex = '(?i:'.preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]).')\.';
+ $state->hostVars = $state->vars;
+ } else {
+ $hostRegex = '(?:(?:[^./]*+\.)++)';
+ $state->hostVars = [];
+ }
+ $state->mark += \strlen($rx = ($prev ? ')' : '')."|{$hostRegex}(?");
+ $code .= "\n .".self::export($rx);
+ $state->regex .= $rx;
+ $prev = true;
+ }
+
+ $tree = new StaticPrefixCollection();
+ foreach ($routes->all() as $name => $route) {
+ preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $route->compile()->getRegex(), $rx);
+
+ $state->vars = [];
+ $regex = preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]);
+ if ($hasTrailingSlash = '/' !== $regex && '/' === $regex[-1]) {
+ $regex = substr($regex, 0, -1);
+ }
+ $hasTrailingVar = (bool) preg_match('#\{\w+\}/?$#', $route->getPath());
+
+ $tree->addRoute($regex, [$name, $regex, $state->vars, $route, $hasTrailingSlash, $hasTrailingVar]);
+ }
+
+ $code .= $this->compileStaticPrefixCollection($tree, $state, 0, $conditions);
+ }
+ if ($matchHost) {
+ $code .= "\n .')'";
+ $state->regex .= ')';
+ }
+ $rx = ")/?$}{$modifiers}";
+ $code .= "\n .'{$rx}',";
+ $state->regex .= $rx;
+ $state->markTail = 0;
+
+ // if the regex is too large, throw a signaling exception to recompute with smaller chunk size
+ set_error_handler(function ($type, $message) { throw str_contains($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message); });
+ try {
+ preg_match($state->regex, '');
+ } finally {
+ restore_error_handler();
+ }
+
+ $regexpList[$startingMark] = $state->regex;
+ }
+
+ $state->routes[$state->mark][] = [null, null, null, null, false, false, 0];
+ unset($state->getVars);
+
+ return [$regexpList, $state->routes, $code];
+ }
+
+ /**
+ * Compiles a regexp tree of subpatterns that matches nested same-prefix routes.
+ *
+ * @param \stdClass $state A simple state object that keeps track of the progress of the compilation,
+ * and gathers the generated switch's "case" and "default" statements
+ */
+ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \stdClass $state, int $prefixLen, array &$conditions): string
+ {
+ $code = '';
+ $prevRegex = null;
+ $routes = $tree->getRoutes();
+
+ foreach ($routes as $i => $route) {
+ if ($route instanceof StaticPrefixCollection) {
+ $prevRegex = null;
+ $prefix = substr($route->getPrefix(), $prefixLen);
+ $state->mark += \strlen($rx = "|{$prefix}(?");
+ $code .= "\n .".self::export($rx);
+ $state->regex .= $rx;
+ $code .= $this->indent($this->compileStaticPrefixCollection($route, $state, $prefixLen + \strlen($prefix), $conditions));
+ $code .= "\n .')'";
+ $state->regex .= ')';
+ ++$state->markTail;
+ continue;
+ }
+
+ [$name, $regex, $vars, $route, $hasTrailingSlash, $hasTrailingVar] = $route;
+ $compiledRoute = $route->compile();
+ $vars = array_merge($state->hostVars, $vars);
+
+ if ($compiledRoute->getRegex() === $prevRegex) {
+ $state->routes[$state->mark][] = $this->compileRoute($route, $name, $vars, $hasTrailingSlash, $hasTrailingVar, $conditions);
+ continue;
+ }
+
+ $state->mark += 3 + $state->markTail + \strlen($regex) - $prefixLen;
+ $state->markTail = 2 + \strlen($state->mark);
+ $rx = sprintf('|%s(*:%s)', substr($regex, $prefixLen), $state->mark);
+ $code .= "\n .".self::export($rx);
+ $state->regex .= $rx;
+
+ $prevRegex = $compiledRoute->getRegex();
+ $state->routes[$state->mark] = [$this->compileRoute($route, $name, $vars, $hasTrailingSlash, $hasTrailingVar, $conditions)];
+ }
+
+ return $code;
+ }
+
+ /**
+ * Compiles a single Route to PHP code used to match it against the path info.
+ */
+ private function compileRoute(Route $route, string $name, $vars, bool $hasTrailingSlash, bool $hasTrailingVar, array &$conditions): array
+ {
+ $defaults = $route->getDefaults();
+
+ if (isset($defaults['_canonical_route'])) {
+ $name = $defaults['_canonical_route'];
+ unset($defaults['_canonical_route']);
+ }
+
+ if ($condition = $route->getCondition()) {
+ $condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request']);
+ $condition = $conditions[$condition] ?? $conditions[$condition] = (str_contains($condition, '$request') ? 1 : -1) * \count($conditions);
+ } else {
+ $condition = null;
+ }
+
+ return [
+ ['_route' => $name] + $defaults,
+ $vars,
+ array_flip($route->getMethods()) ?: null,
+ array_flip($route->getSchemes()) ?: null,
+ $hasTrailingSlash,
+ $hasTrailingVar,
+ $condition,
+ ];
+ }
+
+ private function getExpressionLanguage(): ExpressionLanguage
+ {
+ if (null === $this->expressionLanguage) {
+ if (!class_exists(ExpressionLanguage::class)) {
+ throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
+ }
+ $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
+ }
+
+ return $this->expressionLanguage;
+ }
+
+ private function indent(string $code, int $level = 1): string
+ {
+ return preg_replace('/^./m', str_repeat(' ', $level).'$0', $code);
+ }
+
+ /**
+ * @internal
+ */
+ public static function export($value): string
+ {
+ if (null === $value) {
+ return 'null';
+ }
+ if (!\is_array($value)) {
+ if (\is_object($value)) {
+ throw new \InvalidArgumentException('Symfony\Component\Routing\Route cannot contain objects.');
+ }
+
+ return str_replace("\n", '\'."\n".\'', var_export($value, true));
+ }
+ if (!$value) {
+ return '[]';
+ }
+
+ $i = 0;
+ $export = '[';
+
+ foreach ($value as $k => $v) {
+ if ($i === $k) {
+ ++$i;
+ } else {
+ $export .= self::export($k).' => ';
+
+ if (\is_int($k) && $i < $k) {
+ $i = 1 + $k;
+ }
+ }
+
+ $export .= self::export($v).', ';
+ }
+
+ return substr_replace($export, ']', -2);
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php b/src/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
new file mode 100644
index 000000000..bdb7ba3d0
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
@@ -0,0 +1,191 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\NoConfigurationException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ *
+ * @property RequestContext $context
+ */
+trait CompiledUrlMatcherTrait
+{
+ private $matchHost = false;
+ private $staticRoutes = [];
+ private $regexpList = [];
+ private $dynamicRoutes = [];
+
+ /**
+ * @var callable|null
+ */
+ private $checkCondition;
+
+ public function match(string $pathinfo): array
+ {
+ $allow = $allowSchemes = [];
+ if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
+ return $ret;
+ }
+ if ($allow) {
+ throw new MethodNotAllowedException(array_keys($allow));
+ }
+ if (!$this instanceof RedirectableUrlMatcherInterface) {
+ throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
+ }
+ if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) {
+ // no-op
+ } elseif ($allowSchemes) {
+ redirect_scheme:
+ $scheme = $this->context->getScheme();
+ $this->context->setScheme(key($allowSchemes));
+ try {
+ if ($ret = $this->doMatch($pathinfo)) {
+ return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
+ }
+ } finally {
+ $this->context->setScheme($scheme);
+ }
+ } elseif ('/' !== $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/') {
+ $pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo;
+ if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
+ return $this->redirect($pathinfo, $ret['_route']) + $ret;
+ }
+ if ($allowSchemes) {
+ goto redirect_scheme;
+ }
+ }
+
+ throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
+ }
+
+ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSchemes = []): array
+ {
+ $allow = $allowSchemes = [];
+ $pathinfo = rawurldecode($pathinfo) ?: '/';
+ $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
+ $context = $this->context;
+ $requestMethod = $canonicalMethod = $context->getMethod();
+
+ if ($this->matchHost) {
+ $host = strtolower($context->getHost());
+ }
+
+ if ('HEAD' === $requestMethod) {
+ $canonicalMethod = 'GET';
+ }
+ $supportsRedirections = 'GET' === $canonicalMethod && $this instanceof RedirectableUrlMatcherInterface;
+
+ foreach ($this->staticRoutes[$trimmedPathinfo] ?? [] as [$ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash, , $condition]) {
+ if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ?? $request = $this->request ?: $this->createRequest($pathinfo) : null)) {
+ continue;
+ }
+
+ if ($requiredHost) {
+ if ('{' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) {
+ continue;
+ }
+ if ('{' === $requiredHost[0] && $hostMatches) {
+ $hostMatches['_route'] = $ret['_route'];
+ $ret = $this->mergeDefaults($hostMatches, $ret);
+ }
+ }
+
+ if ('/' !== $pathinfo && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
+ if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
+ return $allow = $allowSchemes = [];
+ }
+ continue;
+ }
+
+ $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
+ if ($hasRequiredScheme && $requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
+ $allow += $requiredMethods;
+ continue;
+ }
+
+ if (!$hasRequiredScheme) {
+ $allowSchemes += $requiredSchemes;
+ continue;
+ }
+
+ return $ret;
+ }
+
+ $matchedPathinfo = $this->matchHost ? $host.'.'.$pathinfo : $pathinfo;
+
+ foreach ($this->regexpList as $offset => $regex) {
+ while (preg_match($regex, $matchedPathinfo, $matches)) {
+ foreach ($this->dynamicRoutes[$m = (int) $matches['MARK']] as [$ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash, $hasTrailingVar, $condition]) {
+ if (null !== $condition) {
+ if (0 === $condition) { // marks the last route in the regexp
+ continue 3;
+ }
+ if (!($this->checkCondition)($condition, $context, 0 < $condition ? $request ?? $request = $this->request ?: $this->createRequest($pathinfo) : null)) {
+ continue;
+ }
+ }
+
+ $hasTrailingVar = $trimmedPathinfo !== $pathinfo && $hasTrailingVar;
+
+ if ($hasTrailingVar && ($hasTrailingSlash || (null === $n = $matches[\count($vars)] ?? null) || '/' !== ($n[-1] ?? '/')) && preg_match($regex, $this->matchHost ? $host.'.'.$trimmedPathinfo : $trimmedPathinfo, $n) && $m === (int) $n['MARK']) {
+ if ($hasTrailingSlash) {
+ $matches = $n;
+ } else {
+ $hasTrailingVar = false;
+ }
+ }
+
+ if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
+ if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
+ return $allow = $allowSchemes = [];
+ }
+ continue;
+ }
+
+ foreach ($vars as $i => $v) {
+ if (isset($matches[1 + $i])) {
+ $ret[$v] = $matches[1 + $i];
+ }
+ }
+
+ if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
+ $allowSchemes += $requiredSchemes;
+ continue;
+ }
+
+ if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
+ $allow += $requiredMethods;
+ continue;
+ }
+
+ return $ret;
+ }
+
+ $regex = substr_replace($regex, 'F', $m - $offset, 1 + \strlen($m));
+ $offset += \strlen($m);
+ }
+ }
+
+ if ('/' === $pathinfo && !$allow && !$allowSchemes) {
+ throw new NoConfigurationException();
+ }
+
+ return [];
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/Dumper/MatcherDumper.php b/src/vendor/symfony/routing/Matcher/Dumper/MatcherDumper.php
new file mode 100644
index 000000000..ea51ab406
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/Dumper/MatcherDumper.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * MatcherDumper is the abstract class for all built-in matcher dumpers.
+ *
+ * @author Fabien Potencier
+ */
+abstract class MatcherDumper implements MatcherDumperInterface
+{
+ private $routes;
+
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php b/src/vendor/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php
new file mode 100644
index 000000000..8e33802d3
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * MatcherDumperInterface is the interface that all matcher dumper classes must implement.
+ *
+ * @author Fabien Potencier
+ */
+interface MatcherDumperInterface
+{
+ /**
+ * Dumps a set of routes to a string representation of executable code
+ * that can then be used to match a request against these routes.
+ *
+ * @return string
+ */
+ public function dump(array $options = []);
+
+ /**
+ * Gets the routes to dump.
+ *
+ * @return RouteCollection
+ */
+ public function getRoutes();
+}
diff --git a/src/vendor/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php b/src/vendor/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php
new file mode 100644
index 000000000..47d923c66
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php
@@ -0,0 +1,206 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Prefix tree of routes preserving routes order.
+ *
+ * @author Frank de Jonge
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class StaticPrefixCollection
+{
+ private $prefix;
+
+ /**
+ * @var string[]
+ */
+ private $staticPrefixes = [];
+
+ /**
+ * @var string[]
+ */
+ private $prefixes = [];
+
+ /**
+ * @var array[]|self[]
+ */
+ private $items = [];
+
+ public function __construct(string $prefix = '/')
+ {
+ $this->prefix = $prefix;
+ }
+
+ public function getPrefix(): string
+ {
+ return $this->prefix;
+ }
+
+ /**
+ * @return array[]|self[]
+ */
+ public function getRoutes(): array
+ {
+ return $this->items;
+ }
+
+ /**
+ * Adds a route to a group.
+ *
+ * @param array|self $route
+ */
+ public function addRoute(string $prefix, $route)
+ {
+ [$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix);
+
+ for ($i = \count($this->items) - 1; 0 <= $i; --$i) {
+ $item = $this->items[$i];
+
+ [$commonPrefix, $commonStaticPrefix] = $this->getCommonPrefix($prefix, $this->prefixes[$i]);
+
+ if ($this->prefix === $commonPrefix) {
+ // the new route and a previous one have no common prefix, let's see if they are exclusive to each others
+
+ if ($this->prefix !== $staticPrefix && $this->prefix !== $this->staticPrefixes[$i]) {
+ // the new route and the previous one have exclusive static prefixes
+ continue;
+ }
+
+ if ($this->prefix === $staticPrefix && $this->prefix === $this->staticPrefixes[$i]) {
+ // the new route and the previous one have no static prefix
+ break;
+ }
+
+ if ($this->prefixes[$i] !== $this->staticPrefixes[$i] && $this->prefix === $this->staticPrefixes[$i]) {
+ // the previous route is non-static and has no static prefix
+ break;
+ }
+
+ if ($prefix !== $staticPrefix && $this->prefix === $staticPrefix) {
+ // the new route is non-static and has no static prefix
+ break;
+ }
+
+ continue;
+ }
+
+ if ($item instanceof self && $this->prefixes[$i] === $commonPrefix) {
+ // the new route is a child of a previous one, let's nest it
+ $item->addRoute($prefix, $route);
+ } else {
+ // the new route and a previous one have a common prefix, let's merge them
+ $child = new self($commonPrefix);
+ [$child->prefixes[0], $child->staticPrefixes[0]] = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]);
+ [$child->prefixes[1], $child->staticPrefixes[1]] = $child->getCommonPrefix($prefix, $prefix);
+ $child->items = [$this->items[$i], $route];
+
+ $this->staticPrefixes[$i] = $commonStaticPrefix;
+ $this->prefixes[$i] = $commonPrefix;
+ $this->items[$i] = $child;
+ }
+
+ return;
+ }
+
+ // No optimised case was found, in this case we simple add the route for possible
+ // grouping when new routes are added.
+ $this->staticPrefixes[] = $staticPrefix;
+ $this->prefixes[] = $prefix;
+ $this->items[] = $route;
+ }
+
+ /**
+ * Linearizes back a set of nested routes into a collection.
+ */
+ public function populateCollection(RouteCollection $routes): RouteCollection
+ {
+ foreach ($this->items as $route) {
+ if ($route instanceof self) {
+ $route->populateCollection($routes);
+ } else {
+ $routes->add(...$route);
+ }
+ }
+
+ return $routes;
+ }
+
+ /**
+ * Gets the full and static common prefixes between two route patterns.
+ *
+ * The static prefix stops at last at the first opening bracket.
+ */
+ private function getCommonPrefix(string $prefix, string $anotherPrefix): array
+ {
+ $baseLength = \strlen($this->prefix);
+ $end = min(\strlen($prefix), \strlen($anotherPrefix));
+ $staticLength = null;
+ set_error_handler([__CLASS__, 'handleError']);
+
+ try {
+ for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
+ if ('(' === $prefix[$i]) {
+ $staticLength = $staticLength ?? $i;
+ for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) {
+ if ($prefix[$j] !== $anotherPrefix[$j]) {
+ break 2;
+ }
+ if ('(' === $prefix[$j]) {
+ ++$n;
+ } elseif (')' === $prefix[$j]) {
+ --$n;
+ } elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) {
+ --$j;
+ break;
+ }
+ }
+ if (0 < $n) {
+ break;
+ }
+ if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
+ break;
+ }
+ $subPattern = substr($prefix, $i, $j - $i);
+ if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) {
+ do {
+ // Prevent cutting in the middle of an UTF-8 characters
+ --$i;
+ } while (0b10 === (\ord($prefix[$i]) >> 6));
+ }
+
+ return [substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i)];
+ }
+
+ public static function handleError(int $type, string $msg)
+ {
+ return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length')
+ || str_contains($msg, 'Compilation failed: length of lookbehind assertion is not limited');
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/ExpressionLanguageProvider.php b/src/vendor/symfony/routing/Matcher/ExpressionLanguageProvider.php
new file mode 100644
index 000000000..96bb7babf
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/ExpressionLanguageProvider.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\ExpressionLanguage\ExpressionFunction;
+use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
+use Symfony\Contracts\Service\ServiceProviderInterface;
+
+/**
+ * Exposes functions defined in the request context to route conditions.
+ *
+ * @author Ahmed TAILOULOUTE
+ */
+class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface
+{
+ private $functions;
+
+ public function __construct(ServiceProviderInterface $functions)
+ {
+ $this->functions = $functions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFunctions()
+ {
+ $functions = [];
+
+ foreach ($this->functions->getProvidedServices() as $function => $type) {
+ $functions[] = new ExpressionFunction(
+ $function,
+ static function (...$args) use ($function) {
+ return sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args));
+ },
+ function ($values, ...$args) use ($function) {
+ return $values['context']->getParameter('_functions')->get($function)(...$args);
+ }
+ );
+ }
+
+ return $functions;
+ }
+
+ public function get(string $function): callable
+ {
+ return $this->functions->get($function);
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php b/src/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php
new file mode 100644
index 000000000..3cd7c81a6
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\ExceptionInterface;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+
+/**
+ * @author Fabien Potencier
+ */
+abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function match(string $pathinfo)
+ {
+ try {
+ return parent::match($pathinfo);
+ } catch (ResourceNotFoundException $e) {
+ if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) {
+ throw $e;
+ }
+
+ if ($this->allowSchemes) {
+ redirect_scheme:
+ $scheme = $this->context->getScheme();
+ $this->context->setScheme(current($this->allowSchemes));
+ try {
+ $ret = parent::match($pathinfo);
+
+ return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret;
+ } catch (ExceptionInterface $e2) {
+ throw $e;
+ } finally {
+ $this->context->setScheme($scheme);
+ }
+ } elseif ('/' === $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/') {
+ throw $e;
+ } else {
+ try {
+ $pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo;
+ $ret = parent::match($pathinfo);
+
+ return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
+ } catch (ExceptionInterface $e2) {
+ if ($this->allowSchemes) {
+ goto redirect_scheme;
+ }
+ throw $e;
+ }
+ }
+ }
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php b/src/vendor/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php
new file mode 100644
index 000000000..a43888b3e
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+/**
+ * RedirectableUrlMatcherInterface knows how to redirect the user.
+ *
+ * @author Fabien Potencier
+ */
+interface RedirectableUrlMatcherInterface
+{
+ /**
+ * Redirects the user to another URL and returns the parameters for the redirection.
+ *
+ * @param string $path The path info to redirect to
+ * @param string $route The route name that matched
+ * @param string|null $scheme The URL scheme (null to keep the current one)
+ *
+ * @return array
+ */
+ public function redirect(string $path, string $route, ?string $scheme = null);
+}
diff --git a/src/vendor/symfony/routing/Matcher/RequestMatcherInterface.php b/src/vendor/symfony/routing/Matcher/RequestMatcherInterface.php
new file mode 100644
index 000000000..c05016e82
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/RequestMatcherInterface.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\NoConfigurationException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+
+/**
+ * RequestMatcherInterface is the interface that all request matcher classes must implement.
+ *
+ * @author Fabien Potencier
+ */
+interface RequestMatcherInterface
+{
+ /**
+ * Tries to match a request with a set of routes.
+ *
+ * If the matcher cannot find information, it must throw one of the exceptions documented
+ * below.
+ *
+ * @return array
+ *
+ * @throws NoConfigurationException If no routing configuration could be found
+ * @throws ResourceNotFoundException If no matching resource could be found
+ * @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed
+ */
+ public function matchRequest(Request $request);
+}
diff --git a/src/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php b/src/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php
new file mode 100644
index 000000000..cddfe0254
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php
@@ -0,0 +1,164 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Exception\ExceptionInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * TraceableUrlMatcher helps debug path info matching by tracing the match.
+ *
+ * @author Fabien Potencier
+ */
+class TraceableUrlMatcher extends UrlMatcher
+{
+ public const ROUTE_DOES_NOT_MATCH = 0;
+ public const ROUTE_ALMOST_MATCHES = 1;
+ public const ROUTE_MATCHES = 2;
+
+ protected $traces;
+
+ public function getTraces(string $pathinfo)
+ {
+ $this->traces = [];
+
+ try {
+ $this->match($pathinfo);
+ } catch (ExceptionInterface $e) {
+ }
+
+ return $this->traces;
+ }
+
+ public function getTracesForRequest(Request $request)
+ {
+ $this->request = $request;
+ $traces = $this->getTraces($request->getPathInfo());
+ $this->request = null;
+
+ return $traces;
+ }
+
+ protected function matchCollection(string $pathinfo, RouteCollection $routes)
+ {
+ // HEAD and GET are equivalent as per RFC
+ if ('HEAD' === $method = $this->context->getMethod()) {
+ $method = 'GET';
+ }
+ $supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface;
+ $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
+
+ foreach ($routes as $name => $route) {
+ $compiledRoute = $route->compile();
+ $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
+ $requiredMethods = $route->getMethods();
+
+ // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
+ if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) {
+ $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
+ continue;
+ }
+ $regex = $compiledRoute->getRegex();
+
+ $pos = strrpos($regex, '$');
+ $hasTrailingSlash = '/' === $regex[$pos - 1];
+ $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
+
+ if (!preg_match($regex, $pathinfo, $matches)) {
+ // does it match without any requirements?
+ $r = new Route($route->getPath(), $route->getDefaults(), [], $route->getOptions());
+ $cr = $r->compile();
+ if (!preg_match($cr->getRegex(), $pathinfo)) {
+ $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
+
+ continue;
+ }
+
+ foreach ($route->getRequirements() as $n => $regex) {
+ $r = new Route($route->getPath(), $route->getDefaults(), [$n => $regex], $route->getOptions());
+ $cr = $r->compile();
+
+ if (\in_array($n, $cr->getVariables()) && !preg_match($cr->getRegex(), $pathinfo)) {
+ $this->addTrace(sprintf('Requirement for "%s" does not match (%s)', $n, $regex), self::ROUTE_ALMOST_MATCHES, $name, $route);
+
+ continue 2;
+ }
+ }
+
+ continue;
+ }
+
+ $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath());
+
+ if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) {
+ if ($hasTrailingSlash) {
+ $matches = $m;
+ } else {
+ $hasTrailingVar = false;
+ }
+ }
+
+ $hostMatches = [];
+ if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
+ $this->addTrace(sprintf('Host "%s" does not match the requirement ("%s")', $this->context->getHost(), $route->getHost()), self::ROUTE_ALMOST_MATCHES, $name, $route);
+ continue;
+ }
+
+ $status = $this->handleRouteRequirements($pathinfo, $name, $route);
+
+ if (self::REQUIREMENT_MISMATCH === $status[0]) {
+ $this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $route->getCondition()), self::ROUTE_ALMOST_MATCHES, $name, $route);
+ continue;
+ }
+
+ if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
+ if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) {
+ $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route);
+
+ return $this->allow = $this->allowSchemes = [];
+ }
+ $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
+ continue;
+ }
+
+ if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) {
+ $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
+ $this->addTrace(sprintf('Scheme "%s" does not match any of the required schemes (%s)', $this->context->getScheme(), implode(', ', $route->getSchemes())), self::ROUTE_ALMOST_MATCHES, $name, $route);
+ continue;
+ }
+
+ if ($requiredMethods && !\in_array($method, $requiredMethods)) {
+ $this->allow = array_merge($this->allow, $requiredMethods);
+ $this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route);
+ continue;
+ }
+
+ $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route);
+
+ return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? []));
+ }
+
+ return [];
+ }
+
+ private function addTrace(string $log, int $level = self::ROUTE_DOES_NOT_MATCH, ?string $name = null, ?Route $route = null)
+ {
+ $this->traces[] = [
+ 'log' => $log,
+ 'name' => $name,
+ 'level' => $level,
+ 'path' => null !== $route ? $route->getPath() : null,
+ ];
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/UrlMatcher.php b/src/vendor/symfony/routing/Matcher/UrlMatcher.php
new file mode 100644
index 000000000..f076a2f5e
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/UrlMatcher.php
@@ -0,0 +1,279 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\NoConfigurationException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * UrlMatcher matches URL based on a set of routes.
+ *
+ * @author Fabien Potencier
+ */
+class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
+{
+ public const REQUIREMENT_MATCH = 0;
+ public const REQUIREMENT_MISMATCH = 1;
+ public const ROUTE_MATCH = 2;
+
+ /** @var RequestContext */
+ protected $context;
+
+ /**
+ * Collects HTTP methods that would be allowed for the request.
+ */
+ protected $allow = [];
+
+ /**
+ * Collects URI schemes that would be allowed for the request.
+ *
+ * @internal
+ */
+ protected $allowSchemes = [];
+
+ protected $routes;
+ protected $request;
+ protected $expressionLanguage;
+
+ /**
+ * @var ExpressionFunctionProviderInterface[]
+ */
+ protected $expressionLanguageProviders = [];
+
+ public function __construct(RouteCollection $routes, RequestContext $context)
+ {
+ $this->routes = $routes;
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function match(string $pathinfo)
+ {
+ $this->allow = $this->allowSchemes = [];
+
+ if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) {
+ return $ret;
+ }
+
+ if ('/' === $pathinfo && !$this->allow && !$this->allowSchemes) {
+ throw new NoConfigurationException();
+ }
+
+ throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function matchRequest(Request $request)
+ {
+ $this->request = $request;
+
+ $ret = $this->match($request->getPathInfo());
+
+ $this->request = null;
+
+ return $ret;
+ }
+
+ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
+ {
+ $this->expressionLanguageProviders[] = $provider;
+ }
+
+ /**
+ * Tries to match a URL with a set of routes.
+ *
+ * @param string $pathinfo The path info to be parsed
+ *
+ * @return array
+ *
+ * @throws NoConfigurationException If no routing configuration could be found
+ * @throws ResourceNotFoundException If the resource could not be found
+ * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
+ */
+ protected function matchCollection(string $pathinfo, RouteCollection $routes)
+ {
+ // HEAD and GET are equivalent as per RFC
+ if ('HEAD' === $method = $this->context->getMethod()) {
+ $method = 'GET';
+ }
+ $supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface;
+ $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
+
+ foreach ($routes as $name => $route) {
+ $compiledRoute = $route->compile();
+ $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
+ $requiredMethods = $route->getMethods();
+
+ // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
+ if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) {
+ continue;
+ }
+ $regex = $compiledRoute->getRegex();
+
+ $pos = strrpos($regex, '$');
+ $hasTrailingSlash = '/' === $regex[$pos - 1];
+ $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
+
+ if (!preg_match($regex, $pathinfo, $matches)) {
+ continue;
+ }
+
+ $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath());
+
+ if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) {
+ if ($hasTrailingSlash) {
+ $matches = $m;
+ } else {
+ $hasTrailingVar = false;
+ }
+ }
+
+ $hostMatches = [];
+ if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
+ continue;
+ }
+
+ $status = $this->handleRouteRequirements($pathinfo, $name, $route);
+
+ if (self::REQUIREMENT_MISMATCH === $status[0]) {
+ continue;
+ }
+
+ if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
+ if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) {
+ return $this->allow = $this->allowSchemes = [];
+ }
+ continue;
+ }
+
+ if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) {
+ $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
+ continue;
+ }
+
+ if ($requiredMethods && !\in_array($method, $requiredMethods)) {
+ $this->allow = array_merge($this->allow, $requiredMethods);
+ continue;
+ }
+
+ return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? []));
+ }
+
+ return [];
+ }
+
+ /**
+ * Returns an array of values to use as request attributes.
+ *
+ * As this method requires the Route object, it is not available
+ * in matchers that do not have access to the matched Route instance
+ * (like the PHP and Apache matcher dumpers).
+ *
+ * @return array
+ */
+ protected function getAttributes(Route $route, string $name, array $attributes)
+ {
+ $defaults = $route->getDefaults();
+ if (isset($defaults['_canonical_route'])) {
+ $name = $defaults['_canonical_route'];
+ unset($defaults['_canonical_route']);
+ }
+ $attributes['_route'] = $name;
+
+ return $this->mergeDefaults($attributes, $defaults);
+ }
+
+ /**
+ * Handles specific route requirements.
+ *
+ * @return array The first element represents the status, the second contains additional information
+ */
+ protected function handleRouteRequirements(string $pathinfo, string $name, Route $route)
+ {
+ // expression condition
+ if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) {
+ return [self::REQUIREMENT_MISMATCH, null];
+ }
+
+ return [self::REQUIREMENT_MATCH, null];
+ }
+
+ /**
+ * Get merged default parameters.
+ *
+ * @return array
+ */
+ protected function mergeDefaults(array $params, array $defaults)
+ {
+ foreach ($params as $key => $value) {
+ if (!\is_int($key) && null !== $value) {
+ $defaults[$key] = $value;
+ }
+ }
+
+ return $defaults;
+ }
+
+ protected function getExpressionLanguage()
+ {
+ if (null === $this->expressionLanguage) {
+ if (!class_exists(ExpressionLanguage::class)) {
+ throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
+ }
+ $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
+ }
+
+ return $this->expressionLanguage;
+ }
+
+ /**
+ * @internal
+ */
+ protected function createRequest(string $pathinfo): ?Request
+ {
+ if (!class_exists(Request::class)) {
+ return null;
+ }
+
+ return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [
+ 'SCRIPT_FILENAME' => $this->context->getBaseUrl(),
+ 'SCRIPT_NAME' => $this->context->getBaseUrl(),
+ ]);
+ }
+}
diff --git a/src/vendor/symfony/routing/Matcher/UrlMatcherInterface.php b/src/vendor/symfony/routing/Matcher/UrlMatcherInterface.php
new file mode 100644
index 000000000..0a5be9744
--- /dev/null
+++ b/src/vendor/symfony/routing/Matcher/UrlMatcherInterface.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\NoConfigurationException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContextAwareInterface;
+
+/**
+ * UrlMatcherInterface is the interface that all URL matcher classes must implement.
+ *
+ * @author Fabien Potencier
+ */
+interface UrlMatcherInterface extends RequestContextAwareInterface
+{
+ /**
+ * Tries to match a URL path with a set of routes.
+ *
+ * If the matcher cannot find information, it must throw one of the exceptions documented
+ * below.
+ *
+ * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded)
+ *
+ * @return array
+ *
+ * @throws NoConfigurationException If no routing configuration could be found
+ * @throws ResourceNotFoundException If the resource could not be found
+ * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
+ */
+ public function match(string $pathinfo);
+}
diff --git a/src/vendor/symfony/routing/README.md b/src/vendor/symfony/routing/README.md
new file mode 100644
index 000000000..ae8284f54
--- /dev/null
+++ b/src/vendor/symfony/routing/README.md
@@ -0,0 +1,51 @@
+Routing Component
+=================
+
+The Routing component maps an HTTP request to a set of configuration variables.
+
+Getting Started
+---------------
+
+```
+$ composer require symfony/routing
+```
+
+```php
+use App\Controller\BlogController;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+$route = new Route('/blog/{slug}', ['_controller' => BlogController::class]);
+$routes = new RouteCollection();
+$routes->add('blog_show', $route);
+
+$context = new RequestContext();
+
+// Routing can match routes with incoming requests
+$matcher = new UrlMatcher($routes, $context);
+$parameters = $matcher->match('/blog/lorem-ipsum');
+// $parameters = [
+// '_controller' => 'App\Controller\BlogController',
+// 'slug' => 'lorem-ipsum',
+// '_route' => 'blog_show'
+// ]
+
+// Routing can also generate URLs for a given route
+$generator = new UrlGenerator($routes, $context);
+$url = $generator->generate('blog_show', [
+ 'slug' => 'my-blog-post',
+]);
+// $url = '/blog/my-blog-post'
+```
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/routing.html)
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
diff --git a/src/vendor/symfony/routing/RequestContext.php b/src/vendor/symfony/routing/RequestContext.php
new file mode 100644
index 000000000..f54c430ee
--- /dev/null
+++ b/src/vendor/symfony/routing/RequestContext.php
@@ -0,0 +1,327 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Holds information about the current request.
+ *
+ * This class implements a fluent interface.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+class RequestContext
+{
+ private $baseUrl;
+ private $pathInfo;
+ private $method;
+ private $host;
+ private $scheme;
+ private $httpPort;
+ private $httpsPort;
+ private $queryString;
+ private $parameters = [];
+
+ public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '')
+ {
+ $this->setBaseUrl($baseUrl);
+ $this->setMethod($method);
+ $this->setHost($host);
+ $this->setScheme($scheme);
+ $this->setHttpPort($httpPort);
+ $this->setHttpsPort($httpsPort);
+ $this->setPathInfo($path);
+ $this->setQueryString($queryString);
+ }
+
+ public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self
+ {
+ $uri = parse_url($uri);
+ $scheme = $uri['scheme'] ?? $scheme;
+ $host = $uri['host'] ?? $host;
+
+ if (isset($uri['port'])) {
+ if ('http' === $scheme) {
+ $httpPort = $uri['port'];
+ } elseif ('https' === $scheme) {
+ $httpsPort = $uri['port'];
+ }
+ }
+
+ return new self($uri['path'] ?? '', 'GET', $host, $scheme, $httpPort, $httpsPort);
+ }
+
+ /**
+ * Updates the RequestContext information based on a HttpFoundation Request.
+ *
+ * @return $this
+ */
+ public function fromRequest(Request $request)
+ {
+ $this->setBaseUrl($request->getBaseUrl());
+ $this->setPathInfo($request->getPathInfo());
+ $this->setMethod($request->getMethod());
+ $this->setHost($request->getHost());
+ $this->setScheme($request->getScheme());
+ $this->setHttpPort($request->isSecure() || null === $request->getPort() ? $this->httpPort : $request->getPort());
+ $this->setHttpsPort($request->isSecure() && null !== $request->getPort() ? $request->getPort() : $this->httpsPort);
+ $this->setQueryString($request->server->get('QUERY_STRING', ''));
+
+ return $this;
+ }
+
+ /**
+ * Gets the base URL.
+ *
+ * @return string
+ */
+ public function getBaseUrl()
+ {
+ return $this->baseUrl;
+ }
+
+ /**
+ * Sets the base URL.
+ *
+ * @return $this
+ */
+ public function setBaseUrl(string $baseUrl)
+ {
+ $this->baseUrl = rtrim($baseUrl, '/');
+
+ return $this;
+ }
+
+ /**
+ * Gets the path info.
+ *
+ * @return string
+ */
+ public function getPathInfo()
+ {
+ return $this->pathInfo;
+ }
+
+ /**
+ * Sets the path info.
+ *
+ * @return $this
+ */
+ public function setPathInfo(string $pathInfo)
+ {
+ $this->pathInfo = $pathInfo;
+
+ return $this;
+ }
+
+ /**
+ * Gets the HTTP method.
+ *
+ * The method is always an uppercased string.
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * Sets the HTTP method.
+ *
+ * @return $this
+ */
+ public function setMethod(string $method)
+ {
+ $this->method = strtoupper($method);
+
+ return $this;
+ }
+
+ /**
+ * Gets the HTTP host.
+ *
+ * The host is always lowercased because it must be treated case-insensitive.
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Sets the HTTP host.
+ *
+ * @return $this
+ */
+ public function setHost(string $host)
+ {
+ $this->host = strtolower($host);
+
+ return $this;
+ }
+
+ /**
+ * Gets the HTTP scheme.
+ *
+ * @return string
+ */
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ /**
+ * Sets the HTTP scheme.
+ *
+ * @return $this
+ */
+ public function setScheme(string $scheme)
+ {
+ $this->scheme = strtolower($scheme);
+
+ return $this;
+ }
+
+ /**
+ * Gets the HTTP port.
+ *
+ * @return int
+ */
+ public function getHttpPort()
+ {
+ return $this->httpPort;
+ }
+
+ /**
+ * Sets the HTTP port.
+ *
+ * @return $this
+ */
+ public function setHttpPort(int $httpPort)
+ {
+ $this->httpPort = $httpPort;
+
+ return $this;
+ }
+
+ /**
+ * Gets the HTTPS port.
+ *
+ * @return int
+ */
+ public function getHttpsPort()
+ {
+ return $this->httpsPort;
+ }
+
+ /**
+ * Sets the HTTPS port.
+ *
+ * @return $this
+ */
+ public function setHttpsPort(int $httpsPort)
+ {
+ $this->httpsPort = $httpsPort;
+
+ return $this;
+ }
+
+ /**
+ * Gets the query string without the "?".
+ *
+ * @return string
+ */
+ public function getQueryString()
+ {
+ return $this->queryString;
+ }
+
+ /**
+ * Sets the query string.
+ *
+ * @return $this
+ */
+ public function setQueryString(?string $queryString)
+ {
+ // string cast to be fault-tolerant, accepting null
+ $this->queryString = (string) $queryString;
+
+ return $this;
+ }
+
+ /**
+ * Returns the parameters.
+ *
+ * @return array
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Sets the parameters.
+ *
+ * @param array $parameters The parameters
+ *
+ * @return $this
+ */
+ public function setParameters(array $parameters)
+ {
+ $this->parameters = $parameters;
+
+ return $this;
+ }
+
+ /**
+ * Gets a parameter value.
+ *
+ * @return mixed
+ */
+ public function getParameter(string $name)
+ {
+ return $this->parameters[$name] ?? null;
+ }
+
+ /**
+ * Checks if a parameter value is set for the given parameter.
+ *
+ * @return bool
+ */
+ public function hasParameter(string $name)
+ {
+ return \array_key_exists($name, $this->parameters);
+ }
+
+ /**
+ * Sets a parameter value.
+ *
+ * @param mixed $parameter The parameter value
+ *
+ * @return $this
+ */
+ public function setParameter(string $name, $parameter)
+ {
+ $this->parameters[$name] = $parameter;
+
+ return $this;
+ }
+
+ public function isSecure(): bool
+ {
+ return 'https' === $this->scheme;
+ }
+}
diff --git a/src/vendor/symfony/routing/RequestContextAwareInterface.php b/src/vendor/symfony/routing/RequestContextAwareInterface.php
new file mode 100644
index 000000000..270a2b084
--- /dev/null
+++ b/src/vendor/symfony/routing/RequestContextAwareInterface.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+interface RequestContextAwareInterface
+{
+ /**
+ * Sets the request context.
+ */
+ public function setContext(RequestContext $context);
+
+ /**
+ * Gets the request context.
+ *
+ * @return RequestContext
+ */
+ public function getContext();
+}
diff --git a/src/vendor/symfony/routing/Route.php b/src/vendor/symfony/routing/Route.php
new file mode 100644
index 000000000..c67bd2de5
--- /dev/null
+++ b/src/vendor/symfony/routing/Route.php
@@ -0,0 +1,507 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * A Route describes a route and its parameters.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+class Route implements \Serializable
+{
+ private $path = '/';
+ private $host = '';
+ private $schemes = [];
+ private $methods = [];
+ private $defaults = [];
+ private $requirements = [];
+ private $options = [];
+ private $condition = '';
+
+ /**
+ * @var CompiledRoute|null
+ */
+ private $compiled;
+
+ /**
+ * Constructor.
+ *
+ * Available options:
+ *
+ * * compiler_class: A class name able to compile this route instance (RouteCompiler by default)
+ * * utf8: Whether UTF-8 matching is enforced ot not
+ *
+ * @param string $path The path pattern to match
+ * @param array $defaults An array of default parameter values
+ * @param array $requirements An array of requirements for parameters (regexes)
+ * @param array $options An array of options
+ * @param string|null $host The host pattern to match
+ * @param string|string[] $schemes A required URI scheme or an array of restricted schemes
+ * @param string|string[] $methods A required HTTP method or an array of restricted methods
+ * @param string|null $condition A condition that should evaluate to true for the route to match
+ */
+ public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', $schemes = [], $methods = [], ?string $condition = '')
+ {
+ $this->setPath($path);
+ $this->addDefaults($defaults);
+ $this->addRequirements($requirements);
+ $this->setOptions($options);
+ $this->setHost($host);
+ $this->setSchemes($schemes);
+ $this->setMethods($methods);
+ $this->setCondition($condition);
+ }
+
+ public function __serialize(): array
+ {
+ return [
+ 'path' => $this->path,
+ 'host' => $this->host,
+ 'defaults' => $this->defaults,
+ 'requirements' => $this->requirements,
+ 'options' => $this->options,
+ 'schemes' => $this->schemes,
+ 'methods' => $this->methods,
+ 'condition' => $this->condition,
+ 'compiled' => $this->compiled,
+ ];
+ }
+
+ /**
+ * @internal
+ */
+ final public function serialize(): string
+ {
+ return serialize($this->__serialize());
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->path = $data['path'];
+ $this->host = $data['host'];
+ $this->defaults = $data['defaults'];
+ $this->requirements = $data['requirements'];
+ $this->options = $data['options'];
+ $this->schemes = $data['schemes'];
+ $this->methods = $data['methods'];
+
+ if (isset($data['condition'])) {
+ $this->condition = $data['condition'];
+ }
+ if (isset($data['compiled'])) {
+ $this->compiled = $data['compiled'];
+ }
+ }
+
+ /**
+ * @internal
+ */
+ final public function unserialize($serialized)
+ {
+ $this->__unserialize(unserialize($serialized));
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setPath(string $pattern)
+ {
+ $pattern = $this->extractInlineDefaultsAndRequirements($pattern);
+
+ // A pattern must start with a slash and must not have multiple slashes at the beginning because the
+ // generated path for this route would be confused with a network path, e.g. '//domain.com/path'.
+ $this->path = '/'.ltrim(trim($pattern), '/');
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setHost(?string $pattern)
+ {
+ $this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern);
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the lowercased schemes this route is restricted to.
+ * So an empty array means that any scheme is allowed.
+ *
+ * @return string[]
+ */
+ public function getSchemes()
+ {
+ return $this->schemes;
+ }
+
+ /**
+ * Sets the schemes (e.g. 'https') this route is restricted to.
+ * So an empty array means that any scheme is allowed.
+ *
+ * @param string|string[] $schemes The scheme or an array of schemes
+ *
+ * @return $this
+ */
+ public function setSchemes($schemes)
+ {
+ $this->schemes = array_map('strtolower', (array) $schemes);
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Checks if a scheme requirement has been set.
+ *
+ * @return bool
+ */
+ public function hasScheme(string $scheme)
+ {
+ return \in_array(strtolower($scheme), $this->schemes, true);
+ }
+
+ /**
+ * Returns the uppercased HTTP methods this route is restricted to.
+ * So an empty array means that any method is allowed.
+ *
+ * @return string[]
+ */
+ public function getMethods()
+ {
+ return $this->methods;
+ }
+
+ /**
+ * Sets the HTTP methods (e.g. 'POST') this route is restricted to.
+ * So an empty array means that any method is allowed.
+ *
+ * @param string|string[] $methods The method or an array of methods
+ *
+ * @return $this
+ */
+ public function setMethods($methods)
+ {
+ $this->methods = array_map('strtoupper', (array) $methods);
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = [
+ 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
+ ];
+
+ return $this->addOptions($options);
+ }
+
+ /**
+ * @return $this
+ */
+ public function addOptions(array $options)
+ {
+ foreach ($options as $name => $option) {
+ $this->options[$name] = $option;
+ }
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Sets an option value.
+ *
+ * @param mixed $value The option value
+ *
+ * @return $this
+ */
+ public function setOption(string $name, $value)
+ {
+ $this->options[$name] = $value;
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the option value or null when not found.
+ *
+ * @return mixed
+ */
+ public function getOption(string $name)
+ {
+ return $this->options[$name] ?? null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasOption(string $name)
+ {
+ return \array_key_exists($name, $this->options);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDefaults()
+ {
+ return $this->defaults;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setDefaults(array $defaults)
+ {
+ $this->defaults = [];
+
+ return $this->addDefaults($defaults);
+ }
+
+ /**
+ * @return $this
+ */
+ public function addDefaults(array $defaults)
+ {
+ if (isset($defaults['_locale']) && $this->isLocalized()) {
+ unset($defaults['_locale']);
+ }
+
+ foreach ($defaults as $name => $default) {
+ $this->defaults[$name] = $default;
+ }
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getDefault(string $name)
+ {
+ return $this->defaults[$name] ?? null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasDefault(string $name)
+ {
+ return \array_key_exists($name, $this->defaults);
+ }
+
+ /**
+ * Sets a default value.
+ *
+ * @param mixed $default The default value
+ *
+ * @return $this
+ */
+ public function setDefault(string $name, $default)
+ {
+ if ('_locale' === $name && $this->isLocalized()) {
+ return $this;
+ }
+
+ $this->defaults[$name] = $default;
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getRequirements()
+ {
+ return $this->requirements;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setRequirements(array $requirements)
+ {
+ $this->requirements = [];
+
+ return $this->addRequirements($requirements);
+ }
+
+ /**
+ * @return $this
+ */
+ public function addRequirements(array $requirements)
+ {
+ if (isset($requirements['_locale']) && $this->isLocalized()) {
+ unset($requirements['_locale']);
+ }
+
+ foreach ($requirements as $key => $regex) {
+ $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
+ }
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getRequirement(string $key)
+ {
+ return $this->requirements[$key] ?? null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasRequirement(string $key)
+ {
+ return \array_key_exists($key, $this->requirements);
+ }
+
+ /**
+ * @return $this
+ */
+ public function setRequirement(string $key, string $regex)
+ {
+ if ('_locale' === $key && $this->isLocalized()) {
+ return $this;
+ }
+
+ $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCondition()
+ {
+ return $this->condition;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setCondition(?string $condition)
+ {
+ $this->condition = (string) $condition;
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Compiles the route.
+ *
+ * @return CompiledRoute
+ *
+ * @throws \LogicException If the Route cannot be compiled because the
+ * path or host pattern is invalid
+ *
+ * @see RouteCompiler which is responsible for the compilation process
+ */
+ public function compile()
+ {
+ if (null !== $this->compiled) {
+ return $this->compiled;
+ }
+
+ $class = $this->getOption('compiler_class');
+
+ return $this->compiled = $class::compile($this);
+ }
+
+ private function extractInlineDefaultsAndRequirements(string $pattern): string
+ {
+ if (false === strpbrk($pattern, '?<')) {
+ return $pattern;
+ }
+
+ return preg_replace_callback('#\{(!?)(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) {
+ if (isset($m[4][0])) {
+ $this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null);
+ }
+ if (isset($m[3][0])) {
+ $this->setRequirement($m[2], substr($m[3], 1, -1));
+ }
+
+ return '{'.$m[1].$m[2].'}';
+ }, $pattern);
+ }
+
+ private function sanitizeRequirement(string $key, string $regex)
+ {
+ if ('' !== $regex) {
+ if ('^' === $regex[0]) {
+ $regex = substr($regex, 1);
+ } elseif (0 === strpos($regex, '\\A')) {
+ $regex = substr($regex, 2);
+ }
+ }
+
+ if (str_ends_with($regex, '$')) {
+ $regex = substr($regex, 0, -1);
+ } elseif (\strlen($regex) - 2 === strpos($regex, '\\z')) {
+ $regex = substr($regex, 0, -2);
+ }
+
+ if ('' === $regex) {
+ throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key));
+ }
+
+ return $regex;
+ }
+
+ private function isLocalized(): bool
+ {
+ return isset($this->defaults['_locale']) && isset($this->defaults['_canonical_route']) && ($this->requirements['_locale'] ?? null) === preg_quote($this->defaults['_locale']);
+ }
+}
diff --git a/src/vendor/symfony/routing/RouteCollection.php b/src/vendor/symfony/routing/RouteCollection.php
new file mode 100644
index 000000000..95faead6e
--- /dev/null
+++ b/src/vendor/symfony/routing/RouteCollection.php
@@ -0,0 +1,403 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\Config\Resource\ResourceInterface;
+use Symfony\Component\Routing\Exception\InvalidArgumentException;
+use Symfony\Component\Routing\Exception\RouteCircularReferenceException;
+
+/**
+ * A RouteCollection represents a set of Route instances.
+ *
+ * When adding a route at the end of the collection, an existing route
+ * with the same name is removed first. So there can only be one route
+ * with a given name.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ *
+ * @implements \IteratorAggregate
+ */
+class RouteCollection implements \IteratorAggregate, \Countable
+{
+ /**
+ * @var array
+ */
+ private $routes = [];
+
+ /**
+ * @var array
+ */
+ private $aliases = [];
+
+ /**
+ * @var array
+ */
+ private $resources = [];
+
+ /**
+ * @var array
+ */
+ private $priorities = [];
+
+ public function __clone()
+ {
+ foreach ($this->routes as $name => $route) {
+ $this->routes[$name] = clone $route;
+ }
+
+ foreach ($this->aliases as $name => $alias) {
+ $this->aliases[$name] = clone $alias;
+ }
+ }
+
+ /**
+ * Gets the current RouteCollection as an Iterator that includes all routes.
+ *
+ * It implements \IteratorAggregate.
+ *
+ * @see all()
+ *
+ * @return \ArrayIterator
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->all());
+ }
+
+ /**
+ * Gets the number of Routes in this collection.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return \count($this->routes);
+ }
+
+ /**
+ * @param int $priority
+ */
+ public function add(string $name, Route $route/* , int $priority = 0 */)
+ {
+ if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
+ trigger_deprecation('symfony/routing', '5.1', 'The "%s()" method will have a new "int $priority = 0" argument in version 6.0, not defining it is deprecated.', __METHOD__);
+ }
+
+ unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
+
+ $this->routes[$name] = $route;
+
+ if ($priority = 3 <= \func_num_args() ? func_get_arg(2) : 0) {
+ $this->priorities[$name] = $priority;
+ }
+ }
+
+ /**
+ * Returns all routes in this collection.
+ *
+ * @return array
+ */
+ public function all()
+ {
+ if ($this->priorities) {
+ $priorities = $this->priorities;
+ $keysOrder = array_flip(array_keys($this->routes));
+ uksort($this->routes, static function ($n1, $n2) use ($priorities, $keysOrder) {
+ return (($priorities[$n2] ?? 0) <=> ($priorities[$n1] ?? 0)) ?: ($keysOrder[$n1] <=> $keysOrder[$n2]);
+ });
+ }
+
+ return $this->routes;
+ }
+
+ /**
+ * Gets a route by name.
+ *
+ * @return Route|null
+ */
+ public function get(string $name)
+ {
+ $visited = [];
+ while (null !== $alias = $this->aliases[$name] ?? null) {
+ if (false !== $searchKey = array_search($name, $visited)) {
+ $visited[] = $name;
+
+ throw new RouteCircularReferenceException($name, \array_slice($visited, $searchKey));
+ }
+
+ if ($alias->isDeprecated()) {
+ $deprecation = $alias->getDeprecation($name);
+
+ trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
+ }
+
+ $visited[] = $name;
+ $name = $alias->getId();
+ }
+
+ return $this->routes[$name] ?? null;
+ }
+
+ /**
+ * Removes a route or an array of routes by name from the collection.
+ *
+ * @param string|string[] $name The route name or an array of route names
+ */
+ public function remove($name)
+ {
+ $routes = [];
+ foreach ((array) $name as $n) {
+ if (isset($this->routes[$n])) {
+ $routes[] = $n;
+ }
+
+ unset($this->routes[$n], $this->priorities[$n], $this->aliases[$n]);
+ }
+
+ if (!$routes) {
+ return;
+ }
+
+ foreach ($this->aliases as $k => $alias) {
+ if (\in_array($alias->getId(), $routes, true)) {
+ unset($this->aliases[$k]);
+ }
+ }
+ }
+
+ /**
+ * Adds a route collection at the end of the current set by appending all
+ * routes of the added collection.
+ */
+ public function addCollection(self $collection)
+ {
+ // we need to remove all routes with the same names first because just replacing them
+ // would not place the new route at the end of the merged array
+ foreach ($collection->all() as $name => $route) {
+ unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
+ $this->routes[$name] = $route;
+
+ if (isset($collection->priorities[$name])) {
+ $this->priorities[$name] = $collection->priorities[$name];
+ }
+ }
+
+ foreach ($collection->getAliases() as $name => $alias) {
+ unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
+
+ $this->aliases[$name] = $alias;
+ }
+
+ foreach ($collection->getResources() as $resource) {
+ $this->addResource($resource);
+ }
+ }
+
+ /**
+ * Adds a prefix to the path of all child routes.
+ */
+ public function addPrefix(string $prefix, array $defaults = [], array $requirements = [])
+ {
+ $prefix = trim(trim($prefix), '/');
+
+ if ('' === $prefix) {
+ return;
+ }
+
+ foreach ($this->routes as $route) {
+ $route->setPath('/'.$prefix.$route->getPath());
+ $route->addDefaults($defaults);
+ $route->addRequirements($requirements);
+ }
+ }
+
+ /**
+ * Adds a prefix to the name of all the routes within in the collection.
+ */
+ public function addNamePrefix(string $prefix)
+ {
+ $prefixedRoutes = [];
+ $prefixedPriorities = [];
+ $prefixedAliases = [];
+
+ foreach ($this->routes as $name => $route) {
+ $prefixedRoutes[$prefix.$name] = $route;
+ if (null !== $canonicalName = $route->getDefault('_canonical_route')) {
+ $route->setDefault('_canonical_route', $prefix.$canonicalName);
+ }
+ if (isset($this->priorities[$name])) {
+ $prefixedPriorities[$prefix.$name] = $this->priorities[$name];
+ }
+ }
+
+ foreach ($this->aliases as $name => $alias) {
+ $prefixedAliases[$prefix.$name] = $alias->withId($prefix.$alias->getId());
+ }
+
+ $this->routes = $prefixedRoutes;
+ $this->priorities = $prefixedPriorities;
+ $this->aliases = $prefixedAliases;
+ }
+
+ /**
+ * Sets the host pattern on all routes.
+ */
+ public function setHost(?string $pattern, array $defaults = [], array $requirements = [])
+ {
+ foreach ($this->routes as $route) {
+ $route->setHost($pattern);
+ $route->addDefaults($defaults);
+ $route->addRequirements($requirements);
+ }
+ }
+
+ /**
+ * Sets a condition on all routes.
+ *
+ * Existing conditions will be overridden.
+ */
+ public function setCondition(?string $condition)
+ {
+ foreach ($this->routes as $route) {
+ $route->setCondition($condition);
+ }
+ }
+
+ /**
+ * Adds defaults to all routes.
+ *
+ * An existing default value under the same name in a route will be overridden.
+ */
+ public function addDefaults(array $defaults)
+ {
+ if ($defaults) {
+ foreach ($this->routes as $route) {
+ $route->addDefaults($defaults);
+ }
+ }
+ }
+
+ /**
+ * Adds requirements to all routes.
+ *
+ * An existing requirement under the same name in a route will be overridden.
+ */
+ public function addRequirements(array $requirements)
+ {
+ if ($requirements) {
+ foreach ($this->routes as $route) {
+ $route->addRequirements($requirements);
+ }
+ }
+ }
+
+ /**
+ * Adds options to all routes.
+ *
+ * An existing option value under the same name in a route will be overridden.
+ */
+ public function addOptions(array $options)
+ {
+ if ($options) {
+ foreach ($this->routes as $route) {
+ $route->addOptions($options);
+ }
+ }
+ }
+
+ /**
+ * Sets the schemes (e.g. 'https') all child routes are restricted to.
+ *
+ * @param string|string[] $schemes The scheme or an array of schemes
+ */
+ public function setSchemes($schemes)
+ {
+ foreach ($this->routes as $route) {
+ $route->setSchemes($schemes);
+ }
+ }
+
+ /**
+ * Sets the HTTP methods (e.g. 'POST') all child routes are restricted to.
+ *
+ * @param string|string[] $methods The method or an array of methods
+ */
+ public function setMethods($methods)
+ {
+ foreach ($this->routes as $route) {
+ $route->setMethods($methods);
+ }
+ }
+
+ /**
+ * Returns an array of resources loaded to build this collection.
+ *
+ * @return ResourceInterface[]
+ */
+ public function getResources()
+ {
+ return array_values($this->resources);
+ }
+
+ /**
+ * Adds a resource for this collection. If the resource already exists
+ * it is not added.
+ */
+ public function addResource(ResourceInterface $resource)
+ {
+ $key = (string) $resource;
+
+ if (!isset($this->resources[$key])) {
+ $this->resources[$key] = $resource;
+ }
+ }
+
+ /**
+ * Sets an alias for an existing route.
+ *
+ * @param string $name The alias to create
+ * @param string $alias The route to alias
+ *
+ * @throws InvalidArgumentException if the alias is for itself
+ */
+ public function addAlias(string $name, string $alias): Alias
+ {
+ if ($name === $alias) {
+ throw new InvalidArgumentException(sprintf('Route alias "%s" can not reference itself.', $name));
+ }
+
+ unset($this->routes[$name], $this->priorities[$name]);
+
+ return $this->aliases[$name] = new Alias($alias);
+ }
+
+ /**
+ * @return array
+ */
+ public function getAliases(): array
+ {
+ return $this->aliases;
+ }
+
+ public function getAlias(string $name): ?Alias
+ {
+ return $this->aliases[$name] ?? null;
+ }
+
+ public function getPriority(string $name): ?int
+ {
+ return $this->priorities[$name] ?? null;
+ }
+}
diff --git a/src/vendor/symfony/routing/RouteCollectionBuilder.php b/src/vendor/symfony/routing/RouteCollectionBuilder.php
new file mode 100644
index 000000000..04a443972
--- /dev/null
+++ b/src/vendor/symfony/routing/RouteCollectionBuilder.php
@@ -0,0 +1,364 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\Config\Exception\LoaderLoadException;
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\Resource\ResourceInterface;
+use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
+
+trigger_deprecation('symfony/routing', '5.1', 'The "%s" class is deprecated, use "%s" instead.', RouteCollectionBuilder::class, RoutingConfigurator::class);
+
+/**
+ * Helps add and import routes into a RouteCollection.
+ *
+ * @author Ryan Weaver
+ *
+ * @deprecated since Symfony 5.1, use RoutingConfigurator instead
+ */
+class RouteCollectionBuilder
+{
+ /**
+ * @var Route[]|RouteCollectionBuilder[]
+ */
+ private $routes = [];
+
+ private $loader;
+ private $defaults = [];
+ private $prefix;
+ private $host;
+ private $condition;
+ private $requirements = [];
+ private $options = [];
+ private $schemes;
+ private $methods;
+ private $resources = [];
+
+ public function __construct(?LoaderInterface $loader = null)
+ {
+ $this->loader = $loader;
+ }
+
+ /**
+ * Import an external routing resource and returns the RouteCollectionBuilder.
+ *
+ * $routes->import('blog.yml', '/blog');
+ *
+ * @param mixed $resource
+ *
+ * @return self
+ *
+ * @throws LoaderLoadException
+ */
+ public function import($resource, string $prefix = '/', ?string $type = null)
+ {
+ /** @var RouteCollection[] $collections */
+ $collections = $this->load($resource, $type);
+
+ // create a builder from the RouteCollection
+ $builder = $this->createBuilder();
+
+ foreach ($collections as $collection) {
+ if (null === $collection) {
+ continue;
+ }
+
+ foreach ($collection->all() as $name => $route) {
+ $builder->addRoute($route, $name);
+ }
+
+ foreach ($collection->getResources() as $resource) {
+ $builder->addResource($resource);
+ }
+ }
+
+ // mount into this builder
+ $this->mount($prefix, $builder);
+
+ return $builder;
+ }
+
+ /**
+ * Adds a route and returns it for future modification.
+ *
+ * @return Route
+ */
+ public function add(string $path, string $controller, ?string $name = null)
+ {
+ $route = new Route($path);
+ $route->setDefault('_controller', $controller);
+ $this->addRoute($route, $name);
+
+ return $route;
+ }
+
+ /**
+ * Returns a RouteCollectionBuilder that can be configured and then added with mount().
+ *
+ * @return self
+ */
+ public function createBuilder()
+ {
+ return new self($this->loader);
+ }
+
+ /**
+ * Add a RouteCollectionBuilder.
+ */
+ public function mount(string $prefix, self $builder)
+ {
+ $builder->prefix = trim(trim($prefix), '/');
+ $this->routes[] = $builder;
+ }
+
+ /**
+ * Adds a Route object to the builder.
+ *
+ * @return $this
+ */
+ public function addRoute(Route $route, ?string $name = null)
+ {
+ if (null === $name) {
+ // used as a flag to know which routes will need a name later
+ $name = '_unnamed_route_'.spl_object_hash($route);
+ }
+
+ $this->routes[$name] = $route;
+
+ return $this;
+ }
+
+ /**
+ * Sets the host on all embedded routes (unless already set).
+ *
+ * @return $this
+ */
+ public function setHost(?string $pattern)
+ {
+ $this->host = $pattern;
+
+ return $this;
+ }
+
+ /**
+ * Sets a condition on all embedded routes (unless already set).
+ *
+ * @return $this
+ */
+ public function setCondition(?string $condition)
+ {
+ $this->condition = $condition;
+
+ return $this;
+ }
+
+ /**
+ * Sets a default value that will be added to all embedded routes (unless that
+ * default value is already set).
+ *
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setDefault(string $key, $value)
+ {
+ $this->defaults[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Sets a requirement that will be added to all embedded routes (unless that
+ * requirement is already set).
+ *
+ * @param mixed $regex
+ *
+ * @return $this
+ */
+ public function setRequirement(string $key, $regex)
+ {
+ $this->requirements[$key] = $regex;
+
+ return $this;
+ }
+
+ /**
+ * Sets an option that will be added to all embedded routes (unless that
+ * option is already set).
+ *
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setOption(string $key, $value)
+ {
+ $this->options[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Sets the schemes on all embedded routes (unless already set).
+ *
+ * @param array|string $schemes
+ *
+ * @return $this
+ */
+ public function setSchemes($schemes)
+ {
+ $this->schemes = $schemes;
+
+ return $this;
+ }
+
+ /**
+ * Sets the methods on all embedded routes (unless already set).
+ *
+ * @param array|string $methods
+ *
+ * @return $this
+ */
+ public function setMethods($methods)
+ {
+ $this->methods = $methods;
+
+ return $this;
+ }
+
+ /**
+ * Adds a resource for this collection.
+ *
+ * @return $this
+ */
+ private function addResource(ResourceInterface $resource): self
+ {
+ $this->resources[] = $resource;
+
+ return $this;
+ }
+
+ /**
+ * Creates the final RouteCollection and returns it.
+ *
+ * @return RouteCollection
+ */
+ public function build()
+ {
+ $routeCollection = new RouteCollection();
+
+ foreach ($this->routes as $name => $route) {
+ if ($route instanceof Route) {
+ $route->setDefaults(array_merge($this->defaults, $route->getDefaults()));
+ $route->setOptions(array_merge($this->options, $route->getOptions()));
+
+ foreach ($this->requirements as $key => $val) {
+ if (!$route->hasRequirement($key)) {
+ $route->setRequirement($key, $val);
+ }
+ }
+
+ if (null !== $this->prefix) {
+ $route->setPath('/'.$this->prefix.$route->getPath());
+ }
+
+ if (!$route->getHost()) {
+ $route->setHost($this->host);
+ }
+
+ if (!$route->getCondition()) {
+ $route->setCondition($this->condition);
+ }
+
+ if (!$route->getSchemes()) {
+ $route->setSchemes($this->schemes);
+ }
+
+ if (!$route->getMethods()) {
+ $route->setMethods($this->methods);
+ }
+
+ // auto-generate the route name if it's been marked
+ if ('_unnamed_route_' === substr($name, 0, 15)) {
+ $name = $this->generateRouteName($route);
+ }
+
+ $routeCollection->add($name, $route);
+ } else {
+ /* @var self $route */
+ $subCollection = $route->build();
+ if (null !== $this->prefix) {
+ $subCollection->addPrefix($this->prefix);
+ }
+
+ $routeCollection->addCollection($subCollection);
+ }
+ }
+
+ foreach ($this->resources as $resource) {
+ $routeCollection->addResource($resource);
+ }
+
+ return $routeCollection;
+ }
+
+ /**
+ * Generates a route name based on details of this route.
+ */
+ private function generateRouteName(Route $route): string
+ {
+ $methods = implode('_', $route->getMethods()).'_';
+
+ $routeName = $methods.$route->getPath();
+ $routeName = str_replace(['/', ':', '|', '-'], '_', $routeName);
+ $routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
+
+ // Collapse consecutive underscores down into a single underscore.
+ $routeName = preg_replace('/_+/', '_', $routeName);
+
+ return $routeName;
+ }
+
+ /**
+ * Finds a loader able to load an imported resource and loads it.
+ *
+ * @param mixed $resource A resource
+ * @param string|null $type The resource type or null if unknown
+ *
+ * @return RouteCollection[]
+ *
+ * @throws LoaderLoadException If no loader is found
+ */
+ private function load($resource, ?string $type = null): array
+ {
+ if (null === $this->loader) {
+ throw new \BadMethodCallException('Cannot import other routing resources: you must pass a LoaderInterface when constructing RouteCollectionBuilder.');
+ }
+
+ if ($this->loader->supports($resource, $type)) {
+ $collections = $this->loader->load($resource, $type);
+
+ return \is_array($collections) ? $collections : [$collections];
+ }
+
+ if (null === $resolver = $this->loader->getResolver()) {
+ throw new LoaderLoadException($resource, null, 0, null, $type);
+ }
+
+ if (false === $loader = $resolver->resolve($resource, $type)) {
+ throw new LoaderLoadException($resource, null, 0, null, $type);
+ }
+
+ $collections = $loader->load($resource, $type);
+
+ return \is_array($collections) ? $collections : [$collections];
+ }
+}
diff --git a/src/vendor/symfony/routing/RouteCompiler.php b/src/vendor/symfony/routing/RouteCompiler.php
new file mode 100644
index 000000000..7e78c2931
--- /dev/null
+++ b/src/vendor/symfony/routing/RouteCompiler.php
@@ -0,0 +1,346 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * RouteCompiler compiles Route instances to CompiledRoute instances.
+ *
+ * @author Fabien Potencier
+ * @author Tobias Schultze
+ */
+class RouteCompiler implements RouteCompilerInterface
+{
+ /**
+ * @deprecated since Symfony 5.1, to be removed in 6.0
+ */
+ public const REGEX_DELIMITER = '#';
+
+ /**
+ * This string defines the characters that are automatically considered separators in front of
+ * optional placeholders (with default and no static text following). Such a single separator
+ * can be left out together with the optional placeholder from matching and generating URLs.
+ */
+ public const SEPARATORS = '/,;.:-_~+*=@|';
+
+ /**
+ * The maximum supported length of a PCRE subpattern name
+ * http://pcre.org/current/doc/html/pcre2pattern.html#SEC16.
+ *
+ * @internal
+ */
+ public const VARIABLE_MAXIMUM_LENGTH = 32;
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws \InvalidArgumentException if a path variable is named _fragment
+ * @throws \LogicException if a variable is referenced more than once
+ * @throws \DomainException if a variable name starts with a digit or if it is too long to be successfully used as
+ * a PCRE subpattern
+ */
+ public static function compile(Route $route)
+ {
+ $hostVariables = [];
+ $variables = [];
+ $hostRegex = null;
+ $hostTokens = [];
+
+ if ('' !== $host = $route->getHost()) {
+ $result = self::compilePattern($route, $host, true);
+
+ $hostVariables = $result['variables'];
+ $variables = $hostVariables;
+
+ $hostTokens = $result['tokens'];
+ $hostRegex = $result['regex'];
+ }
+
+ $locale = $route->getDefault('_locale');
+ if (null !== $locale && null !== $route->getDefault('_canonical_route') && preg_quote($locale) === $route->getRequirement('_locale')) {
+ $requirements = $route->getRequirements();
+ unset($requirements['_locale']);
+ $route->setRequirements($requirements);
+ $route->setPath(str_replace('{_locale}', $locale, $route->getPath()));
+ }
+
+ $path = $route->getPath();
+
+ $result = self::compilePattern($route, $path, false);
+
+ $staticPrefix = $result['staticPrefix'];
+
+ $pathVariables = $result['variables'];
+
+ foreach ($pathVariables as $pathParam) {
+ if ('_fragment' === $pathParam) {
+ throw new \InvalidArgumentException(sprintf('Route pattern "%s" cannot contain "_fragment" as a path parameter.', $route->getPath()));
+ }
+ }
+
+ $variables = array_merge($variables, $pathVariables);
+
+ $tokens = $result['tokens'];
+ $regex = $result['regex'];
+
+ return new CompiledRoute(
+ $staticPrefix,
+ $regex,
+ $tokens,
+ $pathVariables,
+ $hostRegex,
+ $hostTokens,
+ $hostVariables,
+ array_unique($variables)
+ );
+ }
+
+ private static function compilePattern(Route $route, string $pattern, bool $isHost): array
+ {
+ $tokens = [];
+ $variables = [];
+ $matches = [];
+ $pos = 0;
+ $defaultSeparator = $isHost ? '.' : '/';
+ $useUtf8 = preg_match('//u', $pattern);
+ $needsUtf8 = $route->getOption('utf8');
+
+ if (!$needsUtf8 && $useUtf8 && preg_match('/[\x80-\xFF]/', $pattern)) {
+ throw new \LogicException(sprintf('Cannot use UTF-8 route patterns without setting the "utf8" option for route "%s".', $route->getPath()));
+ }
+ if (!$useUtf8 && $needsUtf8) {
+ throw new \LogicException(sprintf('Cannot mix UTF-8 requirements with non-UTF-8 pattern "%s".', $pattern));
+ }
+
+ // Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
+ // in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
+ preg_match_all('#\{(!)?(\w+)\}#', $pattern, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ $important = $match[1][1] >= 0;
+ $varName = $match[2][0];
+ // get all static text preceding the current variable
+ $precedingText = substr($pattern, $pos, $match[0][1] - $pos);
+ $pos = $match[0][1] + \strlen($match[0][0]);
+
+ if (!\strlen($precedingText)) {
+ $precedingChar = '';
+ } elseif ($useUtf8) {
+ preg_match('/.$/u', $precedingText, $precedingChar);
+ $precedingChar = $precedingChar[0];
+ } else {
+ $precedingChar = substr($precedingText, -1);
+ }
+ $isSeparator = '' !== $precedingChar && str_contains(static::SEPARATORS, $precedingChar);
+
+ // A PCRE subpattern name must start with a non-digit. Also a PHP variable cannot start with a digit so the
+ // variable would not be usable as a Controller action argument.
+ if (preg_match('/^\d/', $varName)) {
+ throw new \DomainException(sprintf('Variable name "%s" cannot start with a digit in route pattern "%s". Please use a different name.', $varName, $pattern));
+ }
+ if (\in_array($varName, $variables)) {
+ throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
+ }
+
+ if (\strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
+ throw new \DomainException(sprintf('Variable name "%s" cannot be longer than %d characters in route pattern "%s". Please use a shorter name.', $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern));
+ }
+
+ if ($isSeparator && $precedingText !== $precedingChar) {
+ $tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))];
+ } elseif (!$isSeparator && '' !== $precedingText) {
+ $tokens[] = ['text', $precedingText];
+ }
+
+ $regexp = $route->getRequirement($varName);
+ if (null === $regexp) {
+ $followingPattern = (string) substr($pattern, $pos);
+ // Find the next static character after the variable that functions as a separator. By default, this separator and '/'
+ // are disallowed for the variable. This default requirement makes sure that optional variables can be matched at all
+ // and that the generating-matching-combination of URLs unambiguous, i.e. the params used for generating the URL are
+ // the same that will be matched. Example: new Route('/{page}.{_format}', ['_format' => 'html'])
+ // If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything.
+ // Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
+ // part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
+ $nextSeparator = self::findNextSeparator($followingPattern, $useUtf8);
+ $regexp = sprintf(
+ '[^%s%s]+',
+ preg_quote($defaultSeparator),
+ $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator) : ''
+ );
+ if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {
+ // When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
+ // quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns.
+ // Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow
+ // after it. This optimization cannot be applied when the next char is no real separator or when the next variable is
+ // directly adjacent, e.g. '/{x}{y}'.
+ $regexp .= '+';
+ }
+ } else {
+ if (!preg_match('//u', $regexp)) {
+ $useUtf8 = false;
+ } elseif (!$needsUtf8 && preg_match('/[\x80-\xFF]|(?= 0; --$i) {
+ $token = $tokens[$i];
+ // variable is optional when it is not important and has a default value
+ if ('variable' === $token[0] && !($token[5] ?? false) && $route->hasDefault($token[3])) {
+ $firstOptional = $i;
+ } else {
+ break;
+ }
+ }
+ }
+
+ // compute the matching regexp
+ $regexp = '';
+ for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) {
+ $regexp .= self::computeRegexp($tokens, $i, $firstOptional);
+ }
+ $regexp = '{^'.$regexp.'$}sD'.($isHost ? 'i' : '');
+
+ // enable Utf8 matching if really required
+ if ($needsUtf8) {
+ $regexp .= 'u';
+ for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) {
+ if ('variable' === $tokens[$i][0]) {
+ $tokens[$i][4] = true;
+ }
+ }
+ }
+
+ return [
+ 'staticPrefix' => self::determineStaticPrefix($route, $tokens),
+ 'regex' => $regexp,
+ 'tokens' => array_reverse($tokens),
+ 'variables' => $variables,
+ ];
+ }
+
+ /**
+ * Determines the longest static prefix possible for a route.
+ */
+ private static function determineStaticPrefix(Route $route, array $tokens): string
+ {
+ if ('text' !== $tokens[0][0]) {
+ return ($route->hasDefault($tokens[0][3]) || '/' === $tokens[0][1]) ? '' : $tokens[0][1];
+ }
+
+ $prefix = $tokens[0][1];
+
+ if (isset($tokens[1][1]) && '/' !== $tokens[1][1] && false === $route->hasDefault($tokens[1][3])) {
+ $prefix .= $tokens[1][1];
+ }
+
+ return $prefix;
+ }
+
+ /**
+ * Returns the next static character in the Route pattern that will serve as a separator (or the empty string when none available).
+ */
+ private static function findNextSeparator(string $pattern, bool $useUtf8): string
+ {
+ if ('' == $pattern) {
+ // return empty string if pattern is empty or false (false which can be returned by substr)
+ return '';
+ }
+ // first remove all placeholders from the pattern so we can find the next real static character
+ if ('' === $pattern = preg_replace('#\{\w+\}#', '', $pattern)) {
+ return '';
+ }
+ if ($useUtf8) {
+ preg_match('/^./u', $pattern, $pattern);
+ }
+
+ return str_contains(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
+ }
+
+ /**
+ * Computes the regexp used to match a specific token. It can be static text or a subpattern.
+ *
+ * @param array $tokens The route tokens
+ * @param int $index The index of the current token
+ * @param int $firstOptional The index of the first optional token
+ */
+ private static function computeRegexp(array $tokens, int $index, int $firstOptional): string
+ {
+ $token = $tokens[$index];
+ if ('text' === $token[0]) {
+ // Text tokens
+ return preg_quote($token[1]);
+ } else {
+ // Variable tokens
+ if (0 === $index && 0 === $firstOptional) {
+ // When the only token is an optional variable token, the separator is required
+ return sprintf('%s(?P<%s>%s)?', preg_quote($token[1]), $token[3], $token[2]);
+ } else {
+ $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1]), $token[3], $token[2]);
+ if ($index >= $firstOptional) {
+ // Enclose each optional token in a subpattern to make it optional.
+ // "?:" means it is non-capturing, i.e. the portion of the subject string that
+ // matched the optional subpattern is not passed back.
+ $regexp = "(?:$regexp";
+ $nbTokens = \count($tokens);
+ if ($nbTokens - 1 == $index) {
+ // Close the optional subpatterns
+ $regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
+ }
+ }
+
+ return $regexp;
+ }
+ }
+ }
+
+ private static function transformCapturingGroupsToNonCapturings(string $regexp): string
+ {
+ for ($i = 0; $i < \strlen($regexp); ++$i) {
+ if ('\\' === $regexp[$i]) {
+ ++$i;
+ continue;
+ }
+ if ('(' !== $regexp[$i] || !isset($regexp[$i + 2])) {
+ continue;
+ }
+ if ('*' === $regexp[++$i] || '?' === $regexp[$i]) {
+ ++$i;
+ continue;
+ }
+ $regexp = substr_replace($regexp, '?:', $i, 0);
+ ++$i;
+ }
+
+ return $regexp;
+ }
+}
diff --git a/src/vendor/symfony/routing/RouteCompilerInterface.php b/src/vendor/symfony/routing/RouteCompilerInterface.php
new file mode 100644
index 000000000..9bae33a91
--- /dev/null
+++ b/src/vendor/symfony/routing/RouteCompilerInterface.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * RouteCompilerInterface is the interface that all RouteCompiler classes must implement.
+ *
+ * @author Fabien Potencier
+ */
+interface RouteCompilerInterface
+{
+ /**
+ * Compiles the current route instance.
+ *
+ * @return CompiledRoute
+ *
+ * @throws \LogicException If the Route cannot be compiled because the
+ * path or host pattern is invalid
+ */
+ public static function compile(Route $route);
+}
diff --git a/src/vendor/symfony/routing/Router.php b/src/vendor/symfony/routing/Router.php
new file mode 100644
index 000000000..e3d38c491
--- /dev/null
+++ b/src/vendor/symfony/routing/Router.php
@@ -0,0 +1,393 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Config\ConfigCacheFactory;
+use Symfony\Component\Config\ConfigCacheFactoryInterface;
+use Symfony\Component\Config\ConfigCacheInterface;
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Generator\CompiledUrlGenerator;
+use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface;
+use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper;
+use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
+use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
+use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
+use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
+
+/**
+ * The Router class is an example of the integration of all pieces of the
+ * routing system for easier use.
+ *
+ * @author Fabien Potencier
+ */
+class Router implements RouterInterface, RequestMatcherInterface
+{
+ /**
+ * @var UrlMatcherInterface|null
+ */
+ protected $matcher;
+
+ /**
+ * @var UrlGeneratorInterface|null
+ */
+ protected $generator;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ /**
+ * @var LoaderInterface
+ */
+ protected $loader;
+
+ /**
+ * @var RouteCollection|null
+ */
+ protected $collection;
+
+ /**
+ * @var mixed
+ */
+ protected $resource;
+
+ /**
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * @var LoggerInterface|null
+ */
+ protected $logger;
+
+ /**
+ * @var string|null
+ */
+ protected $defaultLocale;
+
+ /**
+ * @var ConfigCacheFactoryInterface|null
+ */
+ private $configCacheFactory;
+
+ /**
+ * @var ExpressionFunctionProviderInterface[]
+ */
+ private $expressionLanguageProviders = [];
+
+ private static $cache = [];
+
+ /**
+ * @param mixed $resource The main resource to load
+ */
+ public function __construct(LoaderInterface $loader, $resource, array $options = [], ?RequestContext $context = null, ?LoggerInterface $logger = null, ?string $defaultLocale = null)
+ {
+ $this->loader = $loader;
+ $this->resource = $resource;
+ $this->logger = $logger;
+ $this->context = $context ?? new RequestContext();
+ $this->setOptions($options);
+ $this->defaultLocale = $defaultLocale;
+ }
+
+ /**
+ * Sets options.
+ *
+ * Available options:
+ *
+ * * cache_dir: The cache directory (or null to disable caching)
+ * * debug: Whether to enable debugging or not (false by default)
+ * * generator_class: The name of a UrlGeneratorInterface implementation
+ * * generator_dumper_class: The name of a GeneratorDumperInterface implementation
+ * * matcher_class: The name of a UrlMatcherInterface implementation
+ * * matcher_dumper_class: The name of a MatcherDumperInterface implementation
+ * * resource_type: Type hint for the main resource (optional)
+ * * strict_requirements: Configure strict requirement checking for generators
+ * implementing ConfigurableRequirementsInterface (default is true)
+ *
+ * @throws \InvalidArgumentException When unsupported option is provided
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = [
+ 'cache_dir' => null,
+ 'debug' => false,
+ 'generator_class' => CompiledUrlGenerator::class,
+ 'generator_dumper_class' => CompiledUrlGeneratorDumper::class,
+ 'matcher_class' => CompiledUrlMatcher::class,
+ 'matcher_dumper_class' => CompiledUrlMatcherDumper::class,
+ 'resource_type' => null,
+ 'strict_requirements' => true,
+ ];
+
+ // check option names and live merge, if errors are encountered Exception will be thrown
+ $invalid = [];
+ foreach ($options as $key => $value) {
+ if (\array_key_exists($key, $this->options)) {
+ $this->options[$key] = $value;
+ } else {
+ $invalid[] = $key;
+ }
+ }
+
+ if ($invalid) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the following options: "%s".', implode('", "', $invalid)));
+ }
+ }
+
+ /**
+ * Sets an option.
+ *
+ * @param mixed $value The value
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setOption(string $key, $value)
+ {
+ if (!\array_key_exists($key, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
+ }
+
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * Gets an option value.
+ *
+ * @return mixed
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getOption(string $key)
+ {
+ if (!\array_key_exists($key, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
+ }
+
+ return $this->options[$key];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRouteCollection()
+ {
+ if (null === $this->collection) {
+ $this->collection = $this->loader->load($this->resource, $this->options['resource_type']);
+ }
+
+ return $this->collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+
+ if (null !== $this->matcher) {
+ $this->getMatcher()->setContext($context);
+ }
+ if (null !== $this->generator) {
+ $this->getGenerator()->setContext($context);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * Sets the ConfigCache factory to use.
+ */
+ public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
+ {
+ $this->configCacheFactory = $configCacheFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH)
+ {
+ return $this->getGenerator()->generate($name, $parameters, $referenceType);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function match(string $pathinfo)
+ {
+ return $this->getMatcher()->match($pathinfo);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function matchRequest(Request $request)
+ {
+ $matcher = $this->getMatcher();
+ if (!$matcher instanceof RequestMatcherInterface) {
+ // fallback to the default UrlMatcherInterface
+ return $matcher->match($request->getPathInfo());
+ }
+
+ return $matcher->matchRequest($request);
+ }
+
+ /**
+ * Gets the UrlMatcher or RequestMatcher instance associated with this Router.
+ *
+ * @return UrlMatcherInterface|RequestMatcherInterface
+ */
+ public function getMatcher()
+ {
+ if (null !== $this->matcher) {
+ return $this->matcher;
+ }
+
+ if (null === $this->options['cache_dir']) {
+ $routes = $this->getRouteCollection();
+ $compiled = is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true);
+ if ($compiled) {
+ $routes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes();
+ }
+ $this->matcher = new $this->options['matcher_class']($routes, $this->context);
+ if (method_exists($this->matcher, 'addExpressionLanguageProvider')) {
+ foreach ($this->expressionLanguageProviders as $provider) {
+ $this->matcher->addExpressionLanguageProvider($provider);
+ }
+ }
+
+ return $this->matcher;
+ }
+
+ $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_matching_routes.php',
+ function (ConfigCacheInterface $cache) {
+ $dumper = $this->getMatcherDumperInstance();
+ if (method_exists($dumper, 'addExpressionLanguageProvider')) {
+ foreach ($this->expressionLanguageProviders as $provider) {
+ $dumper->addExpressionLanguageProvider($provider);
+ }
+ }
+
+ $cache->write($dumper->dump(), $this->getRouteCollection()->getResources());
+ unset(self::$cache[$cache->getPath()]);
+ }
+ );
+
+ return $this->matcher = new $this->options['matcher_class'](self::getCompiledRoutes($cache->getPath()), $this->context);
+ }
+
+ /**
+ * Gets the UrlGenerator instance associated with this Router.
+ *
+ * @return UrlGeneratorInterface
+ */
+ public function getGenerator()
+ {
+ if (null !== $this->generator) {
+ return $this->generator;
+ }
+
+ if (null === $this->options['cache_dir']) {
+ $routes = $this->getRouteCollection();
+ $compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true);
+ if ($compiled) {
+ $generatorDumper = new CompiledUrlGeneratorDumper($routes);
+ $routes = array_merge($generatorDumper->getCompiledRoutes(), $generatorDumper->getCompiledAliases());
+ }
+ $this->generator = new $this->options['generator_class']($routes, $this->context, $this->logger, $this->defaultLocale);
+ } else {
+ $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_generating_routes.php',
+ function (ConfigCacheInterface $cache) {
+ $dumper = $this->getGeneratorDumperInstance();
+
+ $cache->write($dumper->dump(), $this->getRouteCollection()->getResources());
+ unset(self::$cache[$cache->getPath()]);
+ }
+ );
+
+ $this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale);
+ }
+
+ if ($this->generator instanceof ConfigurableRequirementsInterface) {
+ $this->generator->setStrictRequirements($this->options['strict_requirements']);
+ }
+
+ return $this->generator;
+ }
+
+ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
+ {
+ $this->expressionLanguageProviders[] = $provider;
+ }
+
+ /**
+ * @return GeneratorDumperInterface
+ */
+ protected function getGeneratorDumperInstance()
+ {
+ return new $this->options['generator_dumper_class']($this->getRouteCollection());
+ }
+
+ /**
+ * @return MatcherDumperInterface
+ */
+ protected function getMatcherDumperInstance()
+ {
+ return new $this->options['matcher_dumper_class']($this->getRouteCollection());
+ }
+
+ /**
+ * Provides the ConfigCache factory implementation, falling back to a
+ * default implementation if necessary.
+ */
+ private function getConfigCacheFactory(): ConfigCacheFactoryInterface
+ {
+ if (null === $this->configCacheFactory) {
+ $this->configCacheFactory = new ConfigCacheFactory($this->options['debug']);
+ }
+
+ return $this->configCacheFactory;
+ }
+
+ private static function getCompiledRoutes(string $path): array
+ {
+ if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) {
+ self::$cache = null;
+ }
+
+ if (null === self::$cache) {
+ return require $path;
+ }
+
+ if (isset(self::$cache[$path])) {
+ return self::$cache[$path];
+ }
+
+ return self::$cache[$path] = require $path;
+ }
+}
diff --git a/src/vendor/symfony/routing/RouterInterface.php b/src/vendor/symfony/routing/RouterInterface.php
new file mode 100644
index 000000000..6912f8a15
--- /dev/null
+++ b/src/vendor/symfony/routing/RouterInterface.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
+
+/**
+ * RouterInterface is the interface that all Router classes must implement.
+ *
+ * This interface is the concatenation of UrlMatcherInterface and UrlGeneratorInterface.
+ *
+ * @author Fabien Potencier
+ */
+interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface
+{
+ /**
+ * Gets the RouteCollection instance associated with this Router.
+ *
+ * WARNING: This method should never be used at runtime as it is SLOW.
+ * You might use it in a cache warmer though.
+ *
+ * @return RouteCollection
+ */
+ public function getRouteCollection();
+}
diff --git a/src/vendor/symfony/routing/composer.json b/src/vendor/symfony/routing/composer.json
new file mode 100644
index 000000000..c32219e63
--- /dev/null
+++ b/src/vendor/symfony/routing/composer.json
@@ -0,0 +1,51 @@
+{
+ "name": "symfony/routing",
+ "type": "library",
+ "description": "Maps an HTTP request to a set of configuration variables",
+ "keywords": ["routing", "router", "url", "uri"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "symfony/config": "^5.3|^6.0",
+ "symfony/http-foundation": "^4.4|^5.0|^6.0",
+ "symfony/yaml": "^4.4|^5.0|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/dependency-injection": "^4.4|^5.0|^6.0",
+ "doctrine/annotations": "^1.12|^2",
+ "psr/log": "^1|^2|^3"
+ },
+ "conflict": {
+ "doctrine/annotations": "<1.12",
+ "symfony/config": "<5.3",
+ "symfony/dependency-injection": "<4.4",
+ "symfony/yaml": "<4.4"
+ },
+ "suggest": {
+ "symfony/http-foundation": "For using a Symfony Request object",
+ "symfony/config": "For using the all-in-one router or any loader",
+ "symfony/yaml": "For using the YAML loader",
+ "symfony/expression-language": "For using expression matching"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Routing\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev"
+}
diff --git a/src/ws_server.php b/src/ws_server.php
new file mode 100644
index 000000000..5ec26cfbe
--- /dev/null
+++ b/src/ws_server.php
@@ -0,0 +1,273 @@
+env->load(__DIR__ . '/.env');
+}
+
+// 根据APP_ENV加载环境特定的.env文件
+$appEnv = getenv('APP_ENV') ?: '';
+if ($appEnv) {
+ $envFile = __DIR__ . '/.env.' . $appEnv;
+ if (is_file($envFile)) {
+ $app->env->load($envFile);
+ }
+} else {
+ // 为了兼容性,如果存在.env.local也加载
+ if (is_file(__DIR__ . '/.env.local')) {
+ $app->env->load(__DIR__ . '/.env.local');
+ }
+}
+
+use Ratchet\App as RatchetApp;
+use Ratchet\ConnectionInterface;
+use Ratchet\MessageComponentInterface;
+
+// 配置WebSocket服务器
+$httpHost = 'localhost'; // 客户端连接时使用的主机名
+$port = getenv('WS_PORT') ?: 8080; // WebSocket服务器端口
+$address = '0.0.0.0'; // 监听所有网络接口
+
+// 创建WebSocket服务器应用 - 参数顺序:httpHost, port, address
+$ratchetApp = new RatchetApp($httpHost, $port, $address);
+
+// 获取所有addon(与InitAddon.php逻辑保持一致)
+// 1. 从数据库获取插件列表
+
+use app\model\system\Addon;
+
+$addonDir = __DIR__ . '/addon';
+$addonNames = [];
+
+// 从数据库获取addon列表
+try {
+ $addon_model = new Addon();
+ $addon_data = $addon_model->getAddonList([], 'name,status');
+ $current_addons = $addon_data['data'];
+ $db_addon_names = array_column($current_addons, 'name');
+
+ // 2. 从addon目录读取插件列表
+ $dir_addon_names = [];
+ if (is_dir($addonDir)) {
+ $dir_handle = opendir($addonDir);
+ if ($dir_handle) {
+ while (($file = readdir($dir_handle)) !== false) {
+ if ($file != '.' && $file != '..' && is_dir($addonDir . '/' . $file)) {
+ $dir_addon_names[] = $file;
+ }
+ }
+ closedir($dir_handle);
+ }
+ }
+
+ // 3. 使用数据库中的插件列表进行后续处理
+ $current_addon_names = $db_addon_names;
+
+ // 4. 记录已启用的插件
+ $enabled_addons = [];
+ foreach ($current_addons as $addon) {
+ if (isset($addon['status']) && $addon['status'] == 1) {
+ $enabled_addons[] = $addon['name'];
+ }
+ }
+
+ echo "[WebSocket服务器] 从数据库获取的插件列表: " . implode(', ', $current_addon_names) . "\n";
+ echo "[WebSocket服务器] 已启用的插件: " . implode(', ', $enabled_addons) . "\n";
+ echo "[WebSocket服务器] 目录中存在的插件: " . implode(', ', $dir_addon_names) . "\n";
+
+ // 5. 比较数据库和目录插件列表的差异
+ $db_only_addons = array_diff($db_addon_names, $dir_addon_names);
+ $dir_only_addons = array_diff($dir_addon_names, $db_addon_names);
+
+ if (!empty($db_only_addons)) {
+ echo "[WebSocket服务器] 数据库中存在但目录中不存在的插件: " . implode(', ', $db_only_addons) . "\n";
+ }
+
+ if (!empty($dir_only_addons)) {
+ echo "[WebSocket服务器] 目录中存在但数据库中不存在的插件: " . implode(', ', $dir_only_addons) . "\n";
+ }
+
+} catch (\Exception $e) {
+ echo "[WebSocket服务器] 获取插件列表失败: " . $e->getMessage() . "\n";
+ echo "[WebSocket服务器] 回退到直接扫描目录获取插件列表\n";
+
+ // 回退到直接扫描目录
+ $addonNames = [];
+ if (is_dir($addonDir)) {
+ $handle = opendir($addonDir);
+ while (($file = readdir($handle)) !== false) {
+ if ($file != '.' && $file != '..' && is_dir($addonDir . '/' . $file)) {
+ $addonNames[] = $file;
+ }
+ }
+ closedir($handle);
+ sort($addonNames);
+ }
+
+ $current_addon_names = $addonNames;
+ $enabled_addons = $addonNames; // 回退模式下默认所有插件都启用
+}
+
+// 为每个addon注册WebSocket控制器,并记录注册情况
+$registeredAddons = [];
+$unregisteredAddons = [];
+$disabledAddons = [];
+$missingDirAddons = [];
+
+foreach ($current_addon_names as $addonName) {
+ // 检查插件是否已启用
+ if (!in_array($addonName, $enabled_addons)) {
+ echo "[{$addonName}] 插件未启用,跳过WebSocket控制器注册\n";
+ $disabledAddons[] = $addonName;
+ continue;
+ }
+
+ // 检查插件目录是否存在
+ if (!is_dir($addonDir . '/' . $addonName)) {
+ echo "[{$addonName}] 插件目录不存在,跳过WebSocket控制器注册\n";
+ $missingDirAddons[] = $addonName;
+ continue;
+ }
+
+ $webSocketClass = "addon\\{$addonName}\\api\\controller\\WebSocket";
+
+ // 检查WebSocket控制器是否存在
+ try {
+ if (class_exists($webSocketClass)) {
+ // 注册到/ws/{addonName}路径
+ $path = '/ws/' . $addonName;
+ $ratchetApp->route($path, new $webSocketClass(), array('*'));
+ echo "已注册WebSocket控制器:{$webSocketClass} 到路径 {$path}\n";
+ $registeredAddons[] = $addonName;
+ } else {
+ echo "[{$addonName}] WebSocket控制器不存在 ({$webSocketClass})\n";
+ $unregisteredAddons[] = $addonName;
+ }
+ } catch (\Exception $e) {
+ echo "[{$addonName}] 检查WebSocket控制器时出错:{$e->getMessage()}\n";
+ $unregisteredAddons[] = $addonName;
+ }
+}
+
+// 实现默认的/ws路径的简单测试控制器
+class DefaultWebSocketController implements MessageComponentInterface
+{
+ protected $clients;
+
+ public function __construct()
+ {
+ $this->clients = new \SplObjectStorage;
+ }
+
+ public function onOpen(ConnectionInterface $conn)
+ {
+ $this->clients->attach($conn);
+ echo "[默认路径] New connection! ({$conn->resourceId})\n";
+ $conn->send(json_encode([
+ 'type' => 'welcome',
+ 'message' => '欢迎连接到默认WebSocket测试路径',
+ 'info' => '此路径仅用于测试,不提供实际功能。请使用/ws/{addonName}连接到具体的addon服务。'
+ ]));
+ }
+
+ public function onMessage(ConnectionInterface $conn, $msg)
+ {
+ echo "[默认路径] Received message from {$conn->resourceId}: $msg\n";
+ try {
+ $data = json_decode($msg, true);
+ if (isset($data['action']) && $data['action'] === 'ping') {
+ $conn->send(json_encode(['type' => 'pong']));
+ } else {
+ $conn->send(json_encode([
+ 'type' => 'info',
+ 'message' => '收到消息,但默认路径不提供实际功能',
+ 'received' => $data
+ ]));
+ }
+ } catch (\Exception $e) {
+ $conn->send(json_encode(['type' => 'error', 'message' => '解析消息失败: ' . $e->getMessage()]));
+ }
+ }
+
+ public function onClose(ConnectionInterface $conn)
+ {
+ $this->clients->detach($conn);
+ echo "[默认路径] Connection {$conn->resourceId} has disconnected\n";
+ }
+
+ public function onError(ConnectionInterface $conn, \Exception $e)
+ {
+ echo "[默认路径] An error has occurred: {$e->getMessage()}\n";
+ $conn->close();
+ }
+}
+
+// 注册默认的/ws路径测试控制器
+$ratchetApp->route('/ws', new DefaultWebSocketController(), array('*'));
+echo "已注册默认WebSocket测试控制器到路径 /ws\n";
+
+echo "WebSocket服务器已启动,监听地址: ws://{$httpHost}:{$port}\n";
+
+// 显示已注册WebSocket控制器的addon路径
+echo "\n已注册WebSocket控制器的addon路径:\n";
+if (!empty($registeredAddons)) {
+ foreach ($registeredAddons as $addonName) {
+ echo " - ws://{$httpHost}:{$port}/ws/{$addonName} (已注册)\n";
+ }
+} else {
+ echo " - 暂无已注册的WebSocket控制器\n";
+}
+
+// 显示未注册WebSocket控制器的addon路径
+echo "\n未注册WebSocket控制器的addon路径:\n";
+if (!empty($unregisteredAddons)) {
+ foreach ($unregisteredAddons as $addonName) {
+ echo " - ws://{$httpHost}:{$port}/ws/{$addonName} (未注册)\n";
+ }
+} else {
+ echo " - 所有已启用且目录存在的addon都已注册WebSocket控制器\n";
+}
+
+// 显示未启用的addon
+echo "\n未启用的addon:\n";
+if (!empty($disabledAddons)) {
+ foreach ($disabledAddons as $addonName) {
+ echo " - {$addonName} (未启用)\n";
+ }
+} else {
+ echo " - 所有addon都已启用\n";
+}
+
+// 显示目录不存在的addon
+echo "\n目录不存在的addon:\n";
+if (!empty($missingDirAddons)) {
+ foreach ($missingDirAddons as $addonName) {
+ echo " - {$addonName} (目录不存在)\n";
+ }
+} else {
+ echo " - 所有已启用的addon目录都存在\n";
+}
+
+echo "\n默认测试路径:\n";
+echo " - ws://{$httpHost}:{$port}/ws (默认路径,用于连接测试)\n";
+echo "按 Ctrl+C 停止服务器\n";
+
+// 运行服务器
+$ratchetApp->run();