Back in May last year, one of my colleagues blogged about the introduction of our Python binding for the Cloudflare API and drew reference to our other bindings in Go and Node. Today we are complimenting this range by introducing a new official binding, this time in PHP.
This binding is available via Packagist as cloudflare/sdk, you can install it using Composer simply by running composer require cloudflare/sdk
. We have documented various use-cases in our "Cloudflare PHP API Binding" KB article to help you get started.
Alternatively should you wish to help contribute, or just give us a star on GitHub, feel free to browse to the cloudflare-php source code.
PHP is a controversial language, and there is no doubt there are elements of bad design within the language (as is the case with many other languages). However, love it or hate it, PHP is a language of high adoption; as of September 2017 W3Techs report that PHP is used by 82.8% of all the websites whose server-side programming language is known. In creating this binding the question clearly wasn't on the merits of PHP, but whether we wanted to help drive improvements to the developer experience for the sizeable number of developers integrating with us whilst using PHP.
In order to help those looking to contribute or build upon this library, I write this blog post to explain some of the design decisions made in putting this together.
Exclusively for PHP 7
PHP 5 initially introduced the ability for type hinting on the basis of classes and interfaces, this opened up (albeit seldom used) parametric polymorphic behaviour in PHP. Type hinting on the basis of interfaces made it easier for those developing in PHP to follow the Gang of Four's famous guidance: "Program to an 'interface', not an 'implementation'."
Type hinting has slowly developed in PHP, in PHP 7.0 the ability for Scalar Type Hinting was released after a few rounds of RFCs. Additionally PHP 7.0 introduced Return Type Declarations, allowing return values to be type hinted in a similar way to argument type hinting. In this library we extensively use Scalar Type Hinting and Return Type Declarations thereby restricting the backward compatibility that's available with PHP 5.
In order for backward compatibility to be available, these improvements to type hinting simply would not be implementable and the associated benefits would be lost. With Active Support no longer being offered to PHP 5.6 and Security Support little over a year away from disappearing for the entirety of PHP 5.x, we decided the additional coverage wasn't worth the cost.
Object Composition
What do we mean by a software architecture? To me the term architecture conveys a notion of the core elements of the system, the pieces that are difficult to change. A foundation on which the rest must be built. Martin Fowler
When getting started with this package, you'll notice there are 3 classes you'll need to instantiate:
$key = new \Cloudflare\API\Auth\APIKey('user@example.com', 'apiKey');
$adapter = new Cloudflare\API\Adapter\Guzzle($key);
$user = new \Cloudflare\API\Endpoints\User($adapter);
echo $user->getUserID();
The first class being instantiated is called APIKey
(a few other classes for authentication are available). We then proceed to instantiate the Guzzle
class and the APIKey
object is then injected into the constructor of the Guzzle
class. The Auth
interface that the APIKey
class implements is fairly simple:
namespace Cloudflare\API\Auth;
interface Auth
{
public function getHeaders(): array;
}
The Adapter
interface (which the Guzzle
class implements) makes explicit that an object built on the Auth
interface is expected to be injected into the constructor:
namespace Cloudflare\API\Adapter;
use Cloudflare\API\Auth\Auth;
use Psr\Http\Message\ResponseInterface;
interface Adapter
{
...
public function __construct(Auth $auth, string $baseURI);
...
}
In doing so; we define that classes which implement the Adapter
interface are to be composed using objects made from classes which implement the Auth
interface.
So why am I explaining basic Dependency Injection here? It is critical to understand as the design of our API changes, the mechanisms for Authentication may vary independently of the HTTP Client or indeed API Endpoints themselves. Similarly the HTTP Client or the API Endpoints may vary independently of the other elements involved. Indeed, this package already contains three classes for the purpose of authentication (APIKey
, UserServiceKey
and None
) which need to be interchangeably used. This package therefore considers the possibility for changes to different components in the API and seeks to allow these components to vary independently.
Dependency Injection is also used where the parameters for an API Endpoint become more complicated then what is permitted by simpler variables types; for example, this is done for defining the Target or Configuration when configuring a Page Rule:
require_once('vendor/autoload.php');
$key = new \Cloudflare\API\Auth\APIKey('mjsa@junade.com', 'apiKey');
$adapter = new Cloudflare\API\Adapter\Guzzle($key);
$zones = new \Cloudflare\API\Endpoints\Zones($adapter);
$zoneID = $zones->getZoneID("junade.com");
$pageRulesTarget = new \Cloudflare\API\Configurations\PageRulesTargets('https://junade.com/noCache/*');
$pageRulesConfig = new \Cloudflare\API\Configurations\PageRulesActions();
$pageRulesConfig->setCacheLevel('bypass');
$pageRules = new \Cloudflare\API\Endpoints\PageRules($adapter);
$pageRules->createPageRule($zoneID, $pageRulesTarget, $pageRulesConfig, true, 6);
The structure of this project is overall based on simple object composition; this provides a far more simple object model for the long-term and a design that provides higher flexibility. For example; should we later want to create an Endpoint class which is a composite of other Endpoints, it becomes fairly trivial for us to build this by implementing the same interface as the other Endpoint classes. As more code is added, we are able to keep the design of the software relatively thinly layered.
Testing/Mocking HTTP Requests
If you're interesting in helping contribute to this repository; there are two key ways you can help:
Building out coverage of endpoints on our API
Building out test coverage of those endpoint classes
The PHP-FIG (PHP Framework Interop Group) put together a standard on how HTTP responses can be represented in an interface, this is described in the PSR-7 standard. This response interface is utilised by our HTTP Adapter
interface in which responses to API requests are type hinted to this interface (Psr\Http\Message\ResponseInterface
).
By using this standard, it's easier to add further abstractions for additional HTTP clients and mock HTTP responses for unit testing. Let's assume the JSON response is stored in the $response
variable and we want to test the listIPs
method in the IPs
Endpoint class:
public function testListIPs() {
$stream = GuzzleHttp\Psr7\stream_for($response);
$response = new GuzzleHttp\Psr7\Response(200, ['Content-Type' => 'application/json'], $stream);
$mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock();
$mock->method('get')->willReturn($response);
$mock->expects($this->once())
->method('get')
->with($this->equalTo('ips'), $this->equalTo([])
);
$ips = new \Cloudflare\API\Endpoints\IPs($mock);
$ips = $ips->listIPs();
$this->assertObjectHasAttribute("ipv4_cidrs", $ips);
$this->assertObjectHasAttribute("ipv6_cidrs", $ips);
}
We are able to build a simple mock of our Adapter
interface by using the standardised PSR-7 response format, when we do so we are able to define what parameters PHPUnit expects to be passed to this mock. With a mock Adapter
class in place we are able to test the IPs
Endpoint class as any if it was using a real HTTP client.
Conclusions
Through building on modern versions of PHP, using good Object-Oriented Programming theory and allowing for effective testing we hope our PHP API binding provides a developer experience that is pleasant to build upon.
If you're interesting in helping improve the design of this codebase, I'd encourage you to take a look at the PHP API binding source code on GitHub (and optionally give us a star).
If you work with Go or PHP and you're interested in helping Cloudflare turn our high-traffic customer-facing API into an ever more modern service-oriented environment; we're hiring for Web Engineers in San Francisco, Austin and London.