phporbit
A safe PHP framework that runs on itself. One application, unchanged, on its own server, FrankenPHP, nginx+FPM or Apache.
phporbit is a small PHP framework built around one constraint: the same application runs unchanged on four deployment targets, and it never has to know which one it is on.
| Target | Process model | Used for |
|---|---|---|
./orbit serve | Long-lived, self-hosted | Development |
| FrankenPHP worker | Long-lived | Production |
| nginx + PHP-FPM | Per-request | Production |
| Apache | Per-request | Production |
“Runs on itself” is literal. ./orbit serve is a real HTTP/1.1 server built on sockets, sharing the exact request pipeline used in production — not a router script in front of php -S.
A complete application
This is a working app. Two files, no configuration.
<?php
// app/routes.php
use PhpOrbit\Http\Response;
use PhpOrbit\Routing\RouteCollection;
return static function (RouteCollection $routes, bool $debug): void {
$routes->get('/', static fn (): Response => Response::text('Hello.'));
$routes->get('/hello/{name}', HelloController::class, 'hello');
};<?php
// app/src/Controllers/HelloController.php
final class HelloController implements Handler
{
public function __construct(private readonly TemplateEngine $view)
{
}
public function handle(ServerRequest $request): Response
{
// {{ }} escapes. The payload arrives as text, not markup.
return $this->view->respond('hello', ['name' => $request->attribute('name')]);
}
}$ ./orbit serve
phporbit listening on http://127.0.0.1:8080 (production mode) — Ctrl-C to stopWhat it gives you
Two ideas worth knowing before you start
1. Worker safety is a correctness rule, not a deployment concern
The four targets split into two incompatible process models:
- Per-request (Apache, nginx+FPM) tears down the process after every response. Global state is free — nothing survives to leak.
- Long-lived workers (the built-in server, FrankenPHP) boot once and serve thousands of requests in one process. Anything mutable that outlives a request leaks across users.
Code that is safe under a worker is automatically correct per-request. The reverse is not true. So phporbit assumes the worker model everywhere, and the framework is shaped to make the leak impossible rather than to warn you about it. How that is enforced →
2. The safe path is the default path
You do not opt into safety; you opt out of it, visibly:
- Templates escape with
{{ }}. Raw output needs the deliberately loud{!! !!}. - CSRF protection is on. A route opts out with
csrfExempt: true. - The database has no method that takes an interpolated query.
- An
UPDATEorDELETEwith nowhere()throws unless you callaffectingEveryRow(). - Uploads are judged by their bytes, never by their filename or declared type.
Getting started has you serving pages in about a minute. Architecture explains the process-model split that everything else follows from — it is the shortest path to understanding why the rest of the framework looks the way it does.