Project Structure
Six directories and a single bootstrap. Here's where each kind of code goes.
You will learn
- What lives in each ZealPHP directory — and why the convention exists
- How to bootstrap a server in three lines
- The decision rule: route vs api vs public vs src
- How PSR-4 autoloading wires src/ to your namespace
The shape of a ZealPHP app
A fresh app (from composer create-project sibidharan/zealphp-project) lays out
like this:
my-app/
├── app.php ← bootstrap: middleware + $app->run()
├── composer.json ← autoload "App\\" => "src/"
├── public/ ← drop-in pages — filesystem is the router
│ ├── index.php
│ ├── about.php → GET /about
│ └── css/site.css → GET /css/site.css (static)
├── api/ ← file-based REST API (auto-routed)
│ └── users/
│ ├── get.php → GET /api/users
│ └── post.php → POST /api/users
├── route/ ← explicit routes — loaded at startup
│ ├── ws.php ← WebSocket endpoints
│ ├── streaming.php ← $app->route() with custom paths
│ └── timers.php ← App::tick() / after()
├── template/ ← view templates (rendered via App::render)
│ ├── _master.php
│ ├── components/
│ └── pages/
└── src/ ← your business logic — PSR-4 autoloaded
├── Auth.php
└── Notes.php
The three-line server
Every ZealPHP app begins with one file. Here’s the minimum viable bootstrap:
<?php
require_once __DIR__ . '/vendor/autoload.php';
ZealPHP\App::init('0.0.0.0', 8080)->run();
App::init() creates the server. $app->run() starts the event loop
and blocks until you stop it. Add files to public/ and they become URLs immediately.
No more configuration is required.
A realistic bootstrap
Real apps add middleware, the occasional inline route, maybe a Store table.
app.php stays under 50 lines for most apps:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use ZealPHP\App;
use ZealPHP\Store;
use ZealPHP\Middleware\{CorsMiddleware, ETagMiddleware, SessionStartMiddleware};
App::superglobals(false); // coroutine mode (default for new apps)
$app = App::init('0.0.0.0', 8080);
$app->addMiddleware(new CorsMiddleware()); // outermost
$app->addMiddleware(new ETagMiddleware());
$app->addMiddleware(new SessionStartMiddleware()); // innermost (closest to handler)
// Shared-memory tables MUST be made before run() so workers inherit them.
Store::make('rate_limits', 10000, [
'count' => [\OpenSwoole\Table::TYPE_INT, 4],
'reset' => [\OpenSwoole\Table::TYPE_INT, 4],
]);
// Inline route — fine for one-off endpoints. Most routes live in route/ or api/.
$app->route('/health', fn() => ['ok' => true]);
$app->run();
Everything above $app->run() runs once, in the master process, before
workers fork. Use it for setup that the whole server depends on: middleware, shared tables,
timer registration. Anything per-request goes in a handler.
The six directories — what goes where
public/ — pages, by filesystem
Drop a .php file in public/, get a URL. public/about.php
responds at /about. Static files (CSS, JS, images) work the same way. This is the
Apache-style convention: filesystem = router.
api/ — REST endpoints, by method
File-based REST: api/users/get.php → GET /api/users. Inside the file
you define one closure named after the HTTP method ($get, $post, etc.).
The framework auto-binds it with parameter injection. Cleanest pattern for JSON APIs.
route/ — explicit registrations
Files in route/ are loaded at startup. Use this for anything that doesn’t
fit the filesystem convention: WebSocket endpoints ($app->ws()), URL params
(/users/{id}), namespaced groups ($app->nsRoute('admin', ...)),
or background timers (App::tick()). Each file registers many routes.
template/ — views only
Templates render HTML. Call them with App::render('pages/about', ['title' => ...]).
The hard rule: no business logic, no inline <script> blocks,
no style= attributes. Templates produce HTML; everything else lives in src/
or public/js/.
src/ — your business logic
Classes with proper namespaces, autoloaded via PSR-4. Your route handlers and API files should delegate here. A 50-line handler is a smell — extract a service class.
// composer.json
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
With that, src/Auth.php defining App\Auth is reachable from anywhere
as new \App\Auth(). Run composer dump-autoload after adding new
classes for the first time.
app.php — bootstrap, nothing else
Keep it thin. Middleware registration, Store::make() calls, the occasional inline
route. If app.php grows past ~100 lines, you’re putting business logic where
it doesn’t belong — move it.
Where do I put X?
| I want to… | Put it in… |
|---|---|
Add a marketing page like /pricing | public/pricing.php |
| Add a static CSS / JS file | public/css/ or public/js/ |
Add a JSON API endpoint like GET /api/users | api/users/get.php |
Add a URL-param route like /users/{id} | route/users.php with $app->route(...) |
| Add a WebSocket endpoint | route/ws.php with $app->ws(...) |
| Add a background timer | route/timers.php using App::tick() |
| Add a reusable HTML fragment | template/components/_card.php |
| Add a layout used by many pages | template/_master.php or a new layout file |
Add a class like AuthService | src/AuthService.php (namespaced) |
| Add new middleware | src/Middleware/MyMiddleware.php, register in app.php |
The autoloader is shared across requests
One of ZealPHP’s quiet wins: the Composer autoloader runs once, when the worker starts. Every request after that gets your classes already mapped — no per-request file scan, no opcache warmup penalty. The cost of having a 500-file codebase drops to "the classes you actually use this request."
This is why we lean on PSR-4 hard. The framework is built so the autoloader does its work once and then steps out of the way.
You want to add a POST /api/login endpoint that returns JSON. Where does it go?
Key Takeaways
- Bootstrap is one file:
app.php. Three lines minimum; under 100 for most real apps. public/for filesystem-routed pages,api/for REST,route/for explicit routes,template/for views,src/for business logic.- Routes and APIs should be thin — delegate to
src/classes via PSR-4 autoloading. - Everything before
$app->run()happens once per worker. Everything inside a handler happens per request. - Edits to
template/,api/,public/reload on next request. Edits toapp.php,route/, framework middleware need a restart.