1: Setup
In this tutorial you build Phlo Poll: a small poll app that asks "Which stack wins?". You start from an empty folder and end with a styled, multilingual, realtime poll. Each chapter adds one layer: a route, a view, CSS, data, voting, async updates, translations, and live results. In this first chapter you install Phlo, write your first route, and see it in the browser.
1.1: Install with Docker
The fastest way to run Phlo is the official Docker image. It contains PHP, the FrankenPHP web server, and the Phlo engine at /phlo. Scaffold a new app into ./app:
docker run -it -v $(pwd)/app:/app ghcr.io/q-ainl/phlo php /phlo/install.php /app
The installer asks a few questions. Answer like this:
App name: Poll
Host: localhost
Purpose (one line): Which stack wins?
Resources (comma-separated, empty for none): <press enter>
Create "Poll" in /app for host localhost? y
Leave the resources empty for now; you add them when the app needs them. The installer generates the project structure (app.phlo, www/app.php, data/app.json) and finishes with a clean build, so you always start green.
1.2: Start the server
Run the same image as a web server:
docker run -p 80:80 -v $(pwd)/app:/app ghcr.io/q-ainl/phlo
Open http://localhost in your browser. You see the placeholder home page with the app name and your one-line purpose. That page comes from the scaffolded app.phlo, which already contains a route, a view, and a style block. Leave it as it is; the poll gets its own file.
1.3: Your first route
Every .phlo file compiles to exactly one PHP class, named after the file. Create app/poll.phlo (next to app.phlo) with one line:
route GET hello => view('Hello')
Three things to notice:
- Route paths use spaces, not slashes.
route GET poll votematches/poll/vote. - No semicolons. A line ending terminates a statement in Phlo.
view(...)renders and terminates. A route must end inview(),apply(), orlocation(). A bare return value is discarded, soroute GET hello => 'Hello'would match but render an empty page.
Routes from all files are collected automatically; the scaffolded app.phlo activates them with app::route(). Save the file and reload the browser: nothing breaks, the new route is just not built yet.
1.4: Build and check
In development (build: true) Phlo rebuilds changed sources on every request, so reloading the browser is usually enough. The CLI gives you the same build explicitly, plus a lint check. The CLI runs inside the container, because www/app.php points at the engine at /phlo:
docker run --rm -v $(pwd)/app:/app ghcr.io/q-ainl/phlo php /app/www/app.php build::run
docker run --rm -v $(pwd)/app:/app ghcr.io/q-ainl/phlo php /app/www/app.php build::lint
The first command prints the files it compiled:
["*app.php","+poll.php","*classmap.php","*sourcemap.php"]
Run it again and it returns []: everything is built, nothing changed. build::lint must also return []; that means the compiled PHP parses cleanly. From here on, the chapters write the short form php www/app.php build::run; prefix it with the Docker command above if you use the Docker setup.
Now open http://localhost/hello. The browser shows a minimal page with the text Hello. One line of Phlo, one route, one page. In the next chapter you replace it with a real view.