A functional HTTP library for PHP inspired by Scala's http4s.
Http4p provides a purely functional approach to building HTTP servers and clients in PHP. Built on Phunkie Effect and Streams, it offers:
- Routes as values -
GET,POSTand friends build a list of routes, the router matches the path - Effect-based handlers - Every handler returns
IO<Response>, nothing runs until the server runs it - Bodies decoded against your classes -
decode($req, User::class)validates the JSON body against the constructor and answers a bad body with a 400 listing every problem - Streaming bodies - Responses use
Streamfor constant memory usage - Composable middleware -
Through($app, Logger(), Cors(), Recover(...)) - Errors answered where you decide -
Recover(SomeException::class, $handler)turns an exception into a response for the whole app,IO::recover()for one route
composer require phunkie/http4p- PHP 8.2 || 8.3 || 8.4 || 8.5
- phunkie/phunkie ^1.5
- phunkie/effect ^1.4
- phunkie/streams ^1.2
Http4p uses Response where the body is a Stream:
class Response {
public Status $status;
public Headers $headers;
public Stream $body; // Stream
}This design enables:
- Constant memory - Stream large responses without loading into memory
- Backpressure - Handle slow clients gracefully
- Cancellation - Stop processing when client disconnects
- Composable effects - Body production can perform IO operations
Route handlers return IO:
// Handler signature
// fn(int $id): IO<Response>
fn(int $id): IO;
// Response constructors return IO<Response>
Ok($value); // EntityEncoder turns the value into a JSON body
Ok($stream); // Uses the stream as the bodyThe constructors take the body as their first argument, so they compose as first-class callables: $io->flatMap(Ok(...)).
<?php
use Phunkie\Http4p\Request;
use Phunkie\Http4p\Router;
use Phunkie\Http4p\Server\PhpServer;
use function Phunkie\Http4p\Functions\decode;
use function Phunkie\Http4p\Functions\HttpRoutes;
use function Phunkie\Http4p\Functions\middleware\{Recover, Through};
use function Phunkie\Http4p\Functions\response\{Created, NotFound, Ok};
use function Phunkie\Http4p\Functions\routes\{GET, POST};
require_once __DIR__ . '/vendor/autoload.php';
final readonly class User
{
public function __construct(public string $name, public string $email) {}
}
$users = new Users(); // whatever holds your data; its methods return IO
$app = Through(
new Router(HttpRoutes(
GET('/users', fn() => $users->all()->flatMap(Ok(...))),
GET('/users/:id', fn(int $id) => $users->get($id)->flatMap(Ok(...))),
POST('/users', fn(Request $req) =>
decode($req, User::class)
->flatMap(fn(array $data) => $users->add($data))
->flatMap(Created(...))
),
)),
Recover(UserNotFound::class, fn(UserNotFound $e) => NotFound(['error' => $e->getMessage()])),
);
(new PhpServer($app))->run(8080)->unsafeRun();A POST /users with {"name": 7} never reaches the handler; the router answers:
{"error": "Body does not describe User.", "errors": {"name": "expected string, got int", "email": "missing"}}send is the client: a Request in, an IO<Response> out, its body a stream read from the connection as it is compiled. A newline-delimited JSON export decodes straight into entities, one line at a time:
use function Phunkie\Http4p\Functions\client\send;
use function Phunkie\Http4p\Functions\decodeLines;
send(Request(Method::GET, 'http://catalogue.internal/books/export'))
->flatMap(fn(Response $response) => decodeLines($response, Book::class)
->evalTap(fn(Book $book) => io(fn() => $index->add($book)))
->compile()
->drain())
->unsafeRun();The server writes and flushes each chunk before producing the next, and the client reads the same way, so neither side holds more than a chunk of the export. See Client and Streaming.
Full documentation is available in docs/.
- Getting Started
- Core Concepts
- Entity Decoding
- Streaming
- Client
- Middleware
- REST API example
- API Reference
MIT Licence