Getting Started
From a fresh machine to a running ZealPHP app — install dependencies, scaffold a project, write your first route, deploy.
Installs PHP 8.3 + OpenSwoole + ext-zealphp + composer. Auto-detects your distro and bails with manual steps if it can't install for you (Fedora, Arch, Alpine, etc.). The detailed walkthrough below covers manual install, Docker, scaffolding, and deploy. Inspect the script first ↓
Your AI app without ZealPHP
Your AI app on ZealPHP
php app.phpNo Redis. No message queue. No sidecar. No microservice fan-out.
1. Prerequisites
| Package | Version | Why |
|---|---|---|
PHP | 8.3+ | Tested on 8.3 and 8.4; OpenSwoole 26.2+ adds PHP 8.5 support |
OpenSwoole | 22.1+ | Async runtime, HTTP/WebSocket server, coroutines (26.2+ for PHP 8.5) |
ext-zealphp | ≥0.1.0 | Overrides header(), setcookie(), session_* at runtime (or uopz as fallback) |
composer | 2.x | Dependency management |
uv (optional) | any | Only for AI agent examples (Python) |
2. Before you ship: known risks
- Use
$g->get/$g->session(recommended) — works in every mode, no extension needed. With ext-zealphp,$_GET/$_SESSIONare also per-coroutine safe (saved/restored on every yield/resume — S1). Without ext-zealphp in coroutine mode, PHP superglobals are process-wide and would leak. See the parity rule. Also auditstaticvariables for cross-request leaks. - Coroutine safety — references to
RequestContext::instance()(a.k.a.$g) must not be held acrossyieldpoints; each coroutine has its own context. - ext-zealphp function overrides are alpha —
session_start(),header(), etc. are intercepted via ext-zealphp (our own extension). Edge cases exist; report them. - Memory growth — workers stay alive between requests; profile for leaks under sustained load.
- API stability — v0.3.x; breaking changes possible until v1.0. Pin a version in
composer.json.
Report issues at GitHub Issues. Security disclosures: see SECURITY.md.
3. Install
One-line install on Ubuntu/Debian — pipes setup.sh straight from this site, no clone required:
curl -fsSL https://php.zeal.ninja/install.sh | sudo bash
# Installs: PHP 8.3, OpenSwoole, ext-zealphp, composer
sudo?
curl -fsSL https://php.zeal.ninja/install.sh -o install.sh && less install.sh && sudo bash install.sh
Or fetch from GitHub directly to pin a specific commit:
curl -fsSL https://raw.githubusercontent.com/zealphp/zealphp/master/setup.sh | sudo bash
If you'd rather clone first (e.g. you want to send a PR):
git clone https://github.com/sibidharan/zealphp.git
cd zealphp
sudo bash setup.sh
Or install manually:
# 1. PHP 8.3
sudo add-apt-repository ppa:ondrej/php
sudo apt install php8.3 php8.3-cli php8.3-dev php8.3-mbstring php-pear
# 2. OpenSwoole (via PECL)
sudo pecl install openswoole
echo "extension=openswoole.so" | sudo tee /etc/php/8.3/cli/conf.d/zz-openswoole.ini
echo "short_open_tag=On" | sudo tee -a /etc/php/8.3/cli/conf.d/zz-openswoole.ini
# 3. ext-zealphp (ZealPHP's own extension — ships with the framework)
git clone --depth 1 https://github.com/sibidharan/zealphp.git /tmp/zealphp-src
cd /tmp/zealphp-src/ext/zealphp && phpize && ./configure && make && sudo make install
echo "extension=zealphp.so" | sudo tee /etc/php/8.3/cli/conf.d/50-zealphp.ini
# 4. Composer
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
# 5. Verify
php -m | grep -E 'openswoole|zealphp'
short_open_tag=On? ZealPHP templates often use <?= $var ?> for compact output. This is technically a "short echo tag" (always on in PHP 8) but enabling short_open_tag matches the recommended setup for OpenSwoole.
Dockerfile and docker-compose.yml.
Run docker compose up from the cloned repo to get a fully configured container.
4. Scaffold a project
Three paths depending on what you're building:
Starter project
Clean app tree with examples. Best for new apps.
composer create-project \
zealphp/project:^0.4.12 \
my-app
cd my-app && php app.php
Framework repo
This very site, running locally. Read source + live demos.
git clone \
https://github.com/sibidharan/zealphp.git
cd zealphp
composer install && php app.php
WordPress
Unmodified WordPress on OpenSwoole.
git clone \
https://github.com/sibidharan/zealphp-wordpress.git
cd zealphp-wordpress
composer install && php app.php
How ZealPHP works — the LAMP mental model
In LAMP, Apache was the server and PHP ran inside it. In ZealPHP, OpenSwoole is the server and PHP runs inside it. Same idea, different engine.
| LAMP | ZealPHP |
|---|---|
| Apache / Nginx | OpenSwoole (built into php app.php) |
htdocs/about.php → /about.php | public/about.php → /about |
$_GET, $_POST, $_SESSION | Use $g->get / $g->post / $g->session via RequestContext::instance() — works in both modes, no extension needed. With ext-zealphp, $_GET/$_SESSION are also per-coroutine safe in both modes. See the parity rule. |
session_start(), header() | Same — overridden via ext-zealphp; populates the per-coroutine $g->session in coroutine mode |
| One process per request | One process, thousands of concurrent coroutines |
| Restart Apache after config changes | Restart php app.php after code changes |
| Needs Redis for shared state | Built-in Store — cross-worker shared memory |
| Needs Socket.io / Ratchet for WebSocket | Built-in App::ws() |
The difference: your PHP process stays alive between requests. That means persistent connections, shared memory, WebSocket, streaming — all without leaving PHP.
5. Your first page — just drop a file
Create a file in public/. It becomes a route. No framework code needed.
<?php
use ZealPHP\RequestContext;
$g = RequestContext::instance();
session_start();
$g->session['visits'] = ($g->session['visits'] ?? 0) + 1;
?>
<h1>Hello from ZealPHP</h1>
<p>You've visited this page <?= $g->session['visits'] ?> time(s).</p>
<p>Query string: <?= htmlspecialchars($g->get['name'] ?? 'world') ?></p>
$g->*.
The default app.php runs in coroutine mode (App::mode(App::MODE_COROUTINE)) — $g->session / $g->get / $g->post are the recommended accessors (per-coroutine, always safe). With ext-zealphp, $_GET/$_SESSION are automatically per-coroutine safe in both modes (saved/restored on every yield/resume).
If you're porting a legacy
.htaccess + $_* codebase, use one of the lifecycle presets before App::init(): App::mode(App::MODE_LEGACY_CGI) for unmodified WordPress/Drupal (pre-warmed subprocess pool, true per-request isolation), or App::mode(App::MODE_COROUTINE_LEGACY) to run request-style PHP concurrently with per-coroutine isolation of superglobals (S1), $GLOBALS (S2), function-local statics (S5a), and require_once re-execution (S7) (requires ext-zealphp; per-request define() isolation (S10) is a separate opt-in via App::defineIsolation(true)). The raw App::superglobals(true) flag was the legacy way these presets were configured. See Lifecycle modes and the Legacy apps page for the full migration matrix.
Start the server and visit http://localhost:8080/hello?name=PHP:
php app.php
That's it. No $app->route(), no annotations, no config files. Same for APIs — drop a file in api/:
<?php
$list = function() {
return ['devices' => ['sensor-a', 'sensor-b'], 'count' => 2];
};
public/. They work immediately. When you need WebSocket, streaming, or coroutines — that's when you use $app->route(). See Routing for the full picture.
6. Framework routes — when you need more
For URL parameters, WebSocket, streaming, or middleware — use programmatic routes in app.php:
<?php
require 'vendor/autoload.php';
use ZealPHP\App;
$app = App::init('0.0.0.0', 8080);
// Return array → auto JSON
$app->route('/api/hello', function() {
return ['message' => 'Hello from ZealPHP', 'time' => time()];
});
// URL params (Flask-style)
$app->route('/user/{id}', function($id) {
return ['user_id' => $id];
});
// Return int → HTTP status
$app->route('/forbidden', fn() => 403);
// Streaming
$app->route('/stream', function() {
return (function() {
yield "<h1>Streaming</h1>\n";
for ($i = 1; $i <= 5; $i++) {
yield "Chunk $i<br>\n";
usleep(200000);
}
})();
});
$app->run(['task_worker_num' => 0]);
Restart (Ctrl+C, then php app.php) and visit:
http://localhost:8080/api/hello— JSONhttp://localhost:8080/user/42— URL paramhttp://localhost:8080/forbidden— 403http://localhost:8080/stream— streaming response
7. Deploy
ZealPHP includes built-in CLI management. For production, use the bundled systemd service:
# 1. Adjust paths in deploy/zealphp.service (WorkingDirectory, User)
sudo cp deploy/zealphp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now zealphp
# 2. Check status & logs
sudo systemctl status zealphp
journalctl -u zealphp -f
Or run standalone (without systemd):
php app.php start -p 8080 -d # daemonize on port 8080
php app.php status # check if running
php app.php stop # stop
php app.php start -w 16 -d # 16 workers, daemonized
php app.php --help # all flags
Verification
Confirm everything is wired up:
# Extensions loaded?
php -m | grep -E 'openswoole|zealphp'
# Server responds?
curl -s http://localhost:8080/ | head -5
# Composer dependencies?
composer show zealphp/zealphp
Port in use? Run
php app.php stop or use -p 9000 for a different port.Extension not loaded? Check
php --ini for the config path, ensure extension=openswoole.so is in a loaded .ini.Permission denied on port 80? Use a port above 1024, or run with
setcap / behind a reverse proxy.