How to use PSR HTTP standards to upgrade your code
Tags: programming, php, webdev
Published: 2023-02-13 21:37:04

Almost every developer has made one or more classes that make HTTP requests to external APIs and services or have used composer packages to do this. You probably used cURL or Guzzle to do this, but probably without any abstraction.

Normally this isn't an issue, but if nobody does this you will end up with a lot of extra HTTP clients in your source code.

Luckily a group of smart people got together and created a couple of standards that can be used to base your code on a collection of interfaces. With this you can create packages without hard dependencies on specific HTTP clients and it will allow you to insert your own HTTP clients into the library of choice. 

The strength of allowing developers to insert their own libraries also gives them the ability to configure it as needed. They might need middleware to log failed network attempts or they want to retry every call at least 3 times before giving up! Another benefit is that it allows you to use mocked or fake clients for testing, which makes it easier to write tests for your code.

This article will display the use of 
- [PSR-7](https://www.php-fig.org/psr/psr-7/), the standard for requests and responses
- [PSR-17](https://www.php-fig.org/psr/psr-17/), the standard for request factories
- [PSR-18](https://www.php-fig.org/psr/psr-18/), the standard for HTTP Clients with which to send and receive the PSR-7 requests and responses.

I will demonstrate how these standards can help you by writing a small package which can retrieve cat facts from an external API with your HTTP Client of choice!

The end result will look like this if you want to use the default client
```php
$consumer = new Consumer();
$facts = $consumer->facts()->all();
```

Or like this when you want to configure and use a different client!
```php
$curlClient = new \Http\Client\Curl\Client();
$httpClient = new HttpClient($curlClient);
$catFacts = new CatFacts($httpClient);
$facts = $catFacts->facts()->all();
```

## Let's get started
In the example above you may have noticed that I made my own HttpClient class which acts as a wrapper for the actual client. This will allow us to have an entry point for all requests and responses. Most of the PSR related stuff will be in here. Here is the entire file, but I will try to explain the constructor and the sendRequest below it.

### HttpClient
```php
<?php

declare(strict_types=1);

namespace Rocksheep\CatFacts;

use Exception;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use JsonException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;

class HttpClient
{
    private ClientInterface $client;
    private RequestFactoryInterface $requestFactory;

    protected string $baseUrl = 'https://cat-fact.herokuapp.com';

    public function __construct(
        ?ClientInterface $client = null,
        ?RequestFactoryInterface $requestFactory = null
    )
    {
        $this->client = $client ?: Psr18ClientDiscovery::find();
        $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
    }

    /**
     * @throws JsonException
     * @throws Exception
     */
    public function sendRequest(string $method, string $uri): array
    {
        $request = $this->requestFactory->createRequest($method, sprintf('%s/%s', $this->baseUrl, ltrim($uri, '/')));

        try {
            $response = $this->client->sendRequest($request);
        } catch (ClientExceptionInterface $e) {
            throw new Exception('Oh well');
        }

        if ($response->getStatusCode() >= 400) {
            throw new Exception('Too bad');
        }

        $responseBody = (string) $response->getBody();

        return json_decode($responseBody, false, 512, JSON_THROW_ON_ERROR);
    }
}
```
#### Constructor
In the constructor you will see that we accept a nullable ClientInterface and a nullable RequestFactoryInterface. This is what allows developers to use their own implementation. If a developer does not supply a Client or an Factory we will use the Auto Discover classes to find usable classes like Guzzle, HTTPPlug or cURL.

#### sendRequest
This is where the interfaces are used. Nothing really special happens here. Requests are created from the RequestFactory and are sent via the Client supplied in the constructor. We do some very simple error handling and then return the contents as needed.

### CatFacts, the entry point of our package
This class is the entrance point of our package and is how consumers will use our classes. This will receive the HttpClient and just pass it along to our more useful classes, which takes to the Facts class!
```php
<?php

declare(strict_types=1);

namespace Rocksheep\CatFacts;

use Rocksheep\CatFacts\Api\Facts;

class CatFacts
{
    private HttpClient $httpClient;

    public function __construct(?HttpClient $httpClient = null)
    {
        $this->httpClient = $httpClient ?: new HttpClient();
    }

    public function getHttpClient(): HttpClient
    {
        return $this->httpClient;
    }

    public function facts(): Facts
    {
        return new Facts($this);
    }
}
```

### Facts
This class is one of the endpoints of the API we are consuming and thus also the place where the PSR-18 client gets used! It is given an instance of our entry point, so that it can use the HttpClient from within. 

For this example I only added the all call which retrieves a list of facts and transforms them into something we can work with. In this case a simple Fact data object, which just holds on to the ID and text for us.
```php
<?php

declare(strict_types=1);

namespace Rocksheep\CatFacts\Api;

use Rocksheep\CatFacts\CatFacts;
use Rocksheep\CatFacts\Models\Fact;

class Facts
{
    private CatFacts $catFacts;

    public function __construct(CatFacts $catFacts)
    {
        $this->catFacts = $catFacts;
    }

    /**
     * @return array<Fact>
     * @throws \JsonException
     */
    public function all(): array
    {
        $response = $this->catFacts->getHttpClient()->sendRequest('get', 'facts');

        return array_map(
            fn (\stdClass $fact) => new Fact($fact->_id, $fact->text),
            $response
        );
    }
}

```

## Conclusion
The example I gave is a very simple one that hopefully gets the point across. When you use the PSR standards in this way you will automatically adhere to the Liskov Substitution principle, which states that object of a super class should be replaceable with objects of its subclasses and not break the application.

It will allow consumers of the package to use their own http clients which they may have configured with specific middleware or error handling.

If you want to check out this code in its entirely you can head to [this](https://github.com/Rocksheep/catfacts-example) repo. It's in a useable state, but the API does not contain a lot of facts.