Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 961cd85

Browse filesBrowse files
committed
[HttpFoundation] Extract request matchers for better reusability
1 parent 58117d7 commit 961cd85
Copy full SHA for 961cd85
Expand file treeCollapse file tree

29 files changed

+925
-41
lines changed

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+58-9Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@
3333
use Symfony\Component\EventDispatcher\EventDispatcher;
3434
use Symfony\Component\ExpressionLanguage\Expression;
3535
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
36-
use Symfony\Component\HttpFoundation\RequestMatcher;
36+
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
37+
use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher;
38+
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
39+
use Symfony\Component\HttpFoundation\RequestMatcher\IpsRequestMatcher;
40+
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
41+
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
42+
use Symfony\Component\HttpFoundation\RequestMatcher\PortRequestMatcher;
3743
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
3844
use Symfony\Component\HttpKernel\KernelEvents;
3945
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
@@ -893,7 +899,7 @@ private function createRequestMatcher(ContainerBuilder $container, string $path
893899
$methods = array_map('strtoupper', $methods);
894900
}
895901

896-
if (null !== $ips) {
902+
if ($ips) {
897903
foreach ($ips as $ip) {
898904
$container->resolveEnvPlaceholders($ip, null, $usedEnvs);
899905

@@ -905,22 +911,65 @@ private function createRequestMatcher(ContainerBuilder $container, string $path
905911
}
906912
}
907913

908-
$id = '.security.request_matcher.'.ContainerBuilder::hash([$path, $host, $port, $methods, $ips, $attributes]);
914+
$id = '.security.request_matcher.'.ContainerBuilder::hash([ChainRequestMatcher::class, $path, $host, $port, $methods, $ips, $attributes]);
909915

910916
if (isset($this->requestMatchers[$id])) {
911917
return $this->requestMatchers[$id];
912918
}
913919

914-
// only add arguments that are necessary
915-
$arguments = [$path, $host, $methods, $ips, $attributes, null, $port];
916-
while (\count($arguments) > 0 && !end($arguments)) {
917-
array_pop($arguments);
920+
$arguments = [];
921+
if ($methods && !$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([MethodRequestMatcher::class, $methods]))) {
922+
$container->register($lid, MethodRequestMatcher::class)
923+
->setPublic(false)
924+
->setArguments([$methods])
925+
;
926+
$arguments[] = new Reference($lid);
927+
}
928+
929+
if ($path && !$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([PathRequestMatcher::class, $path]))) {
930+
$container->register($lid, PathRequestMatcher::class)
931+
->setPublic(false)
932+
->setArguments([$path])
933+
;
934+
$arguments[] = new Reference($lid);
935+
}
936+
937+
if ($host && !$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([HostRequestMatcher::class, $host]))) {
938+
$container->register($lid, HostRequestMatcher::class)
939+
->setPublic(false)
940+
->setArguments([$host])
941+
;
942+
$arguments[] = new Reference($lid);
943+
}
944+
945+
if ($ips && !$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([IpsRequestMatcher::class, $ips]))) {
946+
$container->register($lid, IpsRequestMatcher::class)
947+
->setPublic(false)
948+
->setArguments([$ips])
949+
;
950+
$arguments[] = new Reference($lid);
951+
}
952+
953+
if ($attributes && !$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([AttributesRequestMatcher::class, $attributes]))) {
954+
$container->register($lid, AttributesRequestMatcher::class)
955+
->setPublic(false)
956+
->setArguments([$attributes])
957+
;
958+
$arguments[] = new Reference($lid);
959+
}
960+
961+
if ($port && !$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([PortRequestMatcher::class, $port]))) {
962+
$container->register($lid, PortRequestMatcher::class)
963+
->setPublic(false)
964+
->setArguments([$port])
965+
;
966+
$arguments[] = new Reference($lid);
918967
}
919968

920969
$container
921-
->register($id, RequestMatcher::class)
970+
->register($id, ChainRequestMatcher::class)
922971
->setPublic(false)
923-
->setArguments($arguments)
972+
->setArguments([$arguments])
924973
;
925974

926975
return $this->requestMatchers[$id] = new Reference($id);

‎src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
+36-19Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
use Symfony\Component\DependencyInjection\ContainerBuilder;
2020
use Symfony\Component\DependencyInjection\Definition;
2121
use Symfony\Component\DependencyInjection\Reference;
22+
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
23+
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
24+
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
25+
use Symfony\Component\HttpFoundation\RequestMatcher\PortRequestMatcher;
2226
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
2327
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
2428
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
@@ -248,20 +252,27 @@ public function testFirewallRequestMatchers()
248252
foreach ($arguments[1]->getValues() as $reference) {
249253
if ($reference instanceof Reference) {
250254
$definition = $container->getDefinition((string) $reference);
251-
$matchers[] = $definition->getArguments();
255+
$matchers[] = $definition->getArgument(0);
252256
}
253257
}
254258

255-
$this->assertEquals([
256-
[
257-
'/login',
258-
],
259-
[
260-
'/test',
261-
'foo\\.example\\.org',
262-
['GET', 'POST'],
263-
],
264-
], $matchers);
259+
$this->assertCount(2, $matchers);
260+
261+
$this->assertCount(1, $matchers[0]);
262+
$def = $container->getDefinition((string) $matchers[0][0]);
263+
$this->assertSame(PathRequestMatcher::class, $def->getClass());
264+
$this->assertSame('/login', $def->getArgument(0));
265+
266+
$this->assertCount(3, $matchers[1]);
267+
$def = $container->getDefinition((string) $matchers[1][0]);
268+
$this->assertSame(MethodRequestMatcher::class, $def->getClass());
269+
$this->assertSame(['GET', 'POST'], $def->getArgument(0));
270+
$def = $container->getDefinition((string) $matchers[1][1]);
271+
$this->assertSame(PathRequestMatcher::class, $def->getClass());
272+
$this->assertSame('/test', $def->getArgument(0));
273+
$def = $container->getDefinition((string) $matchers[1][2]);
274+
$this->assertSame(HostRequestMatcher::class, $def->getClass());
275+
$this->assertSame('foo\\.example\\.org', $def->getArgument(0));
265276
}
266277

267278
public function testUserCheckerAliasIsRegistered()
@@ -294,17 +305,23 @@ public function testAccess()
294305
if (1 === $i) {
295306
$this->assertEquals(['ROLE_USER'], $attributes);
296307
$this->assertEquals('https', $channel);
297-
$this->assertEquals(
298-
['/blog/524', null, ['GET', 'POST'], [], [], null, 8000],
299-
$requestMatcher->getArguments()
300-
);
308+
$this->assertCount(3, $requestMatcher->getArgument(0));
309+
$def = $container->getDefinition((string) $requestMatcher->getArgument(0)[0]);
310+
$this->assertSame(MethodRequestMatcher::class, $def->getClass());
311+
$this->assertSame(['GET', 'POST'], $def->getArgument(0));
312+
$def = $container->getDefinition((string) $requestMatcher->getArgument(0)[1]);
313+
$this->assertSame(PathRequestMatcher::class, $def->getClass());
314+
$this->assertSame('/blog/524', $def->getArgument(0));
315+
$def = $container->getDefinition((string) $requestMatcher->getArgument(0)[2]);
316+
$this->assertSame(PortRequestMatcher::class, $def->getClass());
317+
$this->assertSame(8000, $def->getArgument(0));
301318
} elseif (2 === $i) {
302319
$this->assertEquals(['IS_AUTHENTICATED_ANONYMOUSLY'], $attributes);
303320
$this->assertNull($channel);
304-
$this->assertEquals(
305-
['/blog/.*'],
306-
$requestMatcher->getArguments()
307-
);
321+
$this->assertCount(1, $requestMatcher->getArgument(0));
322+
$def = $container->getDefinition((string) $requestMatcher->getArgument(0)[0]);
323+
$this->assertSame(PathRequestMatcher::class, $def->getClass());
324+
$this->assertSame('/blog/.*', $def->getArgument(0));
308325
} elseif (3 === $i) {
309326
$this->assertEquals('IS_AUTHENTICATED_ANONYMOUSLY', $attributes[0]);
310327
$expression = $container->getDefinition((string) $attributes[1])->getArgument(0);

‎src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
use Symfony\Component\DependencyInjection\Reference;
2727
use Symfony\Component\ExpressionLanguage\Expression;
2828
use Symfony\Component\HttpFoundation\Request;
29-
use Symfony\Component\HttpFoundation\RequestMatcher;
29+
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
3030
use Symfony\Component\HttpFoundation\Response;
3131
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
3232
use Symfony\Component\Security\Core\Exception\AuthenticationException;
@@ -256,7 +256,7 @@ public function testRegisterAccessControlWithSpecifiedRequestMatcherService()
256256
$container = $this->getRawContainer();
257257

258258
$requestMatcherId = 'My\Test\RequestMatcher';
259-
$requestMatcher = new RequestMatcher('/');
259+
$requestMatcher = new PathRequestMatcher('/');
260260
$container->set($requestMatcherId, $requestMatcher);
261261

262262
$container->loadFromExtension('security', [
@@ -291,7 +291,7 @@ public function testRegisterAccessControlWithRequestMatcherAndAdditionalOptionsT
291291
$container = $this->getRawContainer();
292292

293293
$requestMatcherId = 'My\Test\RequestMatcher';
294-
$requestMatcher = new RequestMatcher('/');
294+
$requestMatcher = new PathRequestMatcher('/');
295295
$container->set($requestMatcherId, $requestMatcher);
296296

297297
$container->loadFromExtension('security', [

‎src/Symfony/Bundle/SecurityBundle/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/composer.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"symfony/dependency-injection": "^6.2",
2424
"symfony/event-dispatcher": "^5.4|^6.0",
2525
"symfony/http-kernel": "^6.2",
26-
"symfony/http-foundation": "^5.4|^6.0",
26+
"symfony/http-foundation": "^6.2",
2727
"symfony/password-hasher": "^5.4|^6.0",
2828
"symfony/security-core": "^6.2",
2929
"symfony/security-csrf": "^5.4|^6.0",

‎src/Symfony/Component/HttpFoundation/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/CHANGELOG.md
+3Lines 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
---
+38Lines 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 <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;
13+
14+
/**
15+
* ChainRequestMatcher verifies that all checks matches against a Request instance.
16+
*
17+
* @author Fabien Potencier <fabien@symfony.com>
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+
}

‎src/Symfony/Component/HttpFoundation/ExpressionRequestMatcher.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/ExpressionRequestMatcher.php
+5Lines 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 <fabien@symfony.com>
24+
*
25+
* @deprecated since Symfony 6.2, use "Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher" instead
2126
*/
2227
class ExpressionRequestMatcher extends RequestMatcher
2328
{

‎src/Symfony/Component/HttpFoundation/RequestMatcher.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/RequestMatcher.php
+4Lines 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 <fabien@symfony.com>
20+
*
21+
* @deprecated since Symfony 6.2, use ChainRequestMatcher instead
1822
*/
1923
class RequestMatcher implements RequestMatcherInterface
2024
{
+45Lines 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\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+
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+
}
+43Lines 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 <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\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 <fabien@symfony.com>
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+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.