Skip to content

Commit

Permalink
Merge pull request #558 from mollie/http_adapters
Browse files Browse the repository at this point in the history
Extract Guzzle into Http adapters
  • Loading branch information
sandervanhooft authored Jun 3, 2021
2 parents 32543e4 + 60f9d10 commit 8bbb42e
Show file tree
Hide file tree
Showing 17 changed files with 543 additions and 239 deletions.
7 changes: 6 additions & 1 deletion .phpstan.ignoreErrors.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ parameters:

-
message: '/Access to undefined constant GuzzleHttp\\ClientInterface::/'
path: src/MollieApiClient.php
path: src/HttpAdapter/MollieHttpAdapterPicker.php
count: 1

-
message: '/Access to undefined constant GuzzleHttp\\ClientInterface::/'
path: src/HttpAdapter/Guzzle6And7MollieHttpAdapter.php
count: 1
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@
"ext-curl": "*",
"ext-json": "*",
"ext-openssl": "*",
"composer/ca-bundle": "^1.1",
"guzzlehttp/guzzle": "^6.3 || ^7.0"
"composer/ca-bundle": "^1.1"
},
"require-dev": {
"eloquent/liberator": "^2.0",
"guzzlehttp/guzzle": "^6.3 || ^7.0",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.1 || ^8.5",
"friendsofphp/php-cs-fixer": "^3.0"
},
Expand Down
2 changes: 1 addition & 1 deletion src/Endpoints/EndpointAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ protected function parseRequestBody(array $body)
}

try {
$encoded = \GuzzleHttp\json_encode($body);
$encoded = @json_encode($body);
} catch (\InvalidArgumentException $e) {
throw new ApiException("Error encoding parameters into JSON: '".$e->getMessage()."'.");
}
Expand Down
50 changes: 13 additions & 37 deletions src/Exceptions/ApiException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
namespace Mollie\Api\Exceptions;

use DateTime;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class ApiException extends \Exception
{
Expand All @@ -14,12 +12,12 @@ class ApiException extends \Exception
protected $field;

/**
* @var RequestInterface
* @var \Psr\Http\Message\RequestInterface|null
*/
protected $request;

/**
* @var ResponseInterface
* @var \Psr\Http\Message\ResponseInterface|null
*/
protected $response;

Expand All @@ -39,17 +37,17 @@ class ApiException extends \Exception
* @param string $message
* @param int $code
* @param string|null $field
* @param RequestInterface|null $request
* @param ResponseInterface|null $response
* @param \Psr\Http\Message\RequestInterface|null $request
* @param \Psr\Http\Message\ResponseInterface|null $response
* @param \Throwable|null $previous
* @throws \Mollie\Api\Exceptions\ApiException
*/
public function __construct(
$message = "",
$code = 0,
$field = null,
RequestInterface $request = null,
ResponseInterface $response = null,
$request = null,
$response = null,
$previous = null
) {
$this->raisedAt = new \DateTimeImmutable();
Expand Down Expand Up @@ -91,35 +89,13 @@ public function __construct(
}

/**
* @param \GuzzleHttp\Exception\GuzzleException $guzzleException
* @param RequestInterface|null $request
* @param \Throwable|null $previous
* @return \Mollie\Api\Exceptions\ApiException
* @throws \Mollie\Api\Exceptions\ApiException
*/
public static function createFromGuzzleException(
$guzzleException,
$request = null,
$previous = null
) {
// Not all Guzzle Exceptions implement hasResponse() / getResponse()
if (method_exists($guzzleException, 'hasResponse') && method_exists($guzzleException, 'getResponse')) {
if ($guzzleException->hasResponse()) {
return static::createFromResponse($guzzleException->getResponse(), $request, $previous);
}
}

return new self($guzzleException->getMessage(), $guzzleException->getCode(), null, $request, null, $previous);
}

/**
* @param ResponseInterface $response
* @param RequestInterface $request
* @param \Psr\Http\Message\ResponseInterface $response
* @param \Psr\Http\Message\RequestInterface $request
* @param \Throwable|null $previous
* @return \Mollie\Api\Exceptions\ApiException
* @throws \Mollie\Api\Exceptions\ApiException
*/
public static function createFromResponse(ResponseInterface $response, RequestInterface $request = null, $previous = null)
public static function createFromResponse($response, $request = null, $previous = null)
{
$object = static::parseResponseBody($response);

Expand Down Expand Up @@ -163,7 +139,7 @@ public function getDashboardUrl()
}

/**
* @return ResponseInterface|null
* @return \Psr\Http\Message\ResponseInterface|null
*/
public function getResponse()
{
Expand Down Expand Up @@ -214,7 +190,7 @@ public function getUrl($key)
}

/**
* @return RequestInterface
* @return \Psr\Http\Message\RequestInterface
*/
public function getRequest()
{
Expand All @@ -232,8 +208,8 @@ public function getRaisedAt()
}

/**
* @param ResponseInterface $response
* @return mixed
* @param \Psr\Http\Message\ResponseInterface $response
* @return \stdClass
* @throws \Mollie\Api\Exceptions\ApiException
*/
protected static function parseResponseBody($response)
Expand Down
7 changes: 7 additions & 0 deletions src/Exceptions/UnrecognizedClientException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Mollie\Api\Exceptions;

class UnrecognizedClientException extends ApiException
{
}
153 changes: 153 additions & 0 deletions src/HttpAdapter/CurlMollieHttpAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

namespace Mollie\Api\HttpAdapter;

use Composer\CaBundle\CaBundle;
use Mollie\Api\Exceptions\ApiException;
use Mollie\Api\MollieApiClient;

final class CurlMollieHttpAdapter implements MollieHttpAdapterInterface
{
/**
* Default response timeout (in seconds).
*/
const DEFAULT_TIMEOUT = 10;

/**
* Default connect timeout (in seconds).
*/
const DEFAULT_CONNECT_TIMEOUT = 2;

/**
* HTTP status code for an empty ok response.
*/
const HTTP_NO_CONTENT = 204;

/**
* @param string $httpMethod
* @param string $url
* @param array $headers
* @param $httpBody
* @return \stdClass|void|null
* @throws \Mollie\Api\Exceptions\ApiException
*/
public function send($httpMethod, $url, $headers, $httpBody)
{
$curl = curl_init($url);
$headers["Content-Type"] = "application/json";

curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $this->parseHeaders($headers));
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, self::DEFAULT_CONNECT_TIMEOUT);
curl_setopt($curl, CURLOPT_TIMEOUT, self::DEFAULT_TIMEOUT);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curl, CURLOPT_CAINFO, CaBundle::getBundledCaBundlePath());

switch ($httpMethod) {
case MollieApiClient::HTTP_POST:
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $httpBody);

break;
case MollieApiClient::HTTP_GET:
break;
case MollieApiClient::HTTP_PATCH:
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($curl, CURLOPT_POSTFIELDS, $httpBody);

break;
case MollieApiClient::HTTP_DELETE:
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($curl, CURLOPT_POSTFIELDS, $httpBody);

break;
default:
throw new \InvalidArgumentException("Invalid http method: ". $httpMethod);
}

$response = curl_exec($curl);

if ($response === false) {
throw new ApiException("Curl error: " . curl_error($curl));
}

$statusCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);

curl_close($curl);

return $this->parseResponseBody($response, $statusCode, $httpBody);
}

/**
* The version number for the underlying http client, if available.
* @example Guzzle/6.3
*
* @return string|null
*/
public function versionString()
{
return 'Curl/*';
}

/**
* @param string $response
* @param int $statusCode
* @param string $httpBody
* @return \stdClass|null
* @throws \Mollie\Api\Exceptions\ApiException
*/
protected function parseResponseBody($response, $statusCode, $httpBody)
{
if (empty($response)) {
if ($statusCode === self::HTTP_NO_CONTENT) {
return null;
}

throw new ApiException("No response body found.");
}

$body = @json_decode($response);

// GUARDS
if (json_last_error() !== JSON_ERROR_NONE) {
throw new ApiException("Unable to decode Mollie response: '{$response}'.");
}

if (isset($body->error)) {
throw new ApiException($body->error->message);
}

if ($statusCode >= 400) {
$message = "Error executing API call ({$body->status}: {$body->title}): {$body->detail}";

$field = null;

if (! empty($body->field)) {
$field = $body->field;
}

if (isset($body->_links, $body->_links->documentation)) {
$message .= ". Documentation: {$body->_links->documentation->href}";
}

if ($httpBody) {
$message .= ". Request body: {$httpBody}";
}

throw new ApiException($message, $statusCode, $field);
}

return $body;
}

protected function parseHeaders($headers)
{
$result = [];

foreach ($headers as $key => $value) {
$result[] = $key .': ' . $value;
}

return $result;
}
}
Loading

0 comments on commit 8bbb42e

Please sign in to comment.