Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

Your First Page

Drop a file, get a URL. The filesystem is the router.

You will learn

  • Create a page by adding a file to public/
  • How implicit routing maps files to URLs
  • Add dynamic content with PHP
  • Handle query parameters

The problem

In most frameworks, adding a new page means editing a routing table, creating a controller, registering it somewhere, and maybe writing a view. That's a lot of ceremony for "show some HTML."

ZealPHP takes a different approach: the filesystem is the router.

Step 1: Create a file

Create a file at public/greeting.php:

<?php
echo "<h1>Hello, World!</h1>";
echo "<p>This page was served by ZealPHP.</p>";

Step 2: Visit the URL

Open http://localhost:8080/greeting in your browser. That's it — no routing config, no controller registration. ZealPHP saw a file at public/greeting.php and mapped it to GET /greeting automatically.

How implicit routing works

The public/ folder is a filing cabinet — folders are drawers, files are documents, and the URL mirrors the file path. public/blog/post.php/blog/post. The .php extension is stripped automatically. No routing table, no config.

That covers static pages with PHP. For URL parameters (/users/{id}), REST APIs, and the priority order between routing styles, see the How Routes Work lesson in Foundations.

Adding dynamic content

These are PHP files, so you can use any PHP you want. Let's make the greeting personal. Depending on your Lifecycle Mode, you have two amazing ways to do this:

Coroutine Mode (Default)
<?php
// $g gives you the current request's data safely.
$g    = \ZealPHP\G::instance();
$name = htmlspecialchars($g->get['name'] ?? 'World');
echo "<h1>Hello, {$name}!</h1>";
Coroutine-Legacy Mode
<?php
// Classic PHP syntax just works!
$name = htmlspecialchars($_GET['name'] ?? 'World');
echo "<h1>Hello, {$name}!</h1>";

Visit /greeting?name=Alice and the page greets Alice by name.

Which one should I use? For brand new apps, Coroutine Mode (the default) is the fastest and uses $g->get to prevent global state leaks. However, if you are migrating an older application or simply prefer writing classic PHP syntax, you can flip the switch in your app.php to App::mode(App::MODE_COROUTINE_LEGACY). This leverages ext-zealphp to magically isolate $_GET and other superglobals per-coroutine! Lesson 4 explains the mental model, and Lesson 26 details the caveats you should be careful about when using the experimental coroutine-legacy mode.

See it live

This very site uses implicit routing. The page you're reading right now is served from public/learn.php. Every page in the docs — /routing, /streaming, /ws — is a file in public/.

Try it: Open the greeting demo →

What about messy URLs?

"But I don't want .php in my URLs!" — you won't see them. ZealPHP strips the extension automatically. public/about.php responds at /about, not /about.php. Requesting /about.php directly returns 403.

Using a layout

Right now, your page outputs raw HTML with no <head>, no stylesheet, no navigation. Every real page needs a shared layout. The next nine Foundations lessons cover the request lifecycle, routing, and the response model that makes layouts work; Lesson 13 (Layouts & Components) teaches you how to wrap pages in a layout template using App::render().

Here's a preview — this is how most pages in ZealPHP apps look:

<?php use ZealPHP\App;
App::render('/_master', [
    'title' => 'About',
    'page'  => 'about',
]);

That's the entire file. Three lines. The master template handles the HTML shell, nav, footer, and CSS — your page template only has the content.

If you create a file at public/blog/post.php, what URL will serve it?

Key Takeaways

  • Files in public/ automatically become URLs — no routing config needed
  • The URL mirrors the file path: public/blog/post.php/blog/post
  • Use $g->get or switch to Coroutine-Legacy mode for classic $_GET — both are safe!
  • .php extensions are stripped — clean URLs by default