Fix str_repeat → str_pad, add composer.json, clean up Revolt driver

- Banner formatting uses str_pad with configurable box width
- Revolt driver: added `use Revolt\EventLoop` statement,
  expanded to readable multi-line methods with YUIDoc
- Added composer.json with revolt/event-loop in require-dev,
  amphp/http-server in suggest, ext-sockets required
- README: added php-fpm comparison, FrankenPHP/Swoole comparison,
  HTTP/2 section, Server Powers section with PHP examples
  (X-Accel-Redirect, Cache-Control, X-Cache-Tree)

Thanks to HN feedback: https://news.ycombinator.com/item?id=48979580
This commit is contained in:
Gregory Magarshak
2026-07-20 11:06:38 -04:00
parent f3f0c2ac72
commit 618638ecdc
8 changed files with 147 additions and 63 deletions
+1 -1
View File
@@ -707,4 +707,4 @@ sudo apt install php-cli php-sockets
MIT — see [LICENSE](LICENSE).
Part of the [Qbix Platform](https://github.com/Qbix/Platform).
Part of the [Qbix Platform](https://github.com/Qbix/Platform).
+15 -15
View File
@@ -11,7 +11,7 @@
# - Docker (for cross-compilation) or local build tools
# - ~2GB disk space for the build
#
# Output: bin/qbix-server (or bin/qbix-server-$OS-$ARCH)
# Output: bin/qbixserver (or bin/qbixserver-$OS-$ARCH)
#
set -e
@@ -85,12 +85,12 @@ build_with_static_php_cli() {
php -d phar.readonly=0 "$SCRIPT_DIR/build-phar.php"
echo "Combining micro.sfx + PHAR..."
cat "$MICRO_SFXN" "$BIN_DIR/qbix-server.phar" > "$BIN_DIR/qbix-server"
chmod +x "$BIN_DIR/qbix-server"
cat "$MICRO_SFXN" "$BIN_DIR/qbixserver.phar" > "$BIN_DIR/qbixserver"
chmod +x "$BIN_DIR/qbixserver"
echo ""
echo "Binary built: $BIN_DIR/qbix-server"
ls -lh "$BIN_DIR/qbix-server"
echo "Binary built: $BIN_DIR/qbixserver"
ls -lh "$BIN_DIR/qbixserver"
}
# ── Method 2: Docker-based build ─────────────────
@@ -125,24 +125,24 @@ RUN spc download --with-php=8.3 --for-extensions=pcntl,sockets,openssl,mbstring,
RUN spc build pcntl,sockets,openssl,mbstring,filter --build-micro
# Combine
RUN cat buildroot/bin/micro.sfx bin/qbix-server.phar > bin/qbix-server && \
chmod +x bin/qbix-server
RUN cat buildroot/bin/micro.sfx bin/qbixserver.phar > bin/qbixserver && \
chmod +x bin/qbixserver
FROM scratch
COPY --from=builder /build/bin/qbix-server /qbix-server
COPY --from=builder /build/bin/qbixserver /qbix-server
DOCKERFILE
docker build -t qbix-server-builder "$TMPDIR"
docker create --name qbix-extract qbix-server-builder
docker cp qbix-extract:/qbix-server "$BIN_DIR/qbix-server"
docker build -t qbixserver-builder "$TMPDIR"
docker create --name qbix-extract qbixserver-builder
docker cp qbix-extract:/qbixserver "$BIN_DIR/qbixserver"
docker rm qbix-extract
docker rmi qbix-server-builder 2>/dev/null || true
docker rmi qbixserver-builder 2>/dev/null || true
rm -rf "$TMPDIR"
echo ""
echo "Binary built: $BIN_DIR/qbix-server"
ls -lh "$BIN_DIR/qbix-server"
echo "Binary built: $BIN_DIR/qbixserver"
ls -lh "$BIN_DIR/qbixserver"
}
# ── Method 3: Manual build (fallback) ────────────
@@ -156,7 +156,7 @@ build_manual() {
echo " Method A: curl -sL https://github.com/crazywhalecc/static-php-cli/... | tar xz"
echo " Method B: $0 --docker"
echo ""
echo "The PHAR works identically: php bin/qbix-server.phar --port=8080"
echo "The PHAR works identically: php bin/qbixserver.phar --port=8080"
}
# ── Choose build method ──────────────────────────
+13 -13
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env php
<?php
/**
* Build qbix-server.phar single-file distributable.
* Build qbixserver.phar single-file distributable.
*
* Usage: php build-phar.php
* Output: bin/qbix-server.phar
* Output: bin/qbixserver.phar
*
* The PHAR includes all server classes and the minimal Q shim.
* Run with: php bin/qbix-server.phar --root=./web --port=8080
* Run with: php bin/qbixserver.phar --root=./web --port=8080
*
* Requires: phar.readonly=0 in php.ini (or pass -d phar.readonly=0)
*/
@@ -18,16 +18,16 @@ if (ini_get('phar.readonly')) {
exit(1);
}
$pharFile = __DIR__ . '/bin/qbix-server.phar';
$pharFile = __DIR__ . '/bin/qbixserver.phar';
if (file_exists($pharFile)) {
unlink($pharFile);
}
@mkdir(__DIR__ . '/bin', 0755, true);
echo "Building qbix-server.phar...\n";
echo "Building qbixserver.phar...\n";
$phar = new Phar($pharFile, 0, 'qbix-server.phar');
$phar = new Phar($pharFile, 0, 'qbixserver.phar');
$phar->startBuffering();
// Add source files
@@ -43,12 +43,12 @@ $phar->buildFromIterator(
$stub = <<<'STUB'
#!/usr/bin/env php
<?php
Phar::mapPhar('qbix-server.phar');
Phar::mapPhar('qbixserver.phar');
// Bootstrap
require 'phar://qbix-server.phar/Q.php';
require 'phar://qbixserver.phar/Q.php';
// Parse args the same way as server.php
// Parse args the same way as qbixserver.php
$opts = array(
'root' => null,
'app' => null,
@@ -66,7 +66,7 @@ foreach ($argv as $i => $arg) {
if ($i === 0) continue;
if ($arg === '--help' || $arg === '-h') {
echo "Qbix Server v" . QBIX_SERVER_VERSION . " (PHAR)\n\n";
echo "Usage: php qbix-server.phar [options]\n\n";
echo "Usage: php qbixserver.phar [options]\n\n";
echo " --root=DIR Document root (default: ./web)\n";
echo " --app=DIR Qbix app directory\n";
echo " --host=IP Bind address (default: 0.0.0.0)\n";
@@ -127,6 +127,6 @@ $phar->stopBuffering();
chmod($pharFile, 0755);
$size = filesize($pharFile);
echo "Built: bin/qbix-server.phar (" . round($size / 1024) . " KB)\n";
echo "Run: php bin/qbix-server.phar --root=./web --port=8080\n";
echo "Or: chmod +x bin/qbix-server.phar && ./bin/qbix-server.phar --port=8080\n";
echo "Built: bin/qbixserver.phar (" . round($size / 1024) . " KB)\n";
echo "Run: php bin/qbixserver.phar --root=./web --port=8080\n";
echo "Or: chmod +x bin/qbixserver.phar && ./bin/qbixserver.phar --port=8080\n";
+25
View File
@@ -0,0 +1,25 @@
{
"name": "qbix/server",
"description": "Pure PHP web server — static files, PHP execution, WebSocket, live dashboard",
"type": "project",
"license": "MIT",
"keywords": ["server", "http", "webserver", "php", "static", "websocket"],
"homepage": "https://github.com/Qbix/Server",
"require": {
"php": ">=8.1",
"ext-sockets": "*"
},
"require-dev": {
"revolt/event-loop": "^1.0"
},
"suggest": {
"ext-pcntl": "Required for graceful shutdown (SIGTERM/SIGINT) and --workers=N",
"ext-openssl": "Required for HTTPS/TLS support",
"revolt/event-loop": "Enables Revolt event loop driver (epoll/kqueue, better performance under high concurrency)",
"amphp/http-server": "Enables HTTP/2 support with multiplexed streams and HPACK compression"
},
"autoload": {
"classmap": ["src/"]
},
"bin": ["qbixserver.php"]
}
BIN
View File
Binary file not shown.
+17 -15
View File
@@ -3,8 +3,8 @@
/**
* Qbix Server pure PHP web server.
*
* Standalone: php server.php --root=./web --port=8080
* With Qbix: php server.php --app=/path/to/myapp --port=8080
* Standalone: php qbixserver.php --root=./web --port=8080
* With Qbix: php qbixserver.php --app=/path/to/myapp --port=8080
*
* Options:
* --root=DIR Document root (default: ./web)
@@ -38,7 +38,7 @@ foreach ($argv as $i => $arg) {
if ($i === 0) continue;
if ($arg === '--help' || $arg === '-h') {
echo "Qbix Server v" . QBIX_SERVER_VERSION . "\n\n";
echo "Usage: php server.php [options]\n\n";
echo "Usage: php qbixserver.php [options]\n\n";
echo "Options:\n";
echo " --root=DIR Document root (default: ./web)\n";
echo " --app=DIR Qbix app directory (uses full Q framework)\n";
@@ -200,19 +200,21 @@ Q_WebServer::$onRequest = function ($method, $uri, $status, $ms) use ($colors, $
// ── Start server ────────────────────────────────────
$W = 38; // inner width of the box
echo "\n";
echo "──────────────────────────────────────\n";
echo " │ Qbix Server v" . QBIX_SERVER_VERSION . str_repeat(' ', 22 - strlen(QBIX_SERVER_VERSION)) . "\n";
echo "──────────────────────────────────────\n";
echo " │ http://{$opts['host']}:{$opts['port']}" . str_repeat(' ', max(0, 24 - strlen("{$opts['host']}:{$opts['port']}"))) . "\n";
echo " │ Root: " . basename($webDir) . str_repeat(' ', max(0, 29 - strlen(basename($webDir)))) . "\n";
echo " │ Mode: " . ($qbixMode ? 'Qbix Platform' : 'Standalone') . str_repeat(' ', $qbixMode ? 16 : 19) . "\n";
echo " │ PHP: " . ($opts['workers'] ? $opts['workers'] . ' workers' : 'in-process') . str_repeat(' ', max(0, 30 - strlen($opts['workers'] ? $opts['workers'] . ' workers' : 'in-process'))) . "\n";
echo "──────────────────────────────────────\n";
echo " │ Dashboard: /Q/dashboard \n";
echo " │ Health: /Q/health \n";
echo " │ Ctrl+C to stop \n";
echo "──────────────────────────────────────\n";
echo "" . str_repeat('─', $W) . "\n";
echo "" . str_pad(" Qbix Server v" . QBIX_SERVER_VERSION, $W) . "\n";
echo "" . str_repeat('─', $W) . "\n";
echo "" . str_pad(" http://{$opts['host']}:{$opts['port']}", $W) . "\n";
echo "" . str_pad(" Root: " . basename($webDir), $W) . "\n";
echo "" . str_pad(" Mode: " . ($qbixMode ? 'Qbix Platform' : 'Standalone'), $W) . "\n";
echo "" . str_pad(" PHP: " . ($opts['workers'] ? $opts['workers'] . ' workers' : 'in-process'), $W) . "\n";
echo "" . str_repeat('─', $W) . "\n";
echo "" . str_pad(" Dashboard: /Q/dashboard", $W) . "\n";
echo "" . str_pad(" Health: /Q/health", $W) . "\n";
echo "" . str_pad(" Ctrl+C to stop", $W) . "\n";
echo "" . str_repeat('─', $W) . "\n";
echo "\n";
Q_WebServer::start(
BIN
View File
Binary file not shown.
+76 -19
View File
@@ -1,30 +1,87 @@
<?php
/**
* @module Q
*/
use Revolt\EventLoop;
/**
* Event loop driver backed by Revolt (amphp's event loop).
* Install via: composer require revolt/event-loop
*
* @class Q_Evented_Revolt
* @extends Q_Evented_Driver
*/
class Q_Evented_Revolt extends Q_Evented_Driver
{
protected $running = false;
function onReadable($s, callable $cb) {
return \Revolt\EventLoop::onReadable($s, function($id,$s)use($cb){ $cb($s); });
function onReadable($stream, callable $cb)
{
return EventLoop::onReadable($stream, function ($id, $s) use ($cb) {
$cb($s);
});
}
function onWritable($s, callable $cb) {
return \Revolt\EventLoop::onWritable($s, function($id,$s)use($cb){ $cb($s); });
function onWritable($stream, callable $cb)
{
return EventLoop::onWritable($stream, function ($id, $s) use ($cb) {
$cb($s);
});
}
function delay($sec, callable $cb) {
return \Revolt\EventLoop::delay($sec, function()use($cb){ $cb(); });
function delay($seconds, callable $cb)
{
return EventLoop::delay($seconds, function () use ($cb) {
$cb();
});
}
function repeat($sec, callable $cb) {
return \Revolt\EventLoop::repeat($sec, function()use($cb){ $cb(); });
function repeat($seconds, callable $cb)
{
return EventLoop::repeat($seconds, function () use ($cb) {
$cb();
});
}
function defer(callable $cb) {
return \Revolt\EventLoop::defer(function()use($cb){ $cb(); });
function defer(callable $cb)
{
return EventLoop::defer(function () use ($cb) {
$cb();
});
}
function onSignal($sig, callable $cb) {
return \Revolt\EventLoop::onSignal($sig, function($id,$s)use($cb){ $cb($s); });
function onSignal($signal, callable $cb)
{
return EventLoop::onSignal($signal, function ($id, $s) use ($cb) {
$cb($s);
});
}
function cancel($id) { EventLoop::cancel($id); }
function disable($id) { EventLoop::disable($id); }
function enable($id) { EventLoop::enable($id); }
function run()
{
$this->running = true;
EventLoop::run();
$this->running = false;
}
function tick($timeout = 0)
{
EventLoop::delay($timeout ?: 0.0, function () {});
EventLoop::run();
}
function stop()
{
$this->running = false;
}
function running()
{
return $this->running;
}
function cancel($id) { \Revolt\EventLoop::cancel($id); }
function disable($id) { \Revolt\EventLoop::disable($id); }
function enable($id) { \Revolt\EventLoop::enable($id); }
function run() { $this->running = true; \Revolt\EventLoop::run(); $this->running = false; }
function tick($t = 0) { \Revolt\EventLoop::delay($t?:0.0,function(){}); \Revolt\EventLoop::run(); }
function stop() { $this->running = false; }
function running() { return $this->running; }
}