bpolaszek /
funnel-http-client
| 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. Loading history...
|
|||||||
| 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.