Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Harm Smits committed Oct 20, 2020
0 parents commit dc8219f
Show file tree
Hide file tree
Showing 18 changed files with 1,237 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor
32 changes: 32 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "best-brands/kiyoh-api",
"type": "library",
"description": "A PHP Client for the Kiyoh API with async support and full object mapping",
"keywords": [
"api",
"php",
"kiyoh"
],
"homepage": "http://github.com/best-brands/kiyoh-client",
"license": "MIT",
"authors": [
{
"name": "Harm Smits",
"email": "[email protected]"
}
],
"require": {
"php": ">=7.4",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.2.0"
},
"require-dev": {
"nette/php-generator": "^3.3",
"phpunit/phpunit": "^9.1"
},
"autoload": {
"psr-4": {
"BestBrands\\KiyohClient\\": "src/"
}
}
}
42 changes: 42 additions & 0 deletions example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

use BestBrands\KiyohClient\Client;
use BestBrands\KiyohClient\Exceptions\KiyohException;
use BestBrands\KiyohClient\Models\Invitee;

require "vendor/autoload.php";

$client = new Client(...);
$location_id = ...;
/**
* Send an invite to a client
*/
try {
if ($client->invite((new Invitee())
->setInviteEmail('[email protected]')
->setDelay(1)
->setFirstName('Harm')
->setLastName('Smits')
->setLocationId($location_id)
->setRefCode(...)
)->getCode() === 'A') {
echo 'Invite is scheduled';
} else {
echo 'Invite is not scheduled';
}
} catch (KiyohException $exception) {
echo 'Invite is not scheduled';
}

/**
* Iterate over the reviews
*/
($date = new DateTime())->setDate(2020, 01, 01);
foreach ($client->getReviews($location_id, $date)->getReviews() as $review)
echo "Review written by: " . $review->getReviewAuthor() . "\n";

/**
* Get location statistics
*/
$stats = $client->getLocationStatistics($location_id);
echo "Average rating of last 12 months: " . $stats->getLast12MonthAverageRating() . "\n";
219 changes: 219 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

namespace BestBrands\KiyohClient;

use BestBrands\KiyohClient\Exceptions\KiyohException;
use BestBrands\KiyohClient\Exceptions\Notice;
use BestBrands\KiyohClient\Models\Invitee;
use BestBrands\KiyohClient\Models\Location;
use BestBrands\KiyohClient\Models\LocationStatistics;
use BestBrands\KiyohClient\Models\ReducedLocation;
use BestBrands\KiyohClient\Models\Response;
use Closure;
use DateTime;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Throwable;

/**
* Class Client
* @package BestBrands\KiyohClient
*
* @method ReducedLocation[] getLatestStatistics(DateTime $since, ?DateTime $updated_since = null, int $limit = 50)
* @method PromiseInterface getLatestStatisticsAsync(DateTime $since, ?DateTime $updated_since = null, int $limit = 50)
* @method Location getReviews(string $locationId, DateTime $since, array $params = [])
* @method PromiseInterface getReviewsAsync(string $locationId, DateTime $since, array $params = [])
* @method string getLastRemovedReview(DateTime $updatedSince)
* @method PromiseInterface getLastRemovedReviewAsync(DateTime $updatedSince)
* @method LocationStatistics getLocationStatistics(string $locationId)
* @method PromiseInterface getLocationStatisticsAsync(string $locationId)
* @method Response invite(Invitee $invitee)
* @method PromiseInterface inviteAsync(Invitee $invitee)
*/
class Client
{
/** @var string base url */
const API_URL_KIYOH_COM = 'https://www.kiyoh.com/';

/** @var string base url */
const API_URL_KLANTEN_VERTELLEN = 'https://www.klantenvertellen.nl';

/** @var string[] all the base urls that are allowed */
private const BASE_URLS = [
self::API_URL_KIYOH_COM,
self::API_URL_KLANTEN_VERTELLEN,
];

/** @var \GuzzleHttp\Client */
protected \GuzzleHttp\Client $client;

/** @var Request request dispatcher */
protected Request $request;

/** @var Populator */
protected Populator $populator;

/** @var string holds the current API client id */
protected string $token;

/** @var string holds the base url for the api requests */
protected string $baseUrl;

/**
* Client constructor.
*
* @param string $token
* @param string $base_url
* @throws Notice
*/
public function __construct(string $token, string $base_url = self::API_URL_KIYOH_COM)
{
$stack = HandlerStack::create();
$stack->push(Middleware::mapRequest($this->getRequestHandler()));
$stack->push(Middleware::retry($this->getRetryHandler()));

if (!in_array(($this->baseUrl = $base_url), self::BASE_URLS))
throw new Notice('Unrecognized base url');

$this->token = $token;
$this->client = new \GuzzleHttp\Client([
'handler' => $stack,
'base_url' => $this->baseUrl,
'timeout' => 1
]);
$this->request = new Request();
$this->populator = new Populator();
}

/**
* Unwrap an array of async requests, very useful when stacking multiple
*
* @param array $promises
*
* @return array
* @throws Throwable
*/
public function unwrap(array $promises)
{
return Utils::unwrap($promises);
}

/**
* Get the retry handler
*
* @return Closure
*/
private function getRetryHandler()
{
return function ($retries, ?RequestInterface $request, ?ResponseInterface $response, ?RequestException $exception) {
if ($response && $response->getStatusCode() !== 200)
throw new KiyohException(json_decode($response->getBody(), true));

return false;
};
}

/**
* Get the request handler
*
* @return Closure
*/
private function getRequestHandler()
{
return function (RequestInterface $request) {
return $request->withHeader('X-Publication-Api-Token', $this->token);
};
}

/**
* Dispatch the request. This is some serious sorcery not to be touched.
*
* @param string $method
* @param array $args
*
* @return array|string|Promise
* @throws GuzzleException|KiyohException
*/
public function __call(string $method, array $args)
{
if ($async = (substr($method, -5) === 'Async'))
$method = substr($method, 0, -5);

[$method, $url, $data, $response] = call_user_func_array([$this->request, $method], $args);

return ($async)
? $this->handleAsyncRequest($method, $url, $data, $response)
: $this->handleRequest($method, $url, $data, $response);
}

/**
* Handle a non-blocking request
*
* @param $method
* @param $url
* @param $data
* @param $responseFormat
*
* @return PromiseInterface
*/
private function handleAsyncRequest($method, $url, $data, array $responseFormat): PromiseInterface
{
return $this->client->requestAsync($method, $url, $data)
->then(function (ResponseInterface $response) use (&$responseFormat) {
return $this->handleResponse($response, $responseFormat);
});
}

/**
* Handle a blocking request
*
* @param string $method
* @param string $url
* @param array $data
* @param array $responseFormat
*
* @return array|mixed|StreamInterface
* @throws GuzzleException|KiyohException
*/
private function handleRequest(string $method, string $url, array $data, array $responseFormat)
{
try {
$result = $this->client->request($method, $url, $data);
} catch (RequestException $exception) {
return $this->handleResponse($exception->getResponse(), $responseFormat);
}

return $this->handleResponse($result, $responseFormat);
}

/**
* Handle the response accordingly
*
* @param ResponseInterface $response
* @param array $responseFormat
*
* @return array|mixed|StreamInterface
* @throws KiyohException
*/
private function handleResponse(ResponseInterface $response, array $responseFormat)
{
if ($response->getStatusCode() !== 200) {
throw new KiyohException(json_decode($response->getBody(), true));
} else if ($responseFormat && isset($responseFormat[$response->getStatusCode()])) {
return $this->populator->populate(
$responseFormat[$response->getStatusCode()],
json_decode($response->getBody(), true)
);
}

return $response->getBody();
}
}
7 changes: 7 additions & 0 deletions src/Exceptions/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace BestBrands\KiyohClient\Exceptions;

class InvalidArgumentException extends \InvalidArgumentException
{
}
63 changes: 63 additions & 0 deletions src/Exceptions/KiyohException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace BestBrands\KiyohClient\Exceptions;

use Exception;
use Throwable;

/**
* Class KiyohException
* @package BestBrands\KiyohClient\Exceptions
*/
class KiyohException extends Exception
{
protected array $rawError;

/**
* KiyohException constructor.
*
* @param array $message
* @param int $code
*
*
* @param Throwable|null $previous
*/
public function __construct(array $message, $code = 0, Throwable $previous = null)
{
$this->rawError = $message;
parent::__construct($this->getIndentMessage($message), $code, $previous);
}

/**
* Creates a formatted message from an array
*
* @param array $message
* @param int $indent
*
* @return string
*/
public function getIndentMessage(array $message, int $indent = 0)
{
$formatted = '';

foreach ($message as $key => $value) {
if (is_array($value)) {
$formatted .= $this->getIndentMessage($value, $indent + 4);
} else {
$formatted .= sprintf("%s%s: %s\n", str_repeat(" ", $indent), $key, $value);
}
}

return $formatted;
}

/**
* Get the raw error
*
* @return array
*/
public function getRawError(): array
{
return $this->rawError;
}
}
11 changes: 11 additions & 0 deletions src/Exceptions/Notice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace BestBrands\KiyohClient\Exceptions;

/**
* Class Notice
* @package BestBrands\KiyohClient\Exceptions
*/
class Notice extends \Exception
{
}
Loading

0 comments on commit dc8219f

Please sign in to comment.