1 | <?php |
||||||
2 | |||||||
3 | namespace BenTools\FunnelHttpClient; |
||||||
4 | |||||||
5 | use BenTools\FunnelHttpClient\Storage\ArrayStorage; |
||||||
6 | use BenTools\FunnelHttpClient\Storage\ThrottleStorageInterface; |
||||||
7 | use BenTools\FunnelHttpClient\Strategy\AlwaysThrottleStrategy; |
||||||
8 | use BenTools\FunnelHttpClient\Strategy\ThrottleStrategyInterface; |
||||||
9 | use LogicException; |
||||||
10 | use Psr\Log\LoggerInterface; |
||||||
11 | use Psr\Log\NullLogger; |
||||||
12 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; |
||||||
13 | use Symfony\Contracts\HttpClient\HttpClientInterface; |
||||||
14 | use Symfony\Contracts\HttpClient\ResponseInterface; |
||||||
15 | use Symfony\Contracts\HttpClient\ResponseStreamInterface; |
||||||
16 | |||||||
17 | use function method_exists; |
||||||
18 | |||||||
19 | final class FunnelHttpClient implements HttpClientInterface |
||||||
20 | { |
||||||
21 | public function __construct( |
||||||
22 | private HttpClientInterface $decorated, |
||||||
23 | private ThrottleStorageInterface $throttleStorage, |
||||||
24 | private ?ThrottleStrategyInterface $throttleStrategy = null, |
||||||
25 | private ?LoggerInterface $logger = null |
||||||
26 | ) { |
||||||
27 | $this->throttleStrategy = $throttleStrategy ?? new AlwaysThrottleStrategy(); |
||||||
28 | $this->logger = $logger ?? new NullLogger(); |
||||||
29 | } |
||||||
30 | |||||||
31 | public function withOptions(array $options): static |
||||||
32 | { |
||||||
33 | // SF 5+ compatibility |
||||||
34 | if (!method_exists($this->decorated, 'withOptions')) { |
||||||
35 | throw new LogicException('%s doesn\'t implement `withOptions`.'); |
||||||
36 | } |
||||||
37 | |||||||
38 | $clone = clone $this; |
||||||
39 | $clone->decorated = $clone->decorated->withOptions($options); |
||||||
40 | |||||||
41 | return $clone; |
||||||
42 | } |
||||||
43 | |||||||
44 | /** |
||||||
45 | * @inheritDoc |
||||||
46 | */ |
||||||
47 | public function request(string $method, string $url, array $options = []): ResponseInterface |
||||||
48 | { |
||||||
49 | if (!$this->throttleStrategy->shouldThrottle($method, $url, $options)) { |
||||||
0 ignored issues
–
show
|
|||||||
50 | return $this->decorated->request($method, $url, $options); |
||||||
51 | } |
||||||
52 | |||||||
53 | if (0 === $this->throttleStorage->getRemainingCalls()) { |
||||||
54 | $this->waitUntilReady($method, $url); |
||||||
55 | } |
||||||
56 | |||||||
57 | $this->throttleStorage->increment(); |
||||||
58 | try { |
||||||
59 | $response = $this->decorated->request($method, $url, $options); |
||||||
60 | } catch (TransportExceptionInterface $e) { |
||||||
61 | $this->throttleStorage->decrement(); |
||||||
62 | throw $e; |
||||||
63 | } |
||||||
64 | |||||||
65 | return $response; |
||||||
66 | } |
||||||
67 | |||||||
68 | /** |
||||||
69 | * @inheritDoc |
||||||
70 | */ |
||||||
71 | public function stream($responses, float $timeout = null): ResponseStreamInterface |
||||||
72 | { |
||||||
73 | return $this->decorated->stream($responses, $timeout); |
||||||
74 | } |
||||||
75 | |||||||
76 | /** |
||||||
77 | * @param string $method |
||||||
78 | * @param string $url |
||||||
79 | */ |
||||||
80 | private function waitUntilReady(string $method, string $url): void |
||||||
81 | { |
||||||
82 | $remainingSeconds = $this->throttleStorage->getRemainingTime(); |
||||||
83 | $this->logger->info(\sprintf('Max requests / window reached. Waiting %s seconds...', $remainingSeconds), ['method' => $method, 'url' => $url]); |
||||||
0 ignored issues
–
show
The method
info() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
84 | |||||||
85 | if (0 === ($remainingSeconds <=> (int) $remainingSeconds)) { |
||||||
86 | \sleep((int) $remainingSeconds); |
||||||
87 | } else { |
||||||
88 | \usleep((int) \round($remainingSeconds * 1000000)); |
||||||
89 | } |
||||||
90 | } |
||||||
91 | |||||||
92 | /** |
||||||
93 | * @param HttpClientInterface $client |
||||||
94 | * @param int $maxRequests |
||||||
95 | * @param float $timeWindow |
||||||
96 | * @param LoggerInterface|null $logger |
||||||
97 | * @return FunnelHttpClient |
||||||
98 | */ |
||||||
99 | public static function throttle(HttpClientInterface $client, int $maxRequests, float $timeWindow, ?LoggerInterface $logger = null): self |
||||||
100 | { |
||||||
101 | return new self($client, new ArrayStorage($maxRequests, $timeWindow), null, $logger); |
||||||
102 | } |
||||||
103 | } |
||||||
104 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.