Overview

Description

Caldera HTTP is a PSR-7, PSR-15 and PSR-17 implementation.

As with the other Caldera components it has been built to be swappable and modular, which means you can change it for any other PSR-7, PSR-15 or PSR-17 implementation should you like to.

Installation

The easisest way to install it is to use Composer:

composer require vecode/caldera-http

Requires

  • php: >=8.1
  • ext-mbstring: *
  • psr/http-message: ^1.0,
  • psr/http-factory: ^1.0,
  • psr/http-server-handler: ^1.0,
  • psr/http-server-middleware: ^1.0
  • psr/psr/http-client: ^1.0

Basic usage

Getting started

The base components are straightforward, just create a new instance of them with its corresponding constructors:

use Caldera\Http\Request;
use Caldera\Http\Response;
use Caldera\Http\Stream;
use Caldera\Http\UploadedFile;
use Caldera\Http\Uri;

$uri = new Uri('https://192.168.1.128:8080/api/echo?mode=default');
$body = new Stream('{}');
$request = new Request('POST', 'localhost');
$response = new Response(200);
$file = new UploadedFile($path, $size, UPLOAD_ERR_OK);

These are the building blocks for the PSR-15 and PSR-17 specifications.

Factories

For an easier approach, the PSR-17 specification defines the concept of HTTP factories whose objective is to provide a standard way of constructing PSR-7 objects, it defines the folllowing interfaces:

  • RequestFactoryInterface - Creates RequestInterface objects
  • ResponseFactoryInterface - Creates ResponseInterface objects
  • ServerRequestFactoryInterface - Creates ServerRequestInterface objects
  • StreamFactoryInterface - Creates StreamInterface objects
  • UploadedFileFactoryInterface - Creates UploadedFileInterface objects
  • UriFactoryInterface - Creates UriInterface objects

These interfaces are implemented in a single Factory class, so that you may use it to create any of these objects:

use Caldera\Http\Factory;

$factory = new Factory();
$request = $factory->createServerRequest('GET', 'https://192.168.1.128:8080/api/echo?mode=default');

Middleware

Middleware is fully supported, just implement MiddlewareInterface to create your own middlewares:

use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class MyMiddleware implements MiddlewareInterface {

    /**
     * Process request
     * @param  ServerRequestInterface  $request Request implementation
     * @param  RequestHandlerInterface $handler RequestHandler implementation
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
        $response = new Response(201);
        return $response;
    }
}
Processing middleware

To process middleware we've included the HasMiddleware trait, just add it to your classes that handle server requests:

use Psr\Container\ContainerInterface;
use Caldera\Http\HasMiddleware

class MyHttpHandler {

    use HasMiddleware;

    /**
     * ContainerInterface implementation
     * @var ContainerInterface
     */
    protected $container;

    /**
     * Constructor
     * @param ContainerInterface $container ContainerInterface implementation
     */
    public function __construct(ContainerInterface $container) {
        $this->container = $container;
    }
}

That way you just need to call the dispatch method:

$handler = new MyHttpHandler();
$handler->with(MyMiddleware::class);
$response = $handler->dispatch($request, $this->container, function() {
    return new Response(201);
});

The dispatch method receives three parameters: $request a ServerRequestInterface implementation, $container a ContainerInterface implementation and a Closure that will be called at the end of the middleware chain; usually this callback generates the response and is commonly known as the default callback.

Also it must return a ResponseInterface implementation.

You may easily alter the middleware chain by adding to it with the with method and removing from it with the without method. Just pass the FQN of the middleware each time.

In the case of with you can also pass a Closure:

$handler = new MyHttpHandler();
$handler->with(function(ServerRequestInterface $request, RequestHandlerInterface $handler) {
    return new Response(418);
});
Before and after middleware

The implementation of dispatch uses a double-pass method, so that you can intercept the request before the default callback or after it has been called, for example:

use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class CheckAuthMiddleware implements MiddlewareInterface {

    /**
     * Process request
     * @param  ServerRequestInterface  $request Request implementation
     * @param  RequestHandlerInterface $handler RequestHandler implementation
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
        if (! $this->isAuthenticated() ) {
            $response = new Response(403);
            return $response;
        } else {
            return $handler->handle($request);
        }
    }

    /**
     * Check if the request is authenticated
     * @return bool
     */
    protected function isAuthenticated(): bool {
        // TBD: Check authentication
        return true;
    }
}

This will be executed before the default callback and if the isAuthenticated method returns false, the response will contain an HTTP/403 status code, with no middleware called after that. Otherwise, it will pass control to the next middleware in the chain.

Also you can have middleware that modify the response rather than generate it, for example to add extra headers:

use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Caldera\Settings\Settings;

class AddCspMiddleware implements MiddlewareInterface {

    /**
     * Settings instance
     * @var Settings
     */
    protected $settings;

    /**
     * Constructor
     * @param Settings $settings Settings instance
     */
    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    /**
     * Process request
     * @param  ServerRequestInterface  $request Request implementation
     * @param  RequestHandlerInterface $handler RequestHandler implementation
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
        $response = $handler->handle($request);
        $response = $response->withHeader('Content-Security-Policy', $this->settings->get('security.csp'));
        return $response;
    }
}

In this case, the middleware does only add a header to an already generated response, note the placement of the $handler->handle($request) call: in the previous example it is called if the middleware was not affecting it, but in the later, it is called to retrieve the $response object, meaning that it gets executed after the default callback.

Cookies

You can easily generate cookies using the Cookie class:

use Caldera\Http\Cookie;

$cookie = new Cookie('Test');
$cookie = $cookie->withValue('4d79b35927147f5ee175b436db8c8a5b');
$cookie = $cookie->withExpiration(1924444800); // Dec, 25 2030, 16:00:00 GMT
$cookie = $cookie->withPath('/');
$cookie = $cookie->withHttpOnly(true);
$cookie = $cookie->withSecureOnly(true);
$cookie = $cookie->withDomain('localhost');

Once created you can easily send it to the browser with the add method:

$cookie->add();

Conversely, you can use the remove method to send it empty to the browser so that it gets deleted from the client machine:

$cookie->remove();

The CookieJar is a helper class to make it easy to check for cookies, get, set and delete them.

To check for a cookie use the has method:

use Caldera\Http\CookieJar;

$cookie_jar = new CookieJar();
$cookie_jar->has('Test');

You can also retrieve the value of a cookie with the get method:

$cookie_jar->get('Test');

With the set method you can set its value either by passing a cookie string:

$cookie_jar->set('Test=4d79b35927147f5ee175b436db8c8a5b; Expires=Wed, 25 Dec 2030...');

Or a Cookie instance:

$cookie_jar->set($cookie);

Finally, you can delete a cookie by its name using the delete method:

$cookie_jar->delete('Test');

It's important to note that the current implementation uses the header function directly.

HTTP Client

A PSR-18 HTTP Client implementation is also included.

To use it create a Client instance:

use Caldera\Http\Client;

$client = new Client();

You may now execute a GET request:

$response = $client->get('https://example.com/auth');

Also, you can set custom headers easily:

$response = $client->get('https://example.com/auth', [
    'headers' => [
        'Authorization' => 'Bearer MTk5ZmY4NTNmNjMxOTQwMzE2MzI0ZGFiZWI5YjcyNDI=',
    ],
]);

The result of the request is returned as $response, and is a ResponseInterface implementation instance.

For POST request you can either include form fields:

$response = $client->post('https://example.com/form', [
    'fields' => [
        'foo' => 'bar',
        'bar' => [
            'baz',
            'qux',
        ],
    ],
]);

Or JSON payloads:

$response = $client->post('https://example.com/json', [
    'json' => [
        'id' => 42,
        'foo' => 'bar',
    ]
]);

There are methods also for PUT, PATCH, DELETE, OPTIONS and HEAD requests.

HTTPS requests

For HTTPS requests you often require a properly configured server so that the certificate bundle is correctly set. If that is not the case, but you still need to connect through HTTPS (and why won't you want to), you can specify the location of the certificate bundle, the cacert.pem file:

$client->setCertificateBundle( dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cacert.pem' );
Advanced options

You can customize the user agent:

$client->setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0')

The referer:

$client->setReferer('https://google.com')

Specify whether or not redirection headers should be followed:

$client->setRedirect(true)

Set a custom timeout (in seconds) for requests:

$client->setTimeout(5)

Or specify a CookieJar instance to handle the cookies:

$client->setCookieJar($cookie_jar);
Downloads & uploads

Finally, you can download and upload files easily, just specify the destination file:

$download = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'response.json';
$response = $client->get('https://example.com/download', [
    'download' => $download
]);

Or in the case of uploads, the source file:

$upload = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'upload.md';
$response = $client->post('https://example.com/upload', [
    'files' => [
        'upload' => $upload
    ]
]);