Architecture

A request, from the rewrite that sends it to index.php to the bytes that go back.

The request, traced

public/index.php ├─ set_exception_handler(…) the last resort, depending on nothing └─ Site::current()->run() ├─ ErrorLog::install(…) every diagnostic into data/logs/ ├─ SecurityHeaders::send() CSP, HSTS, Permissions-Policy, COOP, CORP — before anything can fail ├─ Request::fromGlobals() $_SERVER → a typed, readonly Request ├─ App::handle() the request → an Answer, sending nothing │ ├─ App::layers() what stands around every route: a layer answers, or hands on │ ├─ Router::dispatch() the path → a route → its method gate → its layers → a controller │ └─ Response::answer() status, headers and body — the security headers first └─ Answer::send() the one place anything is sent

The exception handler is installed first because it has to work when nothing else did: it logs, sends a bare 500 if the headers have not gone out, and tells the visitor nothing else.

The layers

src/ ├── App.php what a site tells the framework about itself ├── Router.php URL → controller, and nothing else ├── Controller/ the Controller interface, and the API's ├── Http/ Request, Input, Upload, Session, Answer, the responses, every header typed; Api/, Security/ ├── View/ View, Shell; Html/ — the markup tree, MarkupParser, Vocabulary ├── Form/ a form as an enum of fields, its rules, a submission read and re-rendered ├── Data/ SQLite through PDO: statements, typed rows, transactions, migrations ├── Text/ Translatable, Translation, Language, Languages, an address per language ├── Support/ Collection, File, Directory, Path and Route, Throttle, PublicKey, TarArchive ├── Model/ Api/ (a signed call), Update/ (a push, the release it replaced), Health/ (requirements) ├── Service/ Auth, Login, ApiGate, UpdateApplier, ReleaseRecord; Layer/, and the API's handlers └── Exception/ SiteException and every condition under it

The dependencies point one way. Controllers use services and views, views and models build elements out of View\Html, and View\Html depends on nothing above it.

The app

One per process, booted by the site's autoloader and read back with App::current(). Constructing one does nothing, booting it twice is booting it once, and a second app is refused. The framework's deep code — the API's key and replay serial, the health report — asks the booted app, rather than every constructor on the way down carrying the site's facts.

The wire

A request is parsed defensively. An unknown method is null, not a guess at GET, and a target PHP's parser cannot read is kept as the path it is rather than answered with the home page. The security headers are typed objects, sent before anything that could fail, and every page varies on exactly the headers it was rendered from.

The signed API

An update is a gzipped tar in the body of one POST, signed with an ECDSA P-256 key of which the server holds only the public half. The signature covers the action, a timestamp, a serial the server spends before it applies anything, and a hash of the body. A call that is unsigned or signed wrongly is answered exactly as an address that does not exist.

SPA navigation

Every link is a real href. Navigation intercepts a click on one that stays on this origin, asks for the page with X-Requested-With, and swaps the answer into #content. A server running the framework answers with a fragment; a static host like this one answers with the whole page, and only its content and its title are taken — and, when the page is in another language, the parts of the shell written in the language it replaces.

Further reading