This commit is contained in:
Chris
2018-10-18 15:59:38 +01:00
parent 4f6a0cb7c6
commit 380a0e8623
352 changed files with 32929 additions and 3604 deletions

View File

@@ -0,0 +1,200 @@
<?php
namespace Github\HttpClient;
use Http\Client\Common\HttpMethodsClient;
use Http\Client\Common\Plugin;
use Http\Client\Common\Plugin\Cache\Generator\HeaderCacheKeyGenerator;
use Http\Client\Common\PluginClientFactory;
use Http\Client\HttpClient;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
use Http\Message\MessageFactory;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Psr\Cache\CacheItemPoolInterface;
/**
* A builder that builds the API client.
* This will allow you to fluently add and remove plugins.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class Builder
{
/**
* The object that sends HTTP messages.
*
* @var HttpClient
*/
private $httpClient;
/**
* A HTTP client with all our plugins.
*
* @var HttpMethodsClient
*/
private $pluginClient;
/**
* @var MessageFactory
*/
private $requestFactory;
/**
* @var StreamFactory
*/
private $streamFactory;
/**
* True if we should create a new Plugin client at next request.
*
* @var bool
*/
private $httpClientModified = true;
/**
* @var Plugin[]
*/
private $plugins = [];
/**
* This plugin is special treated because it has to be the very last plugin.
*
* @var Plugin\CachePlugin
*/
private $cachePlugin;
/**
* Http headers.
*
* @var array
*/
private $headers = [];
/**
* @param HttpClient $httpClient
* @param RequestFactory $requestFactory
* @param StreamFactory $streamFactory
*/
public function __construct(
HttpClient $httpClient = null,
RequestFactory $requestFactory = null,
StreamFactory $streamFactory = null
) {
$this->httpClient = $httpClient ?: HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find();
$this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
}
/**
* @return HttpMethodsClient
*/
public function getHttpClient()
{
if ($this->httpClientModified) {
$this->httpClientModified = false;
$plugins = $this->plugins;
if ($this->cachePlugin) {
$plugins[] = $this->cachePlugin;
}
$this->pluginClient = new HttpMethodsClient(
(new PluginClientFactory())->createClient($this->httpClient, $plugins),
$this->requestFactory
);
}
return $this->pluginClient;
}
/**
* Add a new plugin to the end of the plugin chain.
*
* @param Plugin $plugin
*/
public function addPlugin(Plugin $plugin)
{
$this->plugins[] = $plugin;
$this->httpClientModified = true;
}
/**
* Remove a plugin by its fully qualified class name (FQCN).
*
* @param string $fqcn
*/
public function removePlugin($fqcn)
{
foreach ($this->plugins as $idx => $plugin) {
if ($plugin instanceof $fqcn) {
unset($this->plugins[$idx]);
$this->httpClientModified = true;
}
}
}
/**
* Clears used headers.
*/
public function clearHeaders()
{
$this->headers = [];
$this->removePlugin(Plugin\HeaderAppendPlugin::class);
$this->addPlugin(new Plugin\HeaderAppendPlugin($this->headers));
}
/**
* @param array $headers
*/
public function addHeaders(array $headers)
{
$this->headers = array_merge($this->headers, $headers);
$this->removePlugin(Plugin\HeaderAppendPlugin::class);
$this->addPlugin(new Plugin\HeaderAppendPlugin($this->headers));
}
/**
* @param string $header
* @param string $headerValue
*/
public function addHeaderValue($header, $headerValue)
{
if (!isset($this->headers[$header])) {
$this->headers[$header] = $headerValue;
} else {
$this->headers[$header] = array_merge((array) $this->headers[$header], [$headerValue]);
}
$this->removePlugin(Plugin\HeaderAppendPlugin::class);
$this->addPlugin(new Plugin\HeaderAppendPlugin($this->headers));
}
/**
* Add a cache plugin to cache responses locally.
*
* @param CacheItemPoolInterface $cachePool
* @param array $config
*/
public function addCache(CacheItemPoolInterface $cachePool, array $config = [])
{
if (!isset($config['cache_key_generator'])) {
$config['cache_key_generator'] = new HeaderCacheKeyGenerator(['Authorization', 'Cookie', 'Accept', 'Content-type']);
}
$this->cachePlugin = Plugin\CachePlugin::clientCache($cachePool, $this->streamFactory, $config);
$this->httpClientModified = true;
}
/**
* Remove the cache plugin.
*/
public function removeCache()
{
$this->cachePlugin = null;
$this->httpClientModified = true;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Github\HttpClient\Message;
use Github\Exception\ApiLimitExceedException;
use Psr\Http\Message\ResponseInterface;
class ResponseMediator
{
/**
* @param ResponseInterface $response
*
* @return array|string
*/
public static function getContent(ResponseInterface $response)
{
$body = $response->getBody()->__toString();
if (strpos($response->getHeaderLine('Content-Type'), 'application/json') === 0) {
$content = json_decode($body, true);
if (JSON_ERROR_NONE === json_last_error()) {
return $content;
}
}
return $body;
}
/**
* @param ResponseInterface $response
*
* @return array|null
*/
public static function getPagination(ResponseInterface $response)
{
if (!$response->hasHeader('Link')) {
return;
}
$header = self::getHeader($response, 'Link');
$pagination = [];
foreach (explode(',', $header) as $link) {
preg_match('/<(.*)>; rel="(.*)"/i', trim($link, ','), $match);
if (3 === count($match)) {
$pagination[$match[2]] = $match[1];
}
}
return $pagination;
}
/**
* @param ResponseInterface $response
*
* @return null|string
*/
public static function getApiLimit(ResponseInterface $response)
{
$remainingCalls = self::getHeader($response, 'X-RateLimit-Remaining');
if (null !== $remainingCalls && 1 > $remainingCalls) {
throw new ApiLimitExceedException($remainingCalls);
}
return $remainingCalls;
}
/**
* Get the value for a single header.
*
* @param ResponseInterface $response
* @param string $name
*
* @return string|null
*/
public static function getHeader(ResponseInterface $response, $name)
{
$headers = $response->getHeader($name);
return array_shift($headers);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Github\HttpClient\Plugin;
use Github\Client;
use Github\Exception\RuntimeException;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Add authentication to the request.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class Authentication implements Plugin
{
private $tokenOrLogin;
private $password;
private $method;
public function __construct($tokenOrLogin, $password, $method)
{
$this->tokenOrLogin = $tokenOrLogin;
$this->password = $password;
$this->method = $method;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
switch ($this->method) {
case Client::AUTH_HTTP_PASSWORD:
$request = $request->withHeader(
'Authorization',
sprintf('Basic %s', base64_encode($this->tokenOrLogin.':'.$this->password))
);
break;
case Client::AUTH_HTTP_TOKEN:
$request = $request->withHeader('Authorization', sprintf('token %s', $this->tokenOrLogin));
break;
case Client::AUTH_URL_CLIENT_ID:
$uri = $request->getUri();
$query = $uri->getQuery();
$parameters = [
'client_id' => $this->tokenOrLogin,
'client_secret' => $this->password,
];
$query .= empty($query) ? '' : '&';
$query .= utf8_encode(http_build_query($parameters, '', '&'));
$uri = $uri->withQuery($query);
$request = $request->withUri($uri);
break;
case Client::AUTH_URL_TOKEN:
$uri = $request->getUri();
$query = $uri->getQuery();
$parameters = ['access_token' => $this->tokenOrLogin];
$query .= empty($query) ? '' : '&';
$query .= utf8_encode(http_build_query($parameters, '', '&'));
$uri = $uri->withQuery($query);
$request = $request->withUri($uri);
break;
case Client::AUTH_JWT:
$request = $request->withHeader('Authorization', sprintf('Bearer %s', $this->tokenOrLogin));
break;
default:
throw new RuntimeException(sprintf('%s not yet implemented', $this->method));
break;
}
return $next($request);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Github\HttpClient\Plugin;
use Github\Exception\ApiLimitExceedException;
use Github\Exception\ErrorException;
use Github\Exception\RuntimeException;
use Github\Exception\TwoFactorAuthenticationRequiredException;
use Github\Exception\ValidationFailedException;
use Github\HttpClient\Message\ResponseMediator;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class GithubExceptionThrower implements Plugin
{
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
return $next($request)->then(function (ResponseInterface $response) use ($request) {
if ($response->getStatusCode() < 400 || $response->getStatusCode() > 600) {
return $response;
}
// If error:
$remaining = ResponseMediator::getHeader($response, 'X-RateLimit-Remaining');
if (null != $remaining && 1 > $remaining && 'rate_limit' !== substr($request->getRequestTarget(), 1, 10)) {
$limit = ResponseMediator::getHeader($response, 'X-RateLimit-Limit');
$reset = ResponseMediator::getHeader($response, 'X-RateLimit-Reset');
throw new ApiLimitExceedException($limit, $reset);
}
if (401 === $response->getStatusCode()) {
if ($response->hasHeader('X-GitHub-OTP') && 0 === strpos((string) ResponseMediator::getHeader($response, 'X-GitHub-OTP'), 'required;')) {
$type = substr((string) ResponseMediator::getHeader($response, 'X-GitHub-OTP'), 9);
throw new TwoFactorAuthenticationRequiredException($type);
}
}
$content = ResponseMediator::getContent($response);
if (is_array($content) && isset($content['message'])) {
if (400 == $response->getStatusCode()) {
throw new ErrorException($content['message'], 400);
} elseif (422 == $response->getStatusCode() && isset($content['errors'])) {
$errors = [];
foreach ($content['errors'] as $error) {
switch ($error['code']) {
case 'missing':
$errors[] = sprintf('The %s %s does not exist, for resource "%s"', $error['field'], $error['value'], $error['resource']);
break;
case 'missing_field':
$errors[] = sprintf('Field "%s" is missing, for resource "%s"', $error['field'], $error['resource']);
break;
case 'invalid':
if (isset($error['message'])) {
$errors[] = sprintf('Field "%s" is invalid, for resource "%s": "%s"', $error['field'], $error['resource'], $error['message']);
} else {
$errors[] = sprintf('Field "%s" is invalid, for resource "%s"', $error['field'], $error['resource']);
}
break;
case 'already_exists':
$errors[] = sprintf('Field "%s" already exists, for resource "%s"', $error['field'], $error['resource']);
break;
default:
$errors[] = $error['message'];
break;
}
}
throw new ValidationFailedException('Validation Failed: '.implode(', ', $errors), 422);
}
}
throw new RuntimeException(isset($content['message']) ? $content['message'] : $content, $response->getStatusCode());
});
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Github\HttpClient\Plugin;
use Http\Client\Common\Plugin\Journal;
use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A plugin to remember the last response.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class History implements Journal
{
/**
* @var ResponseInterface
*/
private $lastResponse;
/**
* @return ResponseInterface|null
*/
public function getLastResponse()
{
return $this->lastResponse;
}
public function addSuccess(RequestInterface $request, ResponseInterface $response)
{
$this->lastResponse = $response;
}
public function addFailure(RequestInterface $request, Exception $exception)
{
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Github\HttpClient\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Prepend the URI with a string.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class PathPrepend implements Plugin
{
private $path;
/**
* @param string $path
*/
public function __construct($path)
{
$this->path = $path;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$currentPath = $request->getUri()->getPath();
if (strpos($currentPath, $this->path) !== 0) {
$uri = $request->getUri()->withPath($this->path.$currentPath);
$request = $request->withUri($uri);
}
return $next($request);
}
}