Last active
January 2, 2020 18:28
-
-
Save collegeman/ed7969038cece0d0db57bff451bef7bf to your computer and use it in GitHub Desktop.
Using a trait to add an API client to any PHP class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Tests\Feature; | |
use App\Concerns\MakesRequests; | |
class HttpBinClient | |
{ | |
use MakesRequests; | |
protected $config = [ | |
'base_uri' => 'https://httpbin.org', | |
]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Adapters; | |
use Psr\Http\Message\ResponseInterface; | |
use Psr\Http\Message\StreamInterface; | |
class HttpResponse implements ResponseInterface | |
{ | |
/** | |
* @var ResponseInterface | |
*/ | |
protected $proxied; | |
/** | |
* Contains parsed JSON content | |
* @var mixed | |
*/ | |
protected $parsedJsonContent; | |
function __construct(ResponseInterface $response) | |
{ | |
$this->proxied = $response; | |
} | |
public function containsJson() | |
{ | |
if ($this->proxied->hasHeader('Content-Type')) { | |
$contentType = $this->proxied->getHeader('Content-Type'); | |
return in_array('application/json', $contentType) | |
|| in_array('application/json; charset=utf-8', $contentType); | |
} | |
return false; | |
} | |
/** | |
* @return $this | |
*/ | |
public function parseJson() | |
{ | |
if (is_null($this->parsedJsonContent)) { | |
if ($this->containsJson()) { | |
$this->parsedJsonContent = json_decode($this->proxied->getBody()->getContents()); | |
} | |
} | |
return $this; | |
} | |
public function json($key = null) | |
{ | |
$json = $this->parseJson()->parsedJsonContent; | |
return $key ? data_get($json, $key) : $json; | |
} | |
/** | |
* @param null $key | |
* @return \Illuminate\Support\Collection | |
*/ | |
public function collect($key = null) | |
{ | |
return collect($key ? data_get($this->json(), $key) : $this->json()); | |
} | |
public function getProtocolVersion() | |
{ | |
return $this->proxied->getProtocolVersion(); | |
} | |
public function withProtocolVersion($version) | |
{ | |
return $this->withProtocolVersion($version); | |
} | |
public function getHeaders() | |
{ | |
return $this->proxied->getHeaders(); | |
} | |
public function hasHeader($name) | |
{ | |
return $this->proxied->hasHeader($name); | |
} | |
public function getHeader($name) | |
{ | |
return $this->proxied->getHeader($name); | |
} | |
public function getHeaderLine($name) | |
{ | |
return $this->proxied->getHeaderLine($name); | |
} | |
public function withHeader($name, $value) | |
{ | |
return $this->proxied->withHeader($name, $value); | |
} | |
public function withAddedHeader($name, $value) | |
{ | |
return $this->proxied->withAddedHeader($name, $value); | |
} | |
public function withoutHeader($name) | |
{ | |
return $this->proxied->withoutHeader($name); | |
} | |
public function getBody() | |
{ | |
return $this->proxied->getBody(); | |
} | |
public function withBody(StreamInterface $body) | |
{ | |
return $this->proxied->withBody($body); | |
} | |
public function getStatusCode() | |
{ | |
return $this->proxied->getStatusCode(); | |
} | |
public function withStatus($code, $reasonPhrase = '') | |
{ | |
return $this->proxied->withStatus($code, $reasonPhrase); | |
} | |
public function getReasonPhrase() | |
{ | |
return $this->proxied->getReasonPhrase(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Concerns; | |
use App\Adapters\HttpResponse; | |
use GuzzleHttp\Client; | |
use GuzzleHttp\Exception\RequestException; | |
use GuzzleHttp\Psr7\Response; | |
trait MakesRequests | |
{ | |
/** | |
* @var Client | |
*/ | |
protected $httpClient = null; | |
/** | |
* @param Client $client | |
*/ | |
function setHttpClient(Client $client) | |
{ | |
$this->httpClient = $client; | |
} | |
/** | |
* @return Client | |
*/ | |
function getHttpClient() | |
{ | |
if (is_null($this->httpClient)) { | |
$this->httpClient = new Client($this->getHttpConfig()); | |
} | |
return $this->httpClient; | |
} | |
/** | |
* @return array | |
*/ | |
function getHttpConfig() | |
{ | |
$config = []; | |
if (isset($this->httpConfig)) { | |
$config = $this->httpConfig; | |
} else if (isset($this->config)) { | |
$config = $this->config; | |
} | |
return $config; | |
} | |
/** | |
* Make an HTTP request. | |
* @param $method | |
* @param $args | |
* @return HttpResponse | |
* @throws RequestException | |
* @throws \Throwable | |
*/ | |
function __call($method, $args) | |
{ | |
return $this->request($method, $args); | |
} | |
/** | |
* @param $method | |
* @param $args | |
* @return HttpResponse | |
* @throws \Throwable | |
*/ | |
function request($method, $args) | |
{ | |
$method = strtoupper(trim($method)); | |
$uri = $args[0] ?? ''; | |
$options = $args[1] ?? []; | |
try { | |
$response = $this->getHttpClient()->request($method, $uri, $options); | |
} catch (\Throwable $e) { | |
$this->handleError($e); | |
} | |
return $this->handleResponse($response); | |
} | |
/** | |
* @param \Throwable $e | |
* @return mixed | |
* @throws \Throwable | |
*/ | |
function handleError(\Throwable $e) | |
{ | |
throw $e; | |
} | |
/** | |
* @param Response $response | |
* @return HttpResponse | |
*/ | |
function handleResponse(Response $response) | |
{ | |
return new HttpResponse($response); | |
} | |
/** | |
* Retry an operation a given number of times, allowing | |
* for 500, 502, 503, 504, 507, and 508 response codes | |
* and denying retries for all others, e.g., 404. | |
* | |
* @param int $times | |
* @param callable $callback | |
* @param int $sleep | |
* @param callable $when | |
* @return mixed | |
* | |
* @throws \Exception | |
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status | |
*/ | |
function retry($times, callable $callback, $sleep = 0, $when = null) | |
{ | |
// only certain types of HTTP responses | |
$allow = [500, 502, 503, 504, 507, 508]; | |
return retry($times, $callback, $sleep, function($e) use ($allow, $when) { | |
// allow individual invocations to override $when behavior | |
if ($when && !$when($e)) { | |
// don't allow retry | |
return false; | |
} | |
// only retry certain status codes | |
if ($e instanceof RequestException) { | |
if ($e->getResponse() && !in_array($e->getCode(), $allow)) { | |
// don't allow retry | |
return false; | |
} | |
} | |
// allow retry | |
return true; | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Tests\Feature; | |
use Tests\TestCase; | |
class RequestsTests extends TestCase | |
{ | |
/** | |
* A basic feature test example. | |
* | |
* @return void | |
* @group requests | |
*/ | |
public function testBasicRequests() | |
{ | |
$httpBin = new HttpBinClient(); | |
$response = $httpBin->post('/anything', [ | |
'form_params' => [ | |
'foo' => 'bar', | |
] | |
])->json(); | |
$this->assertTrue($response instanceof \stdClass); | |
$this->assertEquals('bar', data_get($response, 'form.foo')); | |
$response = $httpBin->post('/anything', [ | |
'json' => [ | |
'bing' => 'cherry', | |
] | |
])->json(); | |
$this->assertEquals('cherry', data_get($response, 'json.bing')); | |
$response = $httpBin->get('/anything?query=arg')->json(); | |
$this->assertEquals('arg', data_get($response, 'args.query')); | |
$response = $httpBin->get('/anything', [ | |
'query' => [ | |
'query' => 'arg', | |
] | |
])->json(); | |
$this->assertEquals('arg', data_get($response, 'args.query')); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment