Skip to content

Commit 33f97a3

Browse files
committed
[HttpFoundation] Extract request matchers for better reusability
1 parent 58117d7 commit 33f97a3

18 files changed

+725
-67
lines changed

src/Symfony/Component/HttpFoundation/RequestMatcher.php

Lines changed: 71 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,25 @@
1111

1212
namespace Symfony\Component\HttpFoundation;
1313

14+
use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher;
15+
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
16+
use Symfony\Component\HttpFoundation\RequestMatcher\IpsRequestMatcher;
17+
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
18+
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
19+
use Symfony\Component\HttpFoundation\RequestMatcher\PortRequestMatcher;
20+
use Symfony\Component\HttpFoundation\RequestMatcher\SchemeRequestMatcher;
21+
1422
/**
1523
* RequestMatcher compares a pre-defined set of checks against a Request instance.
1624
*
1725
* @author Fabien Potencier <fabien@symfony.com>
1826
*/
1927
class RequestMatcher implements RequestMatcherInterface
2028
{
21-
private ?string $path = null;
22-
private ?string $host = null;
23-
private ?int $port = null;
24-
2529
/**
26-
* @var string[]
30+
* @var RequestMatcherInterface[]
2731
*/
28-
private array $methods = [];
29-
30-
/**
31-
* @var string[]
32-
*/
33-
private array $ips = [];
34-
35-
/**
36-
* @var string[]
37-
*/
38-
private array $attributes = [];
39-
40-
/**
41-
* @var string[]
42-
*/
43-
private array $schemes = [];
32+
private array $matchers = [];
4433

4534
/**
4635
* @param string|string[]|null $methods
@@ -61,22 +50,49 @@ public function __construct(string $path = null, string $host = null, string|arr
6150
}
6251
}
6352

53+
/**
54+
* @return $this
55+
*/
56+
public function add(RequestMatcherInterface $matcher): static
57+
{
58+
$this->matchers[$matcher::class] = $matcher;
59+
60+
return $this;
61+
}
62+
63+
public function remove(string $class)
64+
{
65+
unset($this->matchers[$class]);
66+
}
67+
6468
/**
6569
* Adds a check for the HTTP scheme.
6670
*
6771
* @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes
6872
*/
6973
public function matchScheme(string|array|null $scheme)
7074
{
71-
$this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : [];
75+
if (null === $scheme || !count($scheme)) {
76+
$this->remove(SchemeRequestMatcher::class);
77+
78+
return;
79+
}
80+
81+
$this->add(new SchemeRequestMatcher($scheme));
7282
}
7383

7484
/**
7585
* Adds a check for the URL host name.
7686
*/
7787
public function matchHost(?string $regexp)
7888
{
79-
$this->host = $regexp;
89+
if (null === $regexp) {
90+
$this->remove(HostRequestMatcher::class);
91+
92+
return;
93+
}
94+
95+
$this->add(new HostRequestMatcher($regexp));
8096
}
8197

8298
/**
@@ -86,15 +102,27 @@ public function matchHost(?string $regexp)
86102
*/
87103
public function matchPort(?int $port)
88104
{
89-
$this->port = $port;
105+
if (null === $port) {
106+
$this->remove(PortRequestMatcher::class);
107+
108+
return;
109+
}
110+
111+
$this->add(new PortRequestMatcher($port));
90112
}
91113

92114
/**
93115
* Adds a check for the URL path info.
94116
*/
95117
public function matchPath(?string $regexp)
96118
{
97-
$this->path = $regexp;
119+
if (null === $regexp) {
120+
$this->remove(PathRequestMatcher::class);
121+
122+
return;
123+
}
124+
125+
$this->add(new PathRequestMatcher($regexp));
98126
}
99127

100128
/**
@@ -114,69 +142,47 @@ public function matchIp(string $ip)
114142
*/
115143
public function matchIps(string|array|null $ips)
116144
{
117-
$ips = null !== $ips ? (array) $ips : [];
145+
if (null === $ips || !count($ips)) {
146+
$this->remove(IpsRequestMatcher::class);
118147

119-
$this->ips = array_reduce($ips, static function (array $ips, string $ip) {
120-
return array_merge($ips, preg_split('/\s*,\s*/', $ip));
121-
}, []);
148+
return;
149+
}
150+
151+
$this->add(new IpsRequestMatcher($ips));
122152
}
123153

124154
/**
125155
* Adds a check for the HTTP method.
126156
*
127-
* @param string|string[]|null $method An HTTP method or an array of HTTP methods
157+
* @param string|string[]|null $method
128158
*/
129159
public function matchMethod(string|array|null $method)
130160
{
131-
$this->methods = null !== $method ? array_map('strtoupper', (array) $method) : [];
161+
if (null === $method || !count($method)) {
162+
$this->remove(MethodRequestMatcher::class);
163+
164+
return;
165+
}
166+
167+
$this->add(new MethodRequestMatcher($method));
132168
}
133169

134170
/**
135171
* Adds a check for request attribute.
136172
*/
137173
public function matchAttribute(string $key, string $regexp)
138174
{
139-
$this->attributes[$key] = $regexp;
175+
($this->matchers[AttributesRequestMatcher::class] ??= new AttributesRequestMatcher([]))->addAttributeCheck($key, $regexp);
140176
}
141177

142178
public function matches(Request $request): bool
143179
{
144-
if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) {
145-
return false;
146-
}
147-
148-
if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) {
149-
return false;
150-
}
151-
152-
foreach ($this->attributes as $key => $pattern) {
153-
$requestAttribute = $request->attributes->get($key);
154-
if (!\is_string($requestAttribute)) {
155-
return false;
156-
}
157-
if (!preg_match('{'.$pattern.'}', $requestAttribute)) {
180+
foreach ($this->matchers as $matcher) {
181+
if (!$matcher->matches($request)) {
158182
return false;
159183
}
160184
}
161185

162-
if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getPathInfo()))) {
163-
return false;
164-
}
165-
166-
if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getHost())) {
167-
return false;
168-
}
169-
170-
if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) {
171-
return false;
172-
}
173-
174-
if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) {
175-
return true;
176-
}
177-
178-
// Note to future implementors: add additional checks above the
179-
// foreach above or else your check might not be run!
180-
return 0 === \count($this->ips);
186+
return true;
181187
}
182188
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <fabien@symfony.com>
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+
/**
32+
* @return $this
33+
*/
34+
public function addAttributeCheck(string $key, string $regexp): static
35+
{
36+
$this->regexps[$key] = $regexp;
37+
38+
return $this;
39+
}
40+
41+
public function matches(Request $request): bool
42+
{
43+
foreach ($this->regexps as $key => $regexp) {
44+
$attribute = $request->attributes->get($key);
45+
if (!\is_string($attribute)) {
46+
return false;
47+
}
48+
if (!preg_match('{'.$regexp.'}', $attribute)) {
49+
return false;
50+
}
51+
}
52+
53+
return true;
54+
}
55+
}
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 <fabien@symfony.com>
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 <fabien@symfony.com>
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+
}
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 <fabien@symfony.com>
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 <fabien@symfony.com>
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+
*/
30+
public function __construct(array|string $ips)
31+
{
32+
$this->ips = array_reduce((array) $ips, static function (array $ips, string $ip) {
33+
return array_merge($ips, preg_split('/\s*,\s*/', $ip));
34+
}, []);
35+
}
36+
37+
public function matches(Request $request): bool
38+
{
39+
if (!$this->ips) {
40+
return true;
41+
}
42+
43+
return IpUtils::checkIp($request->getClientIp() ?? '', $this->ips);
44+
}
45+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Exception\JsonException;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
17+
18+
/**
19+
* Checks the Request content is valid JSON.
20+
*
21+
* @author Fabien Potencier <fabien@symfony.com>
22+
*/
23+
class IsJsonRequestMatcher implements RequestMatcherInterface
24+
{
25+
public function matches(Request $request): bool
26+
{
27+
try {
28+
$request->toArray();
29+
} catch (JsonException) {
30+
return false;
31+
}
32+
33+
return true;
34+
}
35+
}

0 commit comments

Comments
 (0)