<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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 array $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(): RequestContext
{
return $this->context;
}
/**
* {@inheritdoc}
*/
public function match(string $pathinfo): array
{
$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): array
{
$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
*
* @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): array
{
// 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).
*/
protected function getAttributes(Route $route, string $name, array $attributes): array
{
$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): array
{
// 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.
*/
protected function mergeDefaults(array $params, array $defaults): array
{
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(),
]);
}
}
In the digital age, privacy concerns have become increasingly paramount, prompting the European Union to enact the General Data Protection Regulation (GDPR) in 2018. Among its many provisions, GDPR sets strict guidelines for the collection and processing of personal data, including the use of cookies on websites. Privacy Policy