Skip to content

Commit

Permalink
Refactoring: testable, PSR-18 http client (#53)
Browse files Browse the repository at this point in the history
* chore: add phpunit library

* refactor: "inject" client through Environment

* tests: add basic tests for HttpClient

uses Guzzle with MockHandler to simulate http client responses

* refactor: extract GuzzleFactory. Extract Response class

* refactor: move logic from Guzzle to Request

* refactor: use psr7 request object

* refactor: create request in GuzzleFactory

* refactor: remove Guzzle class

* refactor: Request does not depend on concrete client.

use GuzzleFactory for default http client / request

* chore: ignore .idea dir

* fix: uri building. Add test cases for query params / body

* refactor: extract "verify" setting outside the factory

* tests: cover Request error handling

* chore!: release 4.0.0 (please see changelog)

---------

Co-authored-by: cb-karthikp <[email protected]>
  • Loading branch information
Wojciechem and cb-karthikp authored May 28, 2024
1 parent 15cd331 commit f7f8d9e
Show file tree
Hide file tree
Showing 13 changed files with 669 additions and 159 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
.idea

#Ignore the loca test file
/lib/test.php
Expand All @@ -9,3 +10,5 @@
# Ignore the vendor directory for people using composer
/vendor/
composer.lock

*.cache
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
### v3.27.2(2024-05-28)
***

#### Refactoring
- PSR-18 compatible http client (decouple from Guzzle)
- introduce HttpClientFactory in Environment class (also covers GH-19, GH-43 use cases)
- make request logic unit-testable by using custom HttpClientFactory (GH-50)
- cover Request class with unit tests
### v3.27.1 (2024-05-02)
* * *

Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"require" : {
"php": ">=5.6.0",
"ext-curl": "*",
"guzzlehttp/guzzle": ">=6.5"
"guzzlehttp/guzzle": ">=6.5",
"psr/http-client": "^1.0"
},
"scripts": {
"test": "php -S localhost:8080 -t test/"
Expand All @@ -22,6 +23,7 @@
}
},
"require-dev": {
"simpletest/simpletest": "^1.1"
"simpletest/simpletest": "^1.1",
"phpunit/phpunit": "^7.0.0|^8.0.0|^9.0.0|^10.3"
}
}
46 changes: 43 additions & 3 deletions lib/ChargeBee/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

namespace ChargeBee\ChargeBee;

use ChargeBee\ChargeBee;
use ChargeBee\ChargeBee\HttpClient\GuzzleFactory;
use ChargeBee\ChargeBee\HttpClient\HttpClientFactory;

class Environment
{
private $apiKey;
private $site;
private $apiEndPoint;

private static $default_env;
private static $httpClientFactory;

public static $scheme = "https";
public static $chargebeeDomain;

Expand All @@ -20,7 +26,7 @@ class Environment

const API_VERSION = "v2";

public function __construct($site, $apiKey)
public function __construct($site, $apiKey, $httpClientFactory = null)
{
$this->site = $site;
$this->apiKey = $apiKey;
Expand All @@ -30,11 +36,26 @@ public function __construct($site, $apiKey)
} else {
$this->apiEndPoint = self::$scheme . "://$site." . self::$chargebeeDomain . "/api/" . self::API_VERSION;
}

if (null === $httpClientFactory) {
self::$httpClientFactory = new GuzzleFactory(
self::$connectTimeoutInSecs,
self::$requestTimeoutInSecs,
// Specifying a CA bundle results in the following error when running in Google App Engine:
// "Unsupported SSL context options are set. The following options are present, but have been ignored: allow_self_signed, cafile"
// https://cloud.google.com/appengine/docs/php/outbound-requests#secure_connections_and_https
['verify' => ChargeBee::getVerifyCaCerts() && !self::isAppEngine() ? ChargeBee::getCaCertPath() : false]
);

return;
}

self::$httpClientFactory = $httpClientFactory;
}

public static function configure($site, $apiKey)
public static function configure($site, $apiKey, $httpClientFactory = null)
{
self::$default_env = new self($site, $apiKey);
self::$default_env = new self($site, $apiKey, $httpClientFactory);
}

public function getApiKey()
Expand Down Expand Up @@ -72,4 +93,23 @@ public static function updateRequestTimeoutInSecs($requestTimeout)
self::$requestTimeoutInSecs = $requestTimeout;

}

/**
* @return HttpClientFactory
*/
public static function getHttpClientFactory()
{
return self::$httpClientFactory;
}

/**
* Recommended way to check if script is running in Google App Engine:
* https://github.com/google/google-api-php-client/blob/master/src/Google/Client.php#L799
*
* @return bool Returns true if running in Google App Engine
*/
public static function isAppEngine()
{
return (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== false);
}
}
152 changes: 0 additions & 152 deletions lib/ChargeBee/Guzzle.php

This file was deleted.

89 changes: 89 additions & 0 deletions lib/ChargeBee/HttpClient/GuzzleFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace ChargeBee\ChargeBee\HttpClient;

use ChargeBee\ChargeBee;
use ChargeBee\ChargeBee\Request;
use ChargeBee\ChargeBee\Version;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

final class GuzzleFactory implements HttpClientFactory
{
private $options;
private $connectTimeoutInSecs;
private $requestTimeoutInSecs;

/**
* @param array $options
* @param float $connectTimeoutInSecs
* @param float $requestTimeoutInSecs
*/
public function __construct($connectTimeoutInSecs, $requestTimeoutInSecs, $options = [])
{
$this->connectTimeoutInSecs = $connectTimeoutInSecs;
$this->requestTimeoutInSecs = $requestTimeoutInSecs;
$this->options = $options;
}

/**
* @return ClientInterface|Client
*/
public function createClient()
{
return new Client(
array_merge(
[
'allow_redirects' => true,
'http_errors' => false,
'connect_timeout' => $this->connectTimeoutInSecs,
'timeout' => $this->requestTimeoutInSecs
],
$this->options
)
);
}

/**
* @throws Exception
* @return RequestInterface
*/
public function createRequest($meth, $headers, $env, $url, $params)
{
if (!in_array($meth, [Request::GET, Request::POST])) {
throw new Exception("Invalid http method $meth");
}

$userAgent = "Chargebee-PHP-Client" . " v" . Version::VERSION;
$httpHeaders = array_merge(
$headers,
[
'Accept' => 'application/json',
'User-Agent' => $userAgent,
'Lang-Version' => phpversion(),
'OS-Version' => PHP_OS,
'Authorization' => 'Basic ' . \base64_encode($env->getApiKey() . ':')
]
);
$body = null;

$uri = new Uri($url);

if ($meth == Request::GET) {
if (count($params) > 0) {
$query = \http_build_query($params, '', '&', \PHP_QUERY_RFC3986);
$uri = $uri->withQuery($query);
}
}

if ($meth == Request::POST) {
$body = \http_build_query($params, '', '&');
$httpHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
}

return new \GuzzleHttp\Psr7\Request($meth, $uri, $httpHeaders, $body);
}
}
20 changes: 20 additions & 0 deletions lib/ChargeBee/HttpClient/HttpClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace ChargeBee\ChargeBee\HttpClient;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

interface HttpClientFactory
{
/**
* @return ClientInterface
*/
public function createClient();

/**
* @throws \Exception
* @return RequestInterface
*/
public function createRequest($meth, $headers, $env, $url, $params);
}
Loading

0 comments on commit f7f8d9e

Please sign in to comment.