|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Bedrock\Bundle\RateLimitBundle\EventListener; |
| 4 | + |
| 5 | +use Bedrock\Bundle\RateLimitBundle\Annotation\GraphQLRateLimit as GraphQLRateLimitAnnotation; |
| 6 | +use Bedrock\Bundle\RateLimitBundle\Model\RateLimit; |
| 7 | +use Bedrock\Bundle\RateLimitBundle\RateLimitModifier\RateLimitModifierInterface; |
| 8 | +use Doctrine\Common\Annotations\Reader; |
| 9 | +use GraphQL\Language\AST\OperationDefinitionNode; |
| 10 | +use GraphQL\Language\Parser; |
| 11 | +use GraphQL\Language\Source; |
| 12 | +use Symfony\Component\DependencyInjection\ContainerInterface; |
| 13 | +use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
| 14 | +use Symfony\Component\HttpKernel\Event\ControllerEvent; |
| 15 | + |
| 16 | +class ReadGraphQLRateLmitAnnotationListener implements EventSubscriberInterface |
| 17 | +{ |
| 18 | + private Reader $annotationReader; |
| 19 | + /** @var iterable<RateLimitModifierInterface> */ |
| 20 | + private $rateLimitModifiers; |
| 21 | + private int $limit; |
| 22 | + private int $period; |
| 23 | + private ContainerInterface $container; |
| 24 | + |
| 25 | + /** |
| 26 | + * @param RateLimitModifierInterface[] $rateLimitModifiers |
| 27 | + */ |
| 28 | + public function __construct(ContainerInterface $container, Reader $annotationReader, iterable $rateLimitModifiers, int $limit, int $period) |
| 29 | + { |
| 30 | + foreach ($rateLimitModifiers as $rateLimitModifier) { |
| 31 | + if (!($rateLimitModifier instanceof RateLimitModifierInterface)) { |
| 32 | + throw new \InvalidArgumentException(('$rateLimitModifiers must be instance of '.RateLimitModifierInterface::class)); |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + $this->annotationReader = $annotationReader; |
| 37 | + $this->rateLimitModifiers = $rateLimitModifiers; |
| 38 | + $this->limit = $limit; |
| 39 | + $this->period = $period; |
| 40 | + $this->container = $container; |
| 41 | + } |
| 42 | + |
| 43 | + public function onKernelController(ControllerEvent $event): void |
| 44 | + { |
| 45 | + $request = $event->getRequest(); |
| 46 | + // retrieve controller and method from request |
| 47 | + $controllerAttribute = $request->attributes->get('_controller', null); |
| 48 | + if (null === $controllerAttribute || !is_string($controllerAttribute)) { |
| 49 | + return; |
| 50 | + } |
| 51 | + // services alias can be used with 'service.alias:functionName' or 'service.alias::functionName' |
| 52 | + $controllerAttributeParts = explode(':', str_replace('::', ':', $controllerAttribute)); |
| 53 | + $controllerName = $controllerAttributeParts[0] ?? ''; |
| 54 | + $methodName = $controllerAttributeParts[1] ?? null; |
| 55 | + |
| 56 | + if (!class_exists($controllerName)) { |
| 57 | + // If controller attribute is an alias instead of a class name |
| 58 | + if (null === ($controllerName = $this->container->get($controllerAttributeParts[0]))) { |
| 59 | + throw new \InvalidArgumentException('Parameter _controller from request : "'.$controllerAttribute.'" do not contains a valid class name'); |
| 60 | + } |
| 61 | + } |
| 62 | + $reflection = new \ReflectionClass($controllerName); |
| 63 | + $annotation = $this->annotationReader->getMethodAnnotation($reflection->getMethod((string) ($methodName ?? '__invoke')), GraphQLRateLimitAnnotation::class); |
| 64 | + |
| 65 | + if (!$annotation instanceof GraphQLRateLimitAnnotation) { |
| 66 | + return; |
| 67 | + } |
| 68 | + |
| 69 | + if (!class_exists('GraphQL\Language\Parser')) { |
| 70 | + throw new \Exception('Run "composer require webonyx/graphql-php" to use @GraphQLRateLimit annotation.'); |
| 71 | + } |
| 72 | + |
| 73 | + $endpoint = $this->extractQueryName($request->request->get('query')); |
| 74 | + |
| 75 | + foreach ($annotation->getEndpointConfigurations() as $graphQLEndpointConfiguration) { |
| 76 | + if ($endpoint === $graphQLEndpointConfiguration->getEndpoint()) { |
| 77 | + $rateLimit = new RateLimit( |
| 78 | + $graphQLEndpointConfiguration->getLimit() ?? $this->limit, |
| 79 | + $graphQLEndpointConfiguration->getPeriod() ?? $this->period |
| 80 | + ); |
| 81 | + $rateLimit->varyHashOn('_graphql_endpoint', $endpoint); |
| 82 | + break; |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + if (!isset($rateLimit)) { |
| 87 | + return; |
| 88 | + } |
| 89 | + |
| 90 | + foreach ($this->rateLimitModifiers as $hashKeyVarier) { |
| 91 | + if ($hashKeyVarier->support($request)) { |
| 92 | + $hashKeyVarier->modifyRateLimit($request, $rateLimit); |
| 93 | + } |
| 94 | + } |
| 95 | + $request->attributes->set('_rate_limit', $rateLimit); |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * @return array<string, string> |
| 100 | + */ |
| 101 | + public static function getSubscribedEvents(): array |
| 102 | + { |
| 103 | + return [ |
| 104 | + ControllerEvent::class => 'onKernelController', |
| 105 | + ]; |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * @param string|int|float|bool|null $query |
| 110 | + */ |
| 111 | + public function extractQueryName($query): string |
| 112 | + { |
| 113 | + /** @var Source $query */ |
| 114 | + $parsedQuery = Parser::parse($query); |
| 115 | + /** @var OperationDefinitionNode $item */ |
| 116 | + foreach ($parsedQuery->definitions->getIterator() as $item) { |
| 117 | + /* @phpstan-ignore-next-line */ |
| 118 | + return (string) $item->selectionSet->selections[0]->name->value; |
| 119 | + } |
| 120 | + |
| 121 | + throw new QueryExtractionException('Unable to extract query'); |
| 122 | + } |
| 123 | +} |
0 commit comments