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
- CreatesRequestInterface
objectsResponseFactoryInterface
- CreatesResponseInterface
objectsServerRequestFactoryInterface
- CreatesServerRequestInterface
objectsStreamFactoryInterface
- CreatesStreamInterface
objectsUploadedFileFactoryInterface
- CreatesUploadedFileInterface
objectsUriFactoryInterface
- CreatesUriInterface
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();
CookieJar
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
]
]);