Skip to content

Instantly share code, notes, and snippets.

@jderusse
Created June 3, 2019 12:23
Show Gist options
  • Save jderusse/20ce9747e7dcefa9515b4bb17eb7ff2a to your computer and use it in GitHub Desktop.
Save jderusse/20ce9747e7dcefa9515b4bb17eb7ff2a to your computer and use it in GitHub Desktop.
Cancelable HttpClient
<?php
$responses = [];
$responses[] = $fooResponse = $this->client->request('GET', '/foo');
$responses[] = $barResponse = $this->client->request('GET', '/foo');
$responses[] = $bazResponse = $this->client->request('GET', '/foo');
// ...
if (/* ... */) {
// cancel pending requests
array_walk($responses, function (CancelableResponseInterface $response): void {
$response->cancel();
});
}
<?php
namespace App\HttpClient;
class CanceledException extends \RuntimeException
{
}
<?php
namespace App\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class CancellableHttpClient implements CancellableHttpClientInterface
{
private $decorated;
public function __construct(HttpClientInterface $decorated)
{
$this->decorated = $decorated;
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$canceler = new ResponseCanceler();
$onProgress = $options['on_progress'] ?? null;
$options['on_progress'] = function ($cur, $tot, array $infos) use ($onProgress, $canceler): void {
if ($canceler->isCanceled()) {
throw new CanceledException();
}
$onProgress && $onProgress($cur, $tot, $infos);
};
return new CancellableHttpResponse($this->decorated->request($method, $url, $options), $canceler);
}
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->decorated->stream($responses, $timeout);
}
}
<?php
namespace App\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
interface CancellableHttpClientInterface extends HttpClientInterface
{
/**
* TODO replace this docBlock by typeHint with covariance in php 7.4
*
* @return CancellableResponseInterface
*/
public function request(string $method, string $url, array $options = []): ResponseInterface;
}
<?php
namespace App\HttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class CancellableHttpResponse implements CancellableResponseInterface
{
private $decorated;
private $canceler;
public function __construct(ResponseInterface $decorated, ResponseCanceler $canceler)
{
$this->decorated = $decorated;
$this->canceler = $canceler;
}
public function __destruct()
{
try {
$this->decorated = null;
} catch (HttpExceptionInterface $e) {
if (!$this->canceler->isCanceled()) {
throw $e;
}
} catch (TransportException $e) {
if (!$e->getPrevious() instanceof CanceledException) {
throw $e;
}
}
}
public function getStatusCode(): int
{
return $this->decorated->getStatusCode();
}
public function getHeaders(bool $throw = true): array
{
return $this->decorated->getHeaders($throw);
}
public function getContent(bool $throw = true): string
{
return $this->decorated->getContent($throw);
}
public function toArray(bool $throw = true): array
{
return $this->decorated->toArray($throw);
}
public function getInfo(string $type = null)
{
return $this->decorated->getInfo($type);
}
public function cancel(): void
{
$this->canceler->cancel();
}
}
<?php
namespace App\HttpClient;
use Symfony\Contracts\HttpClient\ResponseInterface;
interface CancellableResponseInterface extends ResponseInterface
{
public function cancel(): void;
}
<?php
namespace App\HttpClient;
class ResponseCanceler
{
private $canceled = false;
public function cancel(): void
{
$this->canceled = true;
}
public function isCanceled(): bool
{
return $this->canceled;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment