Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract the Guzzle dependency to a HttpClient implementation #230

Closed
wants to merge 12 commits into from
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/.github
/tests export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/Makefile
/phpunit.xml export-ignore
/scoper.inc.php export-ignore
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mollie-api-php.zip:
# First, install all dependencies. Then prefix everything with humbug/php-scoper. Finally, we should dump the
# autoloader again to update the autoloader with the new classnames.
#
composer require php-http/guzzle6-adapter
composer install --no-dev --no-scripts --no-suggest
$(shell composer global config bin-dir --absolute)/php-scoper add-prefix --force
composer dump-autoload --working-dir build --classmap-authoritative
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ To use the Mollie API client, the following things are required:
+ Follow [a few steps](https://www.mollie.com/dashboard/?modal=onboarding) to enable payment methods in live mode, and let us handle the rest.
+ PHP >= 5.6
+ Up-to-date OpenSSL (or other SSL/TLS toolkit)
+ An HTTP client which implements the ``php-http/client-implementation``. You can find the available implementations [here](https://packagist.org/providers/php-http/client-implementation). We recommend ``php-http/guzzle6-adapter``.

## Composer Installation ##

By far the easiest way to install the Mollie API client is to require it with [Composer](http://getcomposer.org/doc/00-intro.md).
By far the easiest way to install the Mollie API client is to require it with [Composer](http://getcomposer.org/doc/00-intro.md).

$ composer require mollie/mollie-api-php:^2.0
The Mollie client depends on a ``php-http/client-implementation`` so make sure to install a package providing that.
We suggest using ``php-http/guzzle6-adapter``.

{
"require": {
"mollie/mollie-api-php": "^2.0"
}
}
```console
$ composer require mollie/mollie-api-php:^2.1 php-http/guzzle6-adapter:^1.0
```

The version of the API client corresponds to the version of the API it implements. Check the [notes on migration](https://docs.mollie.com/migrating-v1-to-v2) to see what changes you need to make if you want to start using a newer API version.

Expand Down
10 changes: 8 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,17 @@
"ext-curl": "*",
"ext-json": "*",
"ext-openssl": "*",
"guzzlehttp/guzzle": "^6.3"
"psr/http-message": "^1.0",
"php-http/client-implementation": "^1.0",
"php-http/discovery": "^1.0",
"php-http/message-factory": "^1.0"
},
"require-dev": {
"eloquent/liberator": "^2.0",
"phpunit/phpunit": "^5.7|^6.5|^7.1",
"eloquent/liberator": "^2.0"
"php-http/mock-client": "^1.1",
"php-http/message": "^1.6",
"guzzlehttp/psr7": "^1.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need guzzle for dev right, we have the mock client

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The mock client does not contain any ways to create a Request or Response object.
Thats why we need the psr7 package.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh sorry !

},
"autoload": {
"psr-4": {
Expand Down
7 changes: 3 additions & 4 deletions src/Endpoints/EndpointAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ private function buildQueryString(array $filters)
*/
protected function rest_create($body, array $filters)
{
try {
$encoded = \GuzzleHttp\json_encode($body);
} catch (\InvalidArgumentException $e) {
throw new ApiException("Error encoding parameters into JSON: '" . $e->getMessage() . "'.");
$encoded = json_encode($body);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new ApiException("Error encoding parameters into JSON: '" . json_last_error_msg() . "'.");
}

$result = $this->api->performHttpCall(
Expand Down
46 changes: 32 additions & 14 deletions src/MollieApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Mollie\Api;

use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Http\Client\Exception\RequestException;
use Http\Client\HttpClient;
use Http\Discovery\Exception;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\RequestFactory;
use Mollie\Api\Endpoints\CustomerEndpoint;
use Mollie\Api\Endpoints\CustomerPaymentsEndpoint;
use Mollie\Api\Endpoints\InvoiceEndpoint;
Expand Down Expand Up @@ -52,10 +54,15 @@ class MollieApiClient
const HTTP_NO_CONTENT = 204;

/**
* @var ClientInterface
* @var HttpClient
*/
protected $httpClient;

/**
* @var RequestFactory
*/
private $requestFactory;

/**
* @var string
*/
Expand Down Expand Up @@ -150,13 +157,25 @@ class MollieApiClient
protected $lastHttpResponseStatusCode;

/**
* @param ClientInterface $httpClient
* @param HttpClient $httpClient
* @param RequestFactory $requestFactory
*
* @throws IncompatiblePlatform
*/
public function __construct(ClientInterface $httpClient = null)
public function __construct(HttpClient $httpClient = null, RequestFactory $requestFactory = null)
{
$this->httpClient = $httpClient ? $httpClient : new Client();
try {
$this->httpClient = $httpClient ? $httpClient : HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ? $requestFactory : MessageFactoryDiscovery::find();
} catch (Exception\NotFoundException $e) {
throw new IncompatiblePlatform(
'We could not find any http-client-implementation. Install a package providing "php-http/client-implementation". Example: "composer install php-http/guzzle6-adapter".'
);
} catch (Exception\DiscoveryFailedException $e) {
throw new IncompatiblePlatform(
'We could not find any http client or message factory. Install a package providing "php-http/client-implementation" and a PSR7 message implementation. Example: "composer install php-http/guzzle6-adapter".'
);
}

$compatibilityChecker = new CompatibilityChecker();
$compatibilityChecker->checkCompatibility();
Expand All @@ -165,10 +184,9 @@ public function __construct(ClientInterface $httpClient = null)

$this->addVersionString("Mollie/" . self::CLIENT_VERSION);
$this->addVersionString("PHP/" . phpversion());
$this->addVersionString("Guzzle/" . ClientInterface::VERSION);
}

public function initializeEndpoints()
private function initializeEndpoints()
{
$this->payments = new PaymentEndpoint($this);
$this->methods = new MethodEndpoint($this);
Expand Down Expand Up @@ -308,11 +326,11 @@ public function performHttpCallToFullUrl($httpMethod, $url, $httpBody = null)
$headers['X-Mollie-Client-Info'] = php_uname();
}

$request = new Request($httpMethod, $url, $headers, $httpBody);
$request = $this->requestFactory->createRequest($httpMethod, $url, $headers, $httpBody);

try {
$response = $this->httpClient->send($request, ['http_errors' => false]);
} catch (GuzzleException $e) {
$response = $this->httpClient->sendRequest($request);
} catch (RequestException $e) {
throw new ApiException($e->getMessage(), $e->getCode(), $e);
}

Expand Down Expand Up @@ -369,7 +387,7 @@ private function parseResponseBody(ResponseInterface $response)

return $object;
}

/**
* Serialization can be used for caching. Of course doing so can be dangerous but some like to live dangerously.
*
Expand Down
55 changes: 17 additions & 38 deletions tests/Mollie/API/Endpoints/BaseEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,41 @@

namespace Tests\Mollie\Api\Endpoints;

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Http\Mock\Client;
use Mollie\Api\MollieApiClient;

abstract class BaseEndpointTest extends \PHPUnit\Framework\TestCase
{
/**
* @var Client|\PHPUnit_Framework_MockObject_MockObject
* @var Client
*/
protected $guzzleClient;
protected $httpClient;

/**
* @var
*/
protected $messageFactory;

/**
* @var MollieApiClient
*/
protected $apiClient;

protected function mockApiCall(Request $expectedRequest, Response $response)
protected function mockApiCall(Response $response)
{
$this->guzzleClient = $this->createMock(Client::class);
$this->httpClient = new Client();
$this->httpClient->addResponse($response);

$this->apiClient = new MollieApiClient($this->guzzleClient);
$this->apiClient = new MollieApiClient($this->httpClient);
$this->apiClient->setApiKey("test_dHar4XY7LxsDOtmnkVtjNVWXLSlXsM");
}

$this->guzzleClient
->expects($this->once())
->method('send')
->with($this->isInstanceOf(Request::class))
->willReturnCallback(function (Request $request) use ($expectedRequest, $response) {
$this->assertEquals($expectedRequest->getMethod(), $request->getMethod(), "HTTP method must be identical");

$this->assertEquals(
$expectedRequest->getUri()->getPath(),
$request->getUri()->getPath(),
"URI path must be identical"
);

$this->assertEquals(
$expectedRequest->getUri()->getQuery(),
$request->getUri()->getQuery(),
'Query string parameters must be identical'
);

$requestBody = $request->getBody()->getContents();
$expectedBody = $expectedRequest->getBody()->getContents();

if (strlen($expectedBody) > 0 && strlen($requestBody) > 0) {
$this->assertJsonStringEqualsJsonString(
$expectedBody,
$requestBody,
"HTTP body must be identical"
);
}

return $response;
});
protected function assertRequest(Request $expected_request)
{
$this->assertEquals($expected_request->getMethod(), $this->httpClient->getLastRequest()->getMethod(), "Expected request method should be equal to actual Request method.");
$this->assertEquals($expected_request->getUri(), $this->httpClient->getLastRequest()->getUri(), "Expected request Uri should be equal to actual Request Uri.");
}

protected function copy($array, $object)
Expand Down
13 changes: 7 additions & 6 deletions tests/Mollie/API/Endpoints/ChargebackEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ class ChargebackEndpointTest extends BaseEndpointTest
public function testGetChargebacksOnPaymentResource()
{
$this->mockApiCall(
new Request(
"GET",
"/v2/payments/tr_44aKxzEbr8/chargebacks",
[],
''
),
new Response(
201,
[],
Expand Down Expand Up @@ -92,6 +86,13 @@ public function testGetChargebacksOnPaymentResource()

$chargebacks = $this->getPayment()->chargebacks();

$this->assertRequest(new Request(
"GET",
"https://api.mollie.com/v2/payments/tr_44aKxzEbr8/chargebacks",
[],
''
));

$this->assertInstanceOf(ChargebackCollection::class, $chargebacks);
$this->assertEquals(2, $chargebacks->count);
$this->assertCount(2, $chargebacks);
Expand Down
23 changes: 18 additions & 5 deletions tests/Mollie/API/Endpoints/CustomerEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class CustomerEndpointTest extends BaseEndpointTest
public function testCreateWorks()
{
$this->mockApiCall(
new Request('POST', '/v2/customers'),
new Response(
200,
[],
Expand Down Expand Up @@ -42,6 +41,11 @@ public function testCreateWorks()
"email" => "johndoe@example.org"
]);

$this->assertRequest(new Request(
'POST',
'https://api.mollie.com/v2/customers'
));

$this->assertInstanceOf(Customer::class, $customer);
$this->assertEquals("customer", $customer->resource);
$this->assertEquals("cst_FhQJRw4s2n", $customer->id);
Expand All @@ -60,7 +64,6 @@ public function testCreateWorks()
public function testGetWorks()
{
$this->mockApiCall(
new Request('GET', '/v2/customers/cst_FhQJRw4s2n'),
new Response(
200,
[],
Expand All @@ -87,6 +90,11 @@ public function testGetWorks()
/** @var Customer $customer */
$customer = $this->apiClient->customers->get("cst_FhQJRw4s2n");

$this->assertRequest(new Request(
'GET',
'https://api.mollie.com/v2/customers/cst_FhQJRw4s2n'
));

$this->assertInstanceOf(Customer::class, $customer);
$this->assertEquals("customer", $customer->resource);
$this->assertEquals("cst_FhQJRw4s2n", $customer->id);
Expand All @@ -105,7 +113,6 @@ public function testGetWorks()
public function testListWorks()
{
$this->mockApiCall(
new Request('GET', '/v2/customers'),
new Response(
200,
[],
Expand Down Expand Up @@ -145,6 +152,11 @@ public function testListWorks()
/** @var Customer $customer */
$customers = $this->apiClient->customers->page();

$this->assertRequest(new Request(
'GET',
'https://api.mollie.com/v2/customers'
));

$this->assertInstanceOf(CustomerCollection::class, $customers);

$documentationLink = (object)["href" => "https://docs.mollie.com/reference/v2/customers-api/list-customers", "type" => "text/html"];
Expand All @@ -167,7 +179,6 @@ public function testUpdateWorks()
$expectedEmail = 'kaas.broodje@gmail.com';

$this->mockApiCall(
new Request('PATCH', '/v2/customers/cst_FhQJRw4s2n'),
new Response(
200,
[],
Expand Down Expand Up @@ -197,6 +208,8 @@ public function testUpdateWorks()

$updatedCustomer = $customer->update();

$this->assertRequest(new Request('PATCH', 'https://api.mollie.com/v2/customers/cst_FhQJRw4s2n'));

$this->assertEquals($expectedName, $updatedCustomer->name);
$this->assertEquals($expectedEmail, $updatedCustomer->email);
}
Expand All @@ -218,7 +231,7 @@ private function getCustomer()
"createdAt": "2018-04-19T08:49:01+00:00",
"_links": {
"self": {
"href": "http://api.mollie.test/v2/customers/cst_FhQJRw4s2n",
"href": "https://api.mollie.com/v2/customers/cst_FhQJRw4s2n",
"type": "application/hal+json"
},
"documentation": {
Expand Down
Loading