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.

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.phpGET /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 /pricingpublic/pricing.php
Add a static CSS / JS filepublic/css/ or public/js/
Add a JSON API endpoint like GET /api/usersapi/users/get.php
Add a URL-param route like /users/{id}route/users.php with $app->route(...)
Add a WebSocket endpointroute/ws.php with $app->ws(...)
Add a background timerroute/timers.php using App::tick()
Add a reusable HTML fragmenttemplate/components/_card.php
Add a layout used by many pagestemplate/_master.php or a new layout file
Add a class like AuthServicesrc/AuthService.php (namespaced)
Add new middlewaresrc/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 to app.php, route/, framework middleware need a restart.