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.

TargetProcess modelUsed for
./orbit serveLong-lived, self-hostedDevelopment
FrankenPHP workerLong-livedProduction
nginx + PHP-FPMPer-requestProduction
ApachePer-requestProduction

“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
<?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
<?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')]);
    }
}
Shell
$ ./orbit serve
phporbit listening on http://127.0.0.1:8080 (production mode) — Ctrl-C to stop

What 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 UPDATE or DELETE with no where() throws unless you call affectingEveryRow().
  • Uploads are judged by their bytes, never by their filename or declared type.
Where to go next

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.