Getting started

A site built on Phpanta is laid out by convention, vendors the framework as a git submodule, and tells it about itself through one class.

The layout

site/ ├── autoload.php requires phpanta/autoload.php, maps the site's namespace, boots the app ├── composer.json psr-4: the site's namespace → src/<Ns>/, and Phpanta\ → phpanta/src/ ├── public/index.php a last-resort handler, then <Site>::current()->run() ├── src/<Ns>/ the app class, controllers, views, models, words ├── data/ what the site reads at runtime, outside the webroot ├── assets/ts/ the site's TypeScript; assets/ts/phpanta links to the framework's ├── assets/css/ the site's stylesheet, as one @import manifest └── phpanta/ the framework, as a git submodule

Vendoring it

git submodule add ../phpanta.git phpanta ln -s ../../phpanta/assets/ts assets/ts/phpanta git config submodule.recurse true git config push.recurseSubmodules on-demand

The URL is relative, so every remote the site is pushed to finds its own copy of the framework beside it. Editing the framework in place is the point: a change is a commit in phpanta/, then git add phpanta and a commit in the site.

The app

A site is a subclass of Phpanta\App, booted once per process by its autoloader. It owes the framework a handful of answers:

final class Site extends App { public function name(): string { return 'Acme'; } public function above(): Directory { return new Directory(dirname(__DIR__, 2)); } public function languages(): Languages { return new Languages(Language::English); } public function shell(): Shell { return new Layout(); } public function vocabulary(): Vocabulary { return Vocabulary::standard(); } public function buildId(): string { return AssetManifest::SCRIPT; } public function routes(): Collection { return new Collection(Route::class)->with( new Route(AcmePath::Home, fn() => new HomeController()), ); } public function notFound(Request $request): Response { return new ViewResponse(new NotFoundView(), HttpStatusCode::NotFound); } protected function ownDataFiles(): Collection { return new Collection(DataFileName::class); } }

Everything else — where data/ is, which directory is the webroot, where the update serial lives, the error log, the route to the API — the framework derives from those, and the derivations are final.

A page

A route pairs a Path case with a controller, and the controller returns a response. A ViewResponse renders its view inside the app's shell — or, for a request Navigation made, as a fragment led by its title.

final class HomeView extends View { public function pageTitle(): Translatable { return self::title(); } public function content(): Node { return new Element(HtmlTag::P)->containing(AcmeText::Hello); } }

Building

tsc # assets/ts/ → public/assets/js/ node phpanta/tools/build-css.mjs # assets/css/main.css → public/assets/css/style.css node phpanta/tools/build-assets.mjs # the stamped AssetManifest the shell reads node phpanta/tools/build-prod.mjs # build/dist/ — bundled, minified, no maps php -S localhost:8080 -t public phpanta/tools/dev-router.php

The build tools find the project by walking up from where they are run to the nearest composer.json, and read the site's namespace from its psr-4.

Deploying

A PHP host gets plain files: phpanta/src/ and phpanta/autoload.php, the site's src/ and autoload.php, and the prod webroot — copied by any means, or sent as one signed request by the framework's PushUpdate command. A static host gets an export:

php phpanta/tools/export.php --out build/pages --base /phpanta/

Every page is rendered by its own route's controller — once more in each language, at an address that names it — every address is moved under the base path, and the export fails on any link to a page it did not write or to an anchor that page does not have. This site is that command's output.