Skip to content

Commit 4711bd0

Browse files
committed
feature #47595 [HttpFoundation] Extract request matchers for better reusability (fabpot)
This PR was merged into the 6.2 branch. Discussion ---------- [HttpFoundation] Extract request matchers for better reusability | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | n/a <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT | Doc PR | The `RequestMatcher` class hardcodes its matchers. This PR extracts those into their own classes so that we can compose a `RequestMatcher` and allow better reusability. Commits ------- 6cfd3b707a [HttpFoundation] Extract request matchers for better reusability
2 parents 87b4217 + 4f560b1 commit 4711bd0

24 files changed

+830
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ CHANGELOG
66

77
* The HTTP cache store uses the `xxh128` algorithm
88
* Deprecate calling `JsonResponse::setCallback()`, `Response::setExpires/setLastModified/setEtag()`, `MockArraySessionStorage/NativeSessionStorage::setMetadataBag()`, `NativeSessionStorage::setSaveHandler()` without arguments
9+
* Add request matchers under the `Symfony\Component\HttpFoundation\RequestMatcher` namespace
10+
* Deprecate `RequestMatcher` in favor of `ChainRequestMatcher`
11+
* Deprecate `Symfony\Component\HttpFoundation\ExpressionRequestMatcher` in favor of `Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher`
912

1013
6.1
1114
---

ChainRequestMatcher.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* ChainRequestMatcher verifies that all checks match against a Request instance.
16+
*
17+
* @author Fabien Potencier <[email protected]>
18+
*/
19+
class ChainRequestMatcher implements RequestMatcherInterface
20+
{
21+
/**
22+
* @param iterable<RequestMatcherInterface> $matchers
23+
*/
24+
public function __construct(private iterable $matchers)
25+
{
26+
}
27+
28+
public function matches(Request $request): bool
29+
{
30+
foreach ($this->matchers as $matcher) {
31+
if (!$matcher->matches($request)) {
32+
return false;
33+
}
34+
}
35+
36+
return true;
37+
}
38+
}

ExpressionRequestMatcher.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313

1414
use Symfony\Component\ExpressionLanguage\Expression;
1515
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16+
use Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher as NewExpressionRequestMatcher;
17+
18+
trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s" class is deprecated, use "%s" instead.', ExpressionRequestMatcher::class, NewExpressionRequestMatcher::class);
1619

1720
/**
1821
* ExpressionRequestMatcher uses an expression to match a Request.
1922
*
2023
* @author Fabien Potencier <[email protected]>
24+
*
25+
* @deprecated since Symfony 6.2, use "Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher" instead
2126
*/
2227
class ExpressionRequestMatcher extends RequestMatcher
2328
{

RequestMatcher.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111

1212
namespace Symfony\Component\HttpFoundation;
1313

14+
trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s" class is deprecated, use "%s" instead.', RequestMatcher::class, ChainRequestMatcher::class);
15+
1416
/**
1517
* RequestMatcher compares a pre-defined set of checks against a Request instance.
1618
*
1719
* @author Fabien Potencier <[email protected]>
20+
*
21+
* @deprecated since Symfony 6.2, use ChainRequestMatcher instead
1822
*/
1923
class RequestMatcher implements RequestMatcherInterface
2024
{
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
16+
17+
/**
18+
* Checks the Request attributes matches all regular expressions.
19+
*
20+
* @author Fabien Potencier <[email protected]>
21+
*/
22+
class AttributesRequestMatcher implements RequestMatcherInterface
23+
{
24+
/**
25+
* @param array<string, string> $regexps
26+
*/
27+
public function __construct(private array $regexps)
28+
{
29+
}
30+
31+
public function matches(Request $request): bool
32+
{
33+
foreach ($this->regexps as $key => $regexp) {
34+
$attribute = $request->attributes->get($key);
35+
if (!\is_string($attribute)) {
36+
return false;
37+
}
38+
if (!preg_match('{'.$regexp.'}', $attribute)) {
39+
return false;
40+
}
41+
}
42+
43+
return true;
44+
}
45+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\ExpressionLanguage\Expression;
15+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
18+
19+
/**
20+
* ExpressionRequestMatcher uses an expression to match a Request.
21+
*
22+
* @author Fabien Potencier <[email protected]>
23+
*/
24+
class ExpressionRequestMatcher implements RequestMatcherInterface
25+
{
26+
public function __construct(
27+
private ExpressionLanguage $language,
28+
private Expression|string $expression,
29+
) {
30+
}
31+
32+
public function matches(Request $request): bool
33+
{
34+
return $this->language->evaluate($this->expression, [
35+
'request' => $request,
36+
'method' => $request->getMethod(),
37+
'path' => rawurldecode($request->getPathInfo()),
38+
'host' => $request->getHost(),
39+
'ip' => $request->getClientIp(),
40+
'attributes' => $request->attributes->all(),
41+
]);
42+
}
43+
}

RequestMatcher/HostRequestMatcher.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
16+
17+
/**
18+
* Checks the Request URL host name matches a regular expression.
19+
*
20+
* @author Fabien Potencier <[email protected]>
21+
*/
22+
class HostRequestMatcher implements RequestMatcherInterface
23+
{
24+
public function __construct(private string $regexp)
25+
{
26+
}
27+
28+
public function matches(Request $request): bool
29+
{
30+
return preg_match('{'.$this->regexp.'}i', $request->getHost());
31+
}
32+
}

RequestMatcher/IpsRequestMatcher.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\HttpFoundation\IpUtils;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
17+
18+
/**
19+
* Checks the client IP of a Request.
20+
*
21+
* @author Fabien Potencier <[email protected]>
22+
*/
23+
class IpsRequestMatcher implements RequestMatcherInterface
24+
{
25+
private array $ips;
26+
27+
/**
28+
* @param string[]|string $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24
29+
* Strings can contain a comma-delimited list of IPs/ranges
30+
*/
31+
public function __construct(array|string $ips)
32+
{
33+
$this->ips = array_reduce((array) $ips, static function (array $ips, string $ip) {
34+
return array_merge($ips, preg_split('/\s*,\s*/', $ip));
35+
}, []);
36+
}
37+
38+
public function matches(Request $request): bool
39+
{
40+
if (!$this->ips) {
41+
return true;
42+
}
43+
44+
return IpUtils::checkIp($request->getClientIp() ?? '', $this->ips);
45+
}
46+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
16+
17+
/**
18+
* Checks the Request content is valid JSON.
19+
*
20+
* @author Fabien Potencier <[email protected]>
21+
*/
22+
class IsJsonRequestMatcher implements RequestMatcherInterface
23+
{
24+
public function matches(Request $request): bool
25+
{
26+
try {
27+
json_decode($request->getContent(), true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
28+
} catch (\JsonException) {
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
16+
17+
/**
18+
* Checks the HTTP method of a Request.
19+
*
20+
* @author Fabien Potencier <[email protected]>
21+
*/
22+
class MethodRequestMatcher implements RequestMatcherInterface
23+
{
24+
/**
25+
* @var string[]
26+
*/
27+
private array $methods = [];
28+
29+
/**
30+
* @param string[]|string $methods An HTTP method or an array of HTTP methods
31+
* Strings can contain a comma-delimited list of methods
32+
*/
33+
public function __construct(array|string $methods)
34+
{
35+
$this->methods = array_reduce(array_map('strtoupper', (array) $methods), static function (array $methods, string $method) {
36+
return array_merge($methods, preg_split('/\s*,\s*/', $method));
37+
}, []);
38+
}
39+
40+
public function matches(Request $request): bool
41+
{
42+
if (!$this->methods) {
43+
return true;
44+
}
45+
46+
return \in_array($request->getMethod(), $this->methods, true);
47+
}
48+
}

RequestMatcher/PathRequestMatcher.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\RequestMatcher;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
16+
17+
/**
18+
* Checks the Request URL path info matches a regular expression.
19+
*
20+
* @author Fabien Potencier <[email protected]>
21+
*/
22+
class PathRequestMatcher implements RequestMatcherInterface
23+
{
24+
public function __construct(private string $regexp)
25+
{
26+
}
27+
28+
public function matches(Request $request): bool
29+
{
30+
return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo()));
31+
}
32+
}

0 commit comments

Comments
 (0)