Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
94.74% covered (success)
94.74%
18 / 19
92.31% covered (success)
92.31%
12 / 13
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserRateLimitSubscriber
100.00% covered (success)
100.00%
23 / 23
94.74% covered (success)
94.74%
18 / 19
92.31% covered (success)
92.31%
12 / 13
100.00% covered (success)
100.00%
6 / 6
11.06
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMainRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 routeShouldBeLimited
100.00% covered (success)
100.00%
5 / 5
83.33% covered (warning)
83.33%
5 / 6
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 limitShouldBeApplied
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 onKernelController
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getSubscribedEvents
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\User\Infrastructure\EventSubscriber;
4
5use Symfony\Bundle\SecurityBundle\Security;
6use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpKernel\Event\ControllerEvent;
9use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
10use Symfony\Component\HttpKernel\HttpKernelInterface;
11use Symfony\Component\HttpKernel\KernelEvents;
12use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
13
14/**
15 * This rate limit applies to the DoctrineUser 'slice' only
16 * Other slices may have their own subscribers (or not)
17 * TODO : maybe we should consider the rate limit a global feature instead or relying on each slice to implement it
18 * Although it may seem 'cleaner' to have each slice implement it, it may result in code duplication
19 * Moreover, should slices become isolated micro services, the rate limiting would probably be handled differently
20 * We should make a decision, but it's not that important at the moment
21 *
22 * @author Wilhelm Zwertvaegher
23 *
24 */
25readonly class UserRateLimitSubscriber implements EventSubscriberInterface
26{
27    public function __construct(
28        private Security                    $security,
29        private RateLimiterFactoryInterface $publicApiByIpLimiter,
30        private RateLimiterFactoryInterface $apiByUserLimiter
31    ) {
32    }
33
34    public function isMainRequest(ControllerEvent $event): bool
35    {
36        return $event->getRequestType() === HttpKernelInterface::MAIN_REQUEST;
37    }
38
39    public function routeShouldBeLimited(Request $request): bool
40    {
41        $routesToLimit = [
42            'api_user_me',
43        ];
44        $route = $request->attributes->get('_route');
45        return $route && in_array($route, $routesToLimit, true);
46    }
47
48    public function limitShouldBeApplied(ControllerEvent $event): bool
49    {
50        return $this->isMainRequest($event) && $this->routeShouldBeLimited($event->getRequest());
51    }
52
53    public function onKernelController(ControllerEvent $event): void
54    {
55        if (!$this->limitShouldBeApplied($event)) {
56            return;
57        }
58
59        $request = $event->getRequest();
60        $user = $this->security->getUser();
61
62        $factory = $this->publicApiByIpLimiter;
63        $key = $request->getClientIp();
64
65        if ($user) {
66            $factory = $this->apiByUserLimiter;
67            $key = $user->getUserIdentifier();
68        }
69        $limiter = $factory->create($key);
70        if (!$limiter->consume()->isAccepted()) {
71            throw new TooManyRequestsHttpException();
72        }
73    }
74
75    public static function getSubscribedEvents(): array
76    {
77        return [
78            KernelEvents::CONTROLLER => 'onKernelController',
79        ];
80    }
81}