Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"license": "MIT",
"require": {
"php": ">=5.4",
"guzzle/http": ">=3.6.0,<4"
"guzzlehttp/guzzle": "~6.3.3",
"ext-json": "*"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though ext-json is present by default, it is apparently best practice to explicitly list it as a requirement if you use things like json_decode.

},
"require-dev": {
"phpunit/phpunit": "4.3.*"
"phpunit/phpunit": "~7.5.16"
},
"autoload": {
"psr-0": {
Expand Down
187 changes: 104 additions & 83 deletions src/ChartBlocks/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,33 @@

namespace ChartBlocks;

use Guzzle\Http\Client as HttpClient;
use Guzzle\Http\Message\Request as HttpRequest;
use ChartBlocks\Repository\Chart;
use ChartBlocks\Repository\ChartData;
use ChartBlocks\Repository\DataSet;
use ChartBlocks\Repository\Profile;
use ChartBlocks\Repository\RepositoryInterface;
use ChartBlocks\Repository\SessionToken;
use ChartBlocks\Repository\Statistics;
use ChartBlocks\Repository\User;
use Closure;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use RuntimeException;
use function GuzzleHttp\choose_handler;

/**
* Client for managing connection to ChartBlocks REST API
*
* @param \ChartBlocks\Repository\Chart $chart
* @param \ChartBlocks\Repository\DataSet $dataSet
* @param \ChartBlocks\Repository\ChartData $chartData
* @param \ChartBlocks\Repository\Profile $profile
* @param \ChartBlocks\Repository\SessionToken $sessionToken
* @param \ChartBlocks\Repository\Statistics $statistics
* @param \ChartBlocks\Repository\User $user
* @param Chart $chart
* @param DataSet $dataSet
* @param ChartData $chartData
* @param Profile $profile
* @param SessionToken $sessionToken
* @param Statistics $statistics
* @param User $user
*
*/
class Client {
Expand All @@ -30,7 +44,6 @@ class Client {

protected $config;
protected $signature;
protected $exceptionHandler;
protected $httpClient;
protected $defaultApiUrl = 'https://api.chartblocks.com/v1/';
protected $repositories = array(
Expand All @@ -55,13 +68,12 @@ public function __construct(array $config = array()) {
/**
*
* @param string $name
* @return \ChartBlocks\Repository\RepositoryInterface
* @throws Exception
* @return RepositoryInterface
*/
public function getRepository($name) {
$repo = lcfirst(trim($name));
if (false === array_key_exists($repo, $this->repositories)) {
throw new \InvalidArgumentException("Repository $repo does not exist");
throw new InvalidArgumentException("Repository $repo does not exist");
}

if (is_string($this->repositories[$repo])) {
Expand All @@ -76,7 +88,7 @@ public function getRepository($name) {
* Tries to load a repository using the syntax $this->chart
*
* @param string $name
* @return \ChartBlocks\Repository\RepositoryInterface
* @return RepositoryInterface
* @throws Exception
*/
public function __get($name) {
Expand All @@ -91,7 +103,8 @@ public function __get($name) {
*
* @return string
*/
public function getApiUrl() {
public function getApiUrl(): string
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's quite a few changes in this PR that are the result of hitting the "Cleanup code" button in PHPStorm which basically amounts to adding a return type signature to functions. Just a heads up.

{
if (array_key_exists('api_url', $this->config)) {
return $this->parseApiUrl($this->config['api_url']);
}
Expand All @@ -104,11 +117,13 @@ public function getApiUrl() {
return $this->defaultApiUrl;
}

protected function parseApiUrl($url) {
protected function parseApiUrl($url): string
{
return rtrim($url, '/') . '/';
}

protected function parseApiPath($path) {
protected function parseApiPath($path): string
{
return ltrim($path, '/');
}

Expand Down Expand Up @@ -148,86 +163,64 @@ public function getAuthSecret() {

public function get($uri, array $params = array()) {
$path = $this->parseApiPath($uri);
$request = $this->getHttpClient()->get($path);
foreach ($params as $key => $value) {
$request->getQuery()->set($key, $value);
}
$response = $this->getHttpClient()->get($path, [
'query' => $params
]);

$response = $request->send();
return $response->json();
return json_decode($response->getBody(), true);
}

public function put($uri, $data = array()) {
$path = $this->parseApiPath($uri);
$json = empty($data) ? null : json_encode($data);

$request = $this->getHttpClient()->put($path, null, $json);
$response = $request->send();
return $response->json();
$response = $this->getHttpClient()->put($path, [
'body' => $json
]);

return json_decode($response->getBody(), true);
}

public function post($uri, $data = array()) {
$path = $this->parseApiPath($uri);
$json = empty($data) ? null : json_encode($data);

$request = $this->getHttpClient()->post($path, null, $json);
$response = $request->send();
$response = $this->getHttpClient()->post($path, [
'body' => $json
]);

return $response->json();
return json_decode($response->getBody(), true);
}

public function delete($uri, $data = array()) {
$json = empty($data) ? null : json_encode($data);

$path = $this->parseApiPath($uri);
$request = $this->getHttpClient()->delete($path, null, $json);
$response = $this->getHttpClient()->delete($path, [
'body' => $json
]);

$response = $request->send();
return $response->json();
return json_decode($response->getBody(), true);
}

public function postFile($uri, $file, $contentType = null) {
$path = $this->parseApiPath($uri);
$request = $this->getHttpClient()->post($path);

$request->addPostFile('upload', $file, $contentType);

$response = $request->send();
return $response->json();
}

/**
*
* @param \Guzzle\Http\Message\Request $request
* @throws Exception
*/
public function bindAuth(HttpRequest $request) {
$token = $this->getAuthToken();
$secret = $this->getAuthSecret();

if ($token && $secret) {
$signature = $this->getSignature()->fromRequest($request, $secret);
$request->setHeader('Authorization', 'Basic ' . base64_encode($token . ':' . $signature));
} elseif ($token xor $secret) {
throw new \RuntimeException('Both token and secret must be set');
}
$body = Psr7\Utils::streamFor(fopen($file, 'r'));
$response = $this->getHttpClient()->post($path, [
'body' => $body,
'headers' => [
'Content-Type' => $contentType ?: 'application/octet-stream'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this default content type automatically set behind the scenes before? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually know, because the API docs for guzzle3 have gone offline, I can't find a version on archive.org that has documentation for Guzzle's version of addPostFile, and the the old PHP docs for the built-in version of addPostFile mark contentType as optional. BUT, I do see in the archived Guzzle docs that it had a protected guessContentType() function, so my best guess is that yes, it was at least trying to set something behind the scenes. And application/octet-stream is the content type for "I don't really know what this is, here's a bunch of binary data", so it seemed like the most appropriate option.

Either way, pragmatically, it doesn't seem like we're actually going to run into a case where contentType is not supplied to this function.

]
]);
return json_decode($response->getBody(), true);
}

/**
*
* @param \Guzzle\Http\Message\Request $request
* @return \ChartBlocks\Client
* @return Signature
*/
public function bindAccept(HttpRequest $request) {
$request->setHeader('Accept', 'application/json');
return $this;
}

/**
*
* @return \ChartBlocks\Signature
*/
public function getSignature() {
public function getSignature(): Signature
{
if ($this->signature === null) {
$this->signature = new Signature();
}
Expand All @@ -236,10 +229,11 @@ public function getSignature() {
}

/**
*
* @return \Guzzle\Http\Client
*
* @return HttpClient
*/
public function getHttpClient() {
public function getHttpClient(): HttpClient
{
if ($this->httpClient === null) {
$this->httpClient = $this->createHttpClient();
}
Expand All @@ -250,27 +244,54 @@ public function getHttpClient() {
/**
*
* @param array $config
* @return \ChartBlocks\Client
* @return Client
*/
protected function setConfig(array $config) {
protected function setConfig(array $config): Client
{
$this->config = $config;
return $this;
}

/**
*
* @return \Guzzle\Http\Client
*
* @return HttpClient
*/
protected function createHttpClient() {
$client = new HttpClient($this->getApiUrl(), array());

$that = $this;
$client->getEventDispatcher()->addListener('request.before_send', function($event) use ($that) {
$that->bindAccept($event['request']);
$that->bindAuth($event['request']);
});
protected function createHttpClient(): HttpClient
{
$stack = new HandlerStack();
$stack->setHandler(choose_handler());
$stack->push($this->handleHeaders());

return new HttpClient([
'base_uri' => $this->getApiUrl(),
'handler' => $stack,
'headers' => [
'Accept' => 'application/json'
]
]);
}

return $client;
/**
* @return Closure
* @throws RuntimeException
*/
public function handleHeaders(): Closure
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
$token = $this->getAuthToken();
$secret = $this->getAuthSecret();

if ($token && $secret) {
$signature = $this->getSignature()->fromRequest($request, $secret);
$request = $request->withHeader('Authorization', 'Basic ' . base64_encode($token . ':' . $signature));
} elseif ($token xor $secret) {
throw new RuntimeException('Both token and secret must be set');
}

return $handler($request, $options);
};
};
}

}
Loading