Skip to content
This repository has been archived by the owner on Sep 12, 2024. It is now read-only.

HTTP Client #2

Merged
merged 6 commits into from
Apr 26, 2022
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ jobs:

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest

- name: Run tests
run: ./vendor/bin/phpunit
24 changes: 23 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@
}
],
"require": {
"php": ">= 7.4"
"php": ">= 7.4",
"ext-json": "*",
"php-http/client-common": "^2.3",
"php-http/discovery": "^1.12",
"php-http/httplug": "^2.2",
"php-http/multipart-stream-builder": "^1.1.2",
"psr/cache": "^1.0 || ^2.0",
"psr/http-client-implementation": "^1.0",
"psr/http-factory-implementation": "^1.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"guzzlehttp/psr7": "^1.7",
"http-interop/http-factory-guzzle": "^1.0",
"guzzlehttp/guzzle": "^7.2",
"php-http/mock-client": "^1.4.1",
"phpunit/phpunit": "^8.5 || ^9.4"
},
"autoload": {
"psr-4": { "Duffel\\": "src/Duffel/" }
},
"autoload-dev": {
"psr-4": { "Duffel\\Tests\\": "tests/Duffel/Tests/"}
}
}
31 changes: 31 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="vendor/autoload.php">
<coverage includeUncoveredFiles="true" processUncoveredFiles="true">
<report>
<html outputDirectory="html-coverage" lowUpperBound="50" highLowerBound="90"/>
<text outputFile="coverage.txt" showUncoveredFiles="false" showOnlySummary="true"/>
</report>
<include>
<directory suffix=".php">./src/Duffel/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="duffel-api-php Test Suite">
<directory>./tests/Duffel/</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>integration</group>
</exclude>
</groups>
</phpunit>
93 changes: 93 additions & 0 deletions src/Duffel/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace Duffel;

use Duffel\Exception\InvalidAccessTokenException;
use Duffel\HttpClient\Builder;
use Http\Client\Common\HttpMethodsClientInterface;
use Http\Client\Common\Plugin\AddHostPlugin;
use Http\Client\Common\Plugin\AuthenticationPlugin;
use Http\Client\Common\Plugin\HeaderDefaultsPlugin;
use Http\Client\Common\Plugin\HistoryPlugin;
use Http\Client\Common\Plugin\RedirectPlugin;
use Http\Client\Common\Plugin\RetryPlugin;
use Http\Message\Authentication\Bearer;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;

class Client {
private const DEFAULT_API_URL = 'https://api.duffel.com/';
private const DEFAULT_API_VERSION = 'beta';
private const VERSION = '0.0.0-alpha';

private $accessToken;
private $apiUrl;
private $apiVersion;
private $httpClientBuilder;

public function __construct(Builder $httpClientBuilder = null) {
$this->httpClientBuilder = $builder = $httpClientBuilder ?? new Builder();
$this->apiUrl = self::DEFAULT_API_URL;
$this->apiVersion = self::DEFAULT_API_VERSION;

$builder->addPlugin(new AuthenticationPlugin(new Bearer($this->getAccessToken())));
$builder->addPlugin(new HeaderDefaultsPlugin($this->getDefaultHeaders()));

$this->setUrl($this->apiUrl);
}

public function getAccessToken() {
if (is_null($this->accessToken)) {
return $this->accessToken;
} else {
return str_replace('input! ', '', $this->accessToken);
}
}

public function setAccessToken(string $token) {
if ('' !== trim($token) && strlen(trim($token)) > 0) {
$this->accessToken = 'input! ' . $token;
} else {
throw new InvalidAccessTokenException("You need to set a token");
}

$authentication = new Bearer('token');
}

public function getApiVersion(): string {
return $this->apiVersion;
}

public function setApiVersion(string $apiVersion): void {
$this->apiVersion = $apiVersion;
}

public function setUrl(string $url): void {
$uri = $this->getHttpClientBuilder()->getUriFactory()->createUri($url);

$this->getHttpClientBuilder()->removePlugin(AddHostPlugin::class);
$this->getHttpClientBuilder()->addPlugin(new AddHostPlugin($uri));
}

public function getHttpClient(): HttpMethodsClientInterface {
return $this->getHttpClientBuilder()->getHttpClient();
}

protected function getHttpClientBuilder(): Builder {
return $this->httpClientBuilder;
}
private function getDefaultHeaders(): array {
return array(
"Duffel-Version" => $this->apiVersion,
"Content-Type" => "application/json",
"User-Agent" => $this->getUserAgent(),
);
}

private function getUserAgent(): string {
return "Duffel/" . $this->apiVersion . " " . "duffel_api_php/" . self::VERSION;
}
}
8 changes: 8 additions & 0 deletions src/Duffel/Exception/InvalidAccessTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

namespace Duffel\Exception;

class InvalidAccessTokenException extends \Exception {
}
82 changes: 82 additions & 0 deletions src/Duffel/HttpClient/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Duffel\HttpClient;

use Http\Client\Common\HttpMethodsClient;
use Http\Client\Common\HttpMethodsClientInterface;
use Http\Client\Common\Plugin;
use Http\Client\Common\Plugin\Cache\Generator\HeaderCacheKeyGenerator;
use Http\Client\Common\Plugin\CachePlugin;
use Http\Client\Common\PluginClientFactory;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;

final class Builder {
private $httpClient;
private $requestFactory;
private $streamFactory;
private $uriFactory;
private $cachePlugin;
private $plugins = [];
private $pluginClient;

public function __construct(
ClientInterface $httpClient = null,
RequestFactoryInterface $requestFactory = null,
StreamFactoryInterface $streamFactory = null,
UriFactoryInterface $uriFactory = null
) {
$this->httpClient = $httpClient ?? Psr18ClientDiscovery::find();
$this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory();
$this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
$this->uriFactory = $uriFactory ?? Psr17FactoryDiscovery::findUriFactory();
}

public function getHttpClient(): HttpMethodsClientInterface {
if (null === $this->pluginClient) {
$plugins = $this->plugins;

$this->pluginClient = new HttpMethodsClient(
(new PluginClientFactory())->createClient($this->httpClient, $plugins),
$this->requestFactory,
$this->streamFactory
);
}

return $this->pluginClient;
}

public function getRequestFactory(): RequestFactoryInterface {
return $this->requestFactory;
}

public function getStreamFactory(): StreamFactoryInterface {
return $this->streamFactory;
}

public function getUriFactory(): UriFactoryInterface {
return $this->uriFactory;
}

public function addPlugin(Plugin $plugin): void {
$this->plugins[] = $plugin;
$this->pluginClient = null;
}

public function removePlugin(string $fqcn): void
{
foreach ($this->plugins as $idx => $plugin) {
if ($plugin instanceof $fqcn) {
unset($this->plugins[$idx]);
$this->pluginClient = null;
}
}
}
}
58 changes: 58 additions & 0 deletions tests/Duffel/ClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Duffel\Tests;

use Duffel\Client;
use Duffel\Exception\InvalidAccessTokenException;
use Duffel\HttpClient\Builder;
use Http\Client\Common\HttpMethodsClient;
use PHPUnit\Framework\TestCase;

class ClientTest extends TestCase {
private $subject;

public function setUp(): void {
$this->subject = new Client(
new Builder(),
);
}

public function testCreatesNewClient(): void {
$this->assertInstanceOf(Client::class, $this->subject);
$this->assertInstanceOf(HttpMethodsClient::class, $this->subject->getHttpClient());
}

public function testNewSetsDefaultApiVersion(): void {
$this->assertSame('beta', $this->subject->getApiVersion());
}

public function testNewSetsDefaultAccessToken(): void {
$this->assertSame(null, $this->subject->getAccessToken());
}

public function testSetAccessTokenChangesValue(): void {
$this->subject->setAccessToken('some-token');

$this->assertSame('some-token', $this->subject->getAccessToken());
}

public function testSetAccessTokenWithEmptyStringThrowsException(): void {
$this->expectException(InvalidAccessTokenException::class);

$this->subject->setAccessToken(' ');
}

public function testSetAccessTokenWithNullThrowsTypeError(): void {
$this->expectException(\TypeError::class);

$this->subject->setAccessToken(null);
}

public function testSetApiVersionChangesValue(): void {
$this->subject->setApiVersion('some-version');

$this->assertSame('some-version', $this->subject->getApiVersion());
}
}
61 changes: 61 additions & 0 deletions tests/Duffel/HttpClient/BuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Duffel\Tests\HttpClient;

use Duffel\HttpClient\Builder;
use Http\Client\Common\HttpMethodsClientInterface;
use Http\Client\Common\Plugin;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;

class BuilderTest extends TestCase {
private $subject;

public function setUp(): void {
$this->subject = new Builder(
$this->createMock(ClientInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
$this->createMock(UriFactoryInterface::class)
);
}

public function testAddPluginShouldInvalidateHttpClient(): void {
$client = $this->subject->getHttpClient();

$this->subject->addPlugin($this->createMock(Plugin::class));

$this->assertNotSame($client, $this->subject->getHttpClient());
}

public function testRemovePluginShouldInvalidateHttpClient(): void {
$this->subject->addPlugin($this->createMock(Plugin::class));

$client = $this->subject->getHttpClient();

$this->subject->removePlugin(Plugin::class);

$this->assertNotSame($client, $this->subject->getHttpClient());
}

public function testHttpClientShouldBeHttpMethodsClient(): void {
$this->assertInstanceOf(HttpMethodsClientInterface::class, $this->subject->getHttpClient());
}

public function testRequestFactoryShouldBeRequestFactory(): void {
$this->assertInstanceOf(RequestFactoryInterface::class, $this->subject->getRequestFactory());
}

public function testStreamFactoryShouldBeStreamFactory(): void {
$this->assertInstanceOf(StreamFactoryInterface::class, $this->subject->getStreamFactory());
}

public function testUriFactoryShouldBeUriFactory(): void {
$this->assertInstanceOf(UriFactoryInterface::class, $this->subject->getUriFactory());
}
}