Completed
Push — 1.x ( 0f325a...edaa8d )
by Joshua
07:46
created

RateLimitAnnotationListener::onKernelController()   C

Complexity

Conditions 9
Paths 16

Size

Total Lines 64
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9

Importance

Changes 9
Bugs 2 Features 3
Metric Value
c 9
b 2
f 3
dl 0
loc 64
ccs 27
cts 27
cp 1
rs 6.5449
cc 9
eloc 27
nc 16
nop 1
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Noxlogic\RateLimitBundle\EventListener;
4
5
use Noxlogic\RateLimitBundle\Annotation\RateLimit;
6
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
7
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
8
use Noxlogic\RateLimitBundle\Service\RateLimitService;
9
use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
10
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
11
use Symfony\Component\HttpFoundation\Request;
12
use Symfony\Component\HttpFoundation\Response;
13
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
14
use Symfony\Component\HttpKernel\HttpKernelInterface;
15
16
class RateLimitAnnotationListener extends BaseListener
17
{
18
19
    /**
20
     * @var eventDispatcherInterface
21
     */
22
    protected $eventDispatcher;
23
24
    /**
25
     * @var \Noxlogic\RateLimitBundle\Service\RateLimitService
26
     */
27
    protected $rateLimitService;
28
29
    /**
30
     * @var \Noxlogic\RateLimitBundle\Util\PathLimitProcessor
31
     */
32
    protected $pathLimitProcessor;
33
34
    /**
35
     * @param RateLimitService                    $rateLimitService
36
     */
37 16
    public function __construct(
38
        EventDispatcherInterface $eventDispatcher,
39
        RateLimitService $rateLimitService,
40
        PathLimitProcessor $pathLimitProcessor
41
    ) {
42 16
        $this->eventDispatcher = $eventDispatcher;
43 16
        $this->rateLimitService = $rateLimitService;
44 16
        $this->pathLimitProcessor = $pathLimitProcessor;
45 16
    }
46
47
    /**
48
     * @param FilterControllerEvent $event
49
     */
50 12
    public function onKernelController(FilterControllerEvent $event)
51
    {
52
        // Skip if we aren't the main request
53 12
        if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
54 1
            return;
55
        }
56
57
        // Skip if we are a closure
58 11
        if (! is_array($controller = $event->getController())) {
59 2
            return;
60
        }
61
62
        // Find the best match
63 10
        $annotations = $event->getRequest()->attributes->get('_x-rate-limit', array());
64 10
        $rateLimit = $this->findBestMethodMatch($event->getRequest(), $annotations);
65
66
        // No matching annotation found
67 10
        if (! $rateLimit) {
68 2
            return;
69
        }
70
71 8
        $key = $this->getKey($event, $rateLimit, $annotations);
72
73
        // Ratelimit the call
74 8
        $rateLimitInfo = $this->rateLimitService->limitRate($key);
75 8
        if (! $rateLimitInfo) {
76
            // Create new rate limit entry for this call
77 5
            $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
78 5
            if (! $rateLimitInfo) {
79
                // @codeCoverageIgnoreStart
80
                return;
81
                // @codeCoverageIgnoreEnd
82
            }
83 5
        }
84
85
86
        // Store the current rating info in the request attributes
87 8
        $request = $event->getRequest();
88 8
        $request->attributes->set('rate_limit_info', $rateLimitInfo);
89
90
        // Reset the rate limits
91 8
        if(time() >= $rateLimitInfo->getResetTimestamp()) {
92
            $this->rateLimitService->resetRate($key);
93
        }
94 4
95 1
        // When we exceeded our limit, return a custom error response
96 1
        if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) {
97
98
            // Throw an exception if configured.
99 3
            if ($this->getParameter('rate_response_exception')) {
100 3
                $class = $this->getParameter('rate_response_exception');
101 3
                throw new $class($this->getParameter('rate_response_message'), $this->getParameter('rate_response_code'));
102
            }
103
104
            $message = $this->getParameter('rate_response_message');
105 3
            $code = $this->getParameter('rate_response_code');
106 3
            $event->setController(function () use ($message, $code) {
107
                // @codeCoverageIgnoreStart
108 7
                return new Response($message, $code);
109
                // @codeCoverageIgnoreEnd
110
            });
111
        }
112
113
    }
114 14
115
116
    /**
117 14
     * @param RateLimit[] $annotations
118 4
     */
119
    protected function findBestMethodMatch(Request $request, array $annotations)
120
    {
121 10
        // Empty array, check the path limits
122 10
        if (count($annotations) == 0) {
123
            return $this->pathLimitProcessor->getRateLimit($request);
124 10
        }
125
126 10
        $best_match = null;
127 3
        foreach ($annotations as $annotation) {
128 3
            // cast methods to array, even method holds a string
129
            $methods = is_array($annotation->getMethods()) ? $annotation->getMethods() : array($annotation->getMethods());
130
131 10
            if (in_array($request->getMethod(), $methods)) {
132 8
                $best_match = $annotation;
133 8
            }
134 10
135
            // Only match "default" annotation when we don't have a best match
136 10
            if (count($annotation->getMethods()) == 0 && $best_match == null) {
137
                $best_match = $annotation;
138
            }
139 8
        }
140
141 8
        return $best_match;
142 8
    }
143
144 8
    private function getKey(FilterControllerEvent $event, RateLimit $rateLimit, array $annotations)
145 8
    {
146 8
        $request = $event->getRequest();
147 8
        $controller = $event->getController();
148
149
        $rateLimitMethods = join("", $rateLimit->getMethods());
150 8
        $rateLimitAlias = count($annotations) === 0
151
            ? $this->pathLimitProcessor->getMatchedPath($request)
152
            : get_class($controller[0]) . ':' . $controller[1];
153 8
154 8
        // Create an initial key by joining the methods and the alias
155
        $key = $rateLimitMethods . ':' . $rateLimitAlias ;
156 8
157
        // Let listeners manipulate the key
158
        $keyEvent = new GenerateKeyEvent($event->getRequest(), $key);
159
        $this->eventDispatcher->dispatch(RateLimitEvents::GENERATE_KEY, $keyEvent);
160
161
        return $keyEvent->getKey();
162
    }
163
}
164