Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0109fe9

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

29 files changed

+895
-41
lines changed

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

+58-9
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

+36-19
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

+3-3
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

+1-1
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

+3
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
---
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 matches 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+
}

src/Symfony/Component/HttpFoundation/ExpressionRequestMatcher.php

+5
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
{

src/Symfony/Component/HttpFoundation/RequestMatcher.php

+4
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
{
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+
}
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+
}

0 commit comments

Comments
 (0)