decodelabs/harvest

PSR-15 HTTP stack without the mess


README

PHP from Packagist Latest Version Total Downloads GitHub Workflow Status PHPStan License

PSR-15 HTTP stack without the mess

Harvest provides a unified PSR-15 HTTP stack with a simple, expressive API on top of PHP Fibers to avoid common pitfalls of other PSR-15 implementations such as call stack size, memory usage and Middleware traversal.

Installation

Install via Composer:

composer require decodelabs/harvest

Usage

Harvest provides the full PSR-15 stack, including Request, Response, Middleware and Handler interfaces. The root of the system is the Dispatcher which takes a Request and a Middleware Profile and returns a Response.

The Profile defines what Middleware is used to process the request and how it is ordered.

use DecodeLabs\Harvest;
use DecodeLabs\Harvest\Dispatcher;
use DecodeLabs\Harvest\Middleware\ContentSecurityPolicy;
use DecodeLabs\Harvest\Profile;

// Create a Middleware Profile
$profile = new Profile(
    'ErrorHandler', // Resolve by name via container / Archetype

    new ContentSecurityPolicy(), // Add middleware instance

    function($request, $handler) {
        // Add middleware callback
        // $handler is the next middleware in the stack
        // $request is the current request

        // Return a response
        return Harvest::text('Hello World!');
    }
);

// Create a Dispatcher
$dispatcher = new Dispatcher($profile);

$request = Harvest::createRequestFromEnvironment();
$response = $dispatcher->dispatch($request);

String names passed to a Profile will resolve via Slingshot, allowing for easy dependency injection and container resolution.

Ordering

Middleware is sorted by a dual level priority system, first grouped by the intent of the Middleware, in this order:

  • ErrorHandler - catches and handles errors within the stack
  • Inbound - processes the request before it is passed to the next Middleware
  • Outbound - processes the response before it is sent to the client
  • Generic - generic Middleware that does not fit into the above categories
  • Generator - generates or loads the primary response content

Then each group is sorted by the priority of the Middleware, with lower numbers being higher in the group. Harvest Middleware implement an extension to the Psr Middleware interface, defining defaults for group and priority.

These can be overridden when defining your Profile:

use DecodeLabs\Harvest\Profile;

$profile = new Profile()
    ->add('Cors', priority: 5, group: 'Generic')
    ->add(new SomeOtherVendorMiddleware(), priority: 10, group: 'Inbound');

Fibers

Harvest uses PHP Fibers to flatten the call stack within the dispatch loop - this makes for considerably less noise when debugging and understanding Exception call stacks.

Instead of a call stack that grows by at least 2 frames for every Middleware instance in the queue (which gets unwieldy very quickly), Harvest utilises the flexbility of Fibers to break out of the stack at each call to the next HTTP handler and effectively run each Middleware as if it were in a flat list, but without breaking Exception handling or any of the semantics of stacking the Middleware contexts.

Transports

Once a Response has been generated, you can then use an instance of a Harvest Transport to send it to the client.

Harvest currently provides a Generic Transport implementation that uses PHP's built in header and output stream functions.

use DecodeLabs\Harvest;

$transport = Harvest::createTransport(
    // $name - a null name will default to the Generic transport
);

$transport->sendResponse(
    $request, $response
);

exit;

Responses

Harvest provides easy shortcuts for creating Responses:

use DecodeLabs\Harvest;

$text = Harvest::text('Hello World!'); // Text

$customText = Harvest::text('Hello World!', 201, [
    'Custom-Header' => 'header-value'
]);

$html = Harvest::html('<h1>Hello World!</h1>'); // HTML

$json = Harvest::json([
    'whatever-data' => 'Hello World!'
]); // JSON

$xml = Harvest::xml($xmlString); // XML

$redirect = Harvest::redirect('/some/other/path'); // Redirect

$file = Harvest::stream('/path/to/file'); // Stream

$resource = Harvest::stream(Harvest::createStreamFromResource($resource)); // Stream

$generator = Harvest::generator(function() {
    yield 'Custom content';
    yield ' can be written';
    yield ' and streamed';
    yield ' from a generator';
}, 200, [
    'Content-Type' => 'text/plain'
]);

Cookies

Harvest provides a Cookies Middleware and a global Cookie Collection that allows you to define request-level cookies separately from the response generation process and merges them into the response. Just make sure the Cookie Middleware is added to your Profile.

use DecodeLabs\Harvest;

$profile->add('Cookies');

Harvest::$cookies->set(
    name: 'cookie-name',
    value: 'cookie-value',
    domain: 'example.com',
    path: '/',
    expires: '10 minutes',
    maxAge: 600,
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    partitioned: true
);

Harvest::$cookies->expire(
    name: 'cookie-name',
    domain: 'example.com',
    path: '/',
);

Licensing

Harvest is licensed under the MIT License. See LICENSE for the full license text.