Curlpit is a PSR-15 middleware orchestrator micro framework for PHP – with branching, looping, declarative flow control, and multi-API aggregation built in.
Most middleware stacks are pipelines: request goes in, response comes out, linearly. Curlpit treats the middleware stack as an instruction sequence – with a program counter, named labels, conditional jumps, loops, and collection mapping. Business logic that would otherwise be hardcoded in handlers can be expressed as configuration.
cURLpit isn’t meant to replace your application logic entirely — only the parts that refuse to stay static.
composer require curlpit/curlpit
Then install a PSR-7/17 implementation of your choice:
# nyholm/psr7 (recommended – lightweight, zero dependencies)
composer require nyholm/psr7 nyholm/psr7-server
# or guzzlehttp/psr7
composer require guzzlehttp/psr7
| Standard PSR-15 | Curlpit | |
|---|---|---|
| Execution | Linear chain | Instruction sequence (program counter) |
| Branching | None | JumpMiddleware, IfMiddleware |
| Looping | None | LoopMiddleware + LoopContext |
| Collection mapping | None | MapMiddleware + CollectMiddleware |
| Flow control | None | BreakMiddleware, ContinueMiddleware, HaltMiddleware |
| HTTP client | None | HttpMiddleware (Guzzle → cURL → file_get_contents) |
| State | Informal | Explicit Set/Get/Inc/Dec middleware |
| Error handling | Manual | Built-in ErrorHandlerMiddleware |
| Config | Code only | JSON-declarable with use aliases |
| Environment | Hardcoded | {env:VAR} token interpolation |
Extend Application, override instantiate() to wire up your dependencies, point it at a middleware.json:
use Curlpit\App\Application;
use Curlpit\Core\Emitter;
class MyApp extends Application
{
protected function instantiate(string $class, array $options): MiddlewareInterface
{
return match ($class) {
MyMiddleware::class => new MyMiddleware($this->responseFactory),
default => parent::instantiate($class, $options),
};
}
}
$app = (new MyApp($responseFactory, $streamFactory))
->withConfig(ConfigLoader::fromFile(__DIR__ . '/middleware.json'));
$response = $app->handle($serverRequest);
(new Emitter())->emit($response);
{
"middleware": [
{ "Curlpit\\Core\\Middleware\\ErrorHandlerMiddleware": { "debug": false } },
{ "My\\AuthMiddleware": {} },
{
"Curlpit\\Core\\Middleware\\JumpMiddleware": {
"condition": { "type": "attr", "name": "user_role", "eq": "admin" },
"jump_to_label": "admin"
}
},
{ "My\\PublicDispatch": {} },
{ "My\\AdminDispatch": { "label": "admin" } }
]
}
use aliasesAvoid repeating full class names throughout middleware.json. Aliases are resolved
at config load time by ConfigLoader, including inside nested middleware blocks:
{
"use": {
"Http": "Curlpit\\Core\\Middleware\\HttpMiddleware",
"Map": "Curlpit\\Core\\Middleware\\MapMiddleware",
"Collect": "Curlpit\\Core\\Middleware\\CollectMiddleware",
"If": "Curlpit\\Core\\Middleware\\IfMiddleware",
"Break": "Curlpit\\Core\\Middleware\\BreakMiddleware",
"Continue": "Curlpit\\Core\\Middleware\\ContinueMiddleware",
"Noop": "Curlpit\\Core\\Middleware\\NoopMiddleware",
"Halt": "Curlpit\\Core\\Middleware\\HaltMiddleware"
},
"middleware": [
{ "Http": { "url": "https://api.example.com/posts" } }
]
}
All middleware that accepts string values supports token interpolation via Interpolator:
| Token | Resolves to |
|---|---|
{attr:key} |
Request attribute |
{attr:key.nested.path} |
Dot-notation into array attribute |
{context:key} |
LoopContext value |
{env:VAR_NAME} |
Environment variable (.env via phpdotenv) |
"url": "https://api.example.com/users/{attr:current_post.userId}",
"headers": { "x-api-key": "{env:MY_API_KEY}" }
Custom token types can be added by extending Curlpit\Core\Interpolator::resolveToken().
The {env:...} token makes the same middleware.json work across environments
without any PHP code changes – only .env differs:
# .env.dev
PAYMENTS_API_URL=https://sandbox.stripe.com
# .env.prod
PAYMENTS_API_URL=https://api.stripe.com
"url": "{env:PAYMENTS_API_URL}/transactions/{attr:order.id}"
{ "type": "always" }
{ "type": "never" }
{ "type": "attr", "name": "status", "eq": "active" }
{ "type": "attr", "name": "retries", "lte": 3 }
{ "type": "context", "name": "has_more" }
{ "type": "context", "name": "count", "gt": 0 }
Operators: eq, neq, gt, gte, lt, lte. Without an operator, truthy check.
RequestHandler – PSR-15 handler with program counter and label-based jumpsJumpMiddleware – conditional branch to a named label (else_label for two-way branch)IfMiddleware – compile-time construct translating to Jump + label combinations. Both then and else support full nested pipelines. else is optional.LoopMiddleware – repeat a sub-handler while a condition holdsBreakMiddleware – breaks out of the enclosing LoopMiddleware or MapMiddleware. Analogous to C’s break.ContinueMiddleware – skips the remainder of the current iteration. Analogous to C’s continue.TryMiddleware – execute a sub-handler, jump to catch label on exceptionMapMiddleware – iterates a collection attribute, runs inner pipeline per item. Supports source_path dot-notation for envelope unwrapping. Multiple nested instances must use distinct accumulator_attr values.CollectMiddleware – gathers key-value pairs from request attributes, LoopContext, and env vars into MapAccumulator. Must be used inside a MapMiddleware pipeline.HttpMiddleware – outgoing HTTP request with transport fallback: Guzzle → cURL → file_get_contents. Supports full token interpolation in URL, headers, and body.RoutingMiddleware – path pattern matching with {param} placeholders, 404/405 awareDispatchMiddleware – resolves and calls the matched handler via an injected resolver callableErrorHandlerMiddleware – catches all exceptions, returns JSON or plaintext based on Accept headerSetVariableMiddleware / GetVariableMiddleware – read/write request attributesIncrementMiddleware / DecrementMiddleware – numeric counters in request attributesNoopMiddleware – passes the request unchanged. Used as a label anchor (e.g. in IfMiddleware else branches) or as a placeholder.HaltMiddleware – terminates the pipeline immediately and returns a response without calling the next handler. Supports static status_code and dynamic status_attr.LoopContext – mutable state container for loop iterationsMapAccumulator – mutable result container for MapMiddleware iterations.FlowSignal – mutable PSR-7 compliant signal object for break/continue communication between BreakMiddleware/ContinueMiddleware and the enclosing loop.Interpolator – unified token resolver shared by HttpMiddleware, CollectMiddleware, and any custom middleware. Extensible via resolveToken() override.ConfigLoader – loads and validates middleware.json with use alias resolution, standalone and cacheableEmitter – sends PSR-7 responses to the SAPI with chunked streaming{
"Curlpit\\Core\\Middleware\\HttpMiddleware": {
"url": "https://api.example.com/posts/{attr:current_post.id}/comments",
"method": "GET",
"headers": { "Accept": "application/json", "x-api-key": "{env:API_KEY}" },
"store_response_as": "comments_raw",
"store_status_as": "comments_status",
"timeout": 10
}
}
{
"Curlpit\\Core\\Middleware\\MapMiddleware": {
"source_attr": "posts_raw",
"source_path": "data",
"item_attr": "current_post",
"result_attr": "final_results",
"accumulator_attr": "__map_posts",
"middleware": [
{
"Curlpit\\Core\\Middleware\\HttpMiddleware": {
"url": "https://api.example.com/users/{attr:current_post.userId}",
"store_response_as": "user_raw",
"store_status_as": "user_status"
}
},
{
"Curlpit\\Core\\Middleware\\IfMiddleware": {
"condition": { "type": "attr", "name": "user_status", "eq": 404 },
"then": [ { "Curlpit\\Core\\Middleware\\ContinueMiddleware": {} } ]
}
},
{
"Curlpit\\Core\\Middleware\\CollectMiddleware": {
"accumulator_attr": "__map_posts",
"template": {
"post": "{attr:current_post}",
"user": "{attr:user_raw}"
}
}
}
]
}
}
{
"Curlpit\\Core\\Middleware\\IfMiddleware": {
"condition": { "type": "attr", "name": "api_status", "eq": 503 },
"then": [
{ "Curlpit\\Core\\Middleware\\HaltMiddleware": { "status_code": 503 } }
],
"else": [
{ "Curlpit\\Core\\Middleware\\NoopMiddleware": {} }
]
}
}
Any PSR-15 compliant middleware works with Curlpit without modification or wrappers – including inside loops, maps, and try bodies. Curlpit’s flow control state (__pc, __jump_to, __flow_signal) travels in request attributes and is invisible to third-party middleware.
For middleware with complex constructor dependencies, wire them up in instantiate():
protected function instantiate(string $class, array $options): MiddlewareInterface
{
return match ($class) {
\Middlewares\AccessLog::class => new AccessLog($this->buildLogger()),
default => parent::instantiate($class, $options),
};
}
Curlpit supports fully declarative middleware configuration via middleware.json.
For simpler cases, there is no need to register or wire anything in your application code. All dependencies, configuration values, and even method calls can be defined in a single place.
Constructor parameter names must match the keys in middleware.json – snake_case is the convention
for autowire-compatible middleware (e.g. store_response_as maps directly to the JSON key).
Note: Declarative autowire is experimental. The API may change in future versions.
composer require middlewares/access-log monolog/monolog
{
"middleware": [
{
"Middlewares\\AccessLog": {
"autowire": {
"logger": {
"class": "Monolog\\Logger",
"args": [
"access",
[
{
"class": "Monolog\\Handler\\StreamHandler",
"args": ["../logs/access.log", 200]
}
]
]
}
}
}
}
]
}
composer require middlewares/http-authentication
Generate a password hash – the middleware uses password_verify() internally,
so plain text passwords will never match. The hash is safe to store in
middleware.json: even if the file is exposed, it cannot be reversed.
php -r "echo password_hash('your_password', PASSWORD_DEFAULT);"
{
"middleware": [
{
"Middlewares\\BasicAuthentication": {
"autowire": {
"users": { "admin": "$2y$12$abc123..." }
},
"calls": [["verifyHash", []]]
}
}
]
}
This is an optional integration. Most projects will not need it.
For projects where middleware share services (loggers, database connections, etc.) and a PSR-11 container is already in use, Curlpit provides ContainerApplication:
use Curlpit\App\ContainerApplication;
$app = new ContainerApplication($responseFactory, $streamFactory, $container);
$response = $app->handle($serverRequest);
(new Emitter())->emit($response);
See middleware.example.json for a full pipeline demonstrating all built-in middleware
working together: multi-API aggregation across 3 domains, conditional branching,
iteration flow control with break/continue, environment-based auth, and graceful
error handling with halt.
DBCommander – a Norton Commander-style MySQL manager built on Curlpit.
MIT