Skip to main content

The Problem with Traditional PHP

Traditional PHP follows a “shared-nothing” architecture:
Request → PHP Boots → Load Files → Execute → Shutdown → Response

Next Request → PHP Boots → Load Files → Execute → Shutdown → Response
Every request:
  • Starts a new PHP process
  • Loads the framework from disk
  • Parses and compiles PHP files
  • Initializes dependencies
  • Finally executes your code
  • Destroys everything
Traditional PHP spends 50-80% of request time on startup overhead, not your actual code.

The Always-Alive Solution

Lyger’s Always-Alive architecture keeps PHP running in memory:
┌──────────────────────────────────────┐
│   Rust HTTP Server (Port 8000)      │
│   - Ultra-fast request parsing       │
│   - Connection handling              │
│   - Static file serving              │
└──────────────────────────────────────┘
               ↓ FFI Callback
┌──────────────────────────────────────┐
│   PHP Worker (Persistent Process)   │
│   - Framework pre-loaded             │
│   - Classes in memory                │
│   - Connections pooled               │
│   - Routes pre-compiled              │
└──────────────────────────────────────┘
With Always-Alive, only your application code executes per request. The framework overhead is paid once at startup.

How It Works

Server Startup

// From your application's entry point
use Lyger\Core\Engine;

// Define your router
$router = function(string $uri, string $method, array $data) {
    // Your routing logic here
    return Router::handle($uri, $method);
};

// Start the Always-Alive server
Engine::startServer($router, 8000);

Behind the Scenes

// From Lyger/Core/Engine.php
public static function startServer(callable $routerHandler, int $port = 8000): void
{
    echo "\n";
    echo "========================================\n";
    echo "   LYGER v0.1 - Always-Alive Server\n";
    echo "========================================\n\n";

    // Preload framework into memory
    echo "Loading framework into memory...\n";
    ServerManager::start($routerHandler);

    // Start Rust HTTP server if FFI available
    $instance = self::getInstance();

    if ($instance->ffi !== null) {
        try {
            echo "Starting Rust HTTP server on port {$port}...\n";
            $instance->ffi->lyger_start_server($port);
            self::$serverRunning = true;
        } catch (\Throwable $e) {
            echo "Note: Using PHP built-in server (FFI start_server not available)\n";
        }
    } else {
        echo "Note: Using PHP built-in server (FFI not available)\n";
    }

    // Keep the PHP worker alive
    while (self::$serverRunning) {
        sleep(1);
    }
}

ServerManager

The ServerManager class orchestrates the persistent PHP worker:
// From Lyger/Core/ServerManager.php
class ServerManager
{
    private static bool $running = false;
    private static $router = null;
    private static array $loadedClasses = [];

    public static function start(callable $router): void
    {
        self::$running = true;
        self::$router = $router;

        // Preload all framework classes into memory
        self::preloadFramework();

        echo "\n";
        echo "========================================\n";
        echo "   LYGER SERVER v0.1\n";
        echo "   Always-Alive Mode\n";
        echo "========================================\n\n";
        echo "✓ Framework loaded in memory\n";
        echo "✓ Waiting for requests...\n";
        echo "   Ctrl+C to stop\n\n";

        // Keep PHP alive - waiting for FFI callbacks from Rust
        while (self::$running) {
            sleep(1);

            if (!self::$running) {
                break;
            }
        }
    }
}

Framework Preloading

// From Lyger/Core/ServerManager.php
private static function preloadFramework(): void
{
    $classes = [
        // Core
        'Lyger\Core\Engine',
        'Lyger\Routing\Router',
        'Lyger\Http\Request',
        'Lyger\Http\Response',

        // Container
        'Lyger\Container\Container',

        // Database
        'Lyger\Database\QueryBuilder',
        'Lyger\Database\Schema',
        'Lyger\Database\Model',
        'Lyger\Database\Collection',

        // Middleware
        'Lyger\Middleware\Middleware',
        'Lyger\Middleware\CorsMiddleware',
        'Lyger\Middleware\RateLimitMiddleware',
        'Lyger\Middleware\AuthMiddleware',

        // Validation
        'Lyger\Validation\Validator',

        // Cache
        'Lyger\Cache\Cache',

        // Foundation
        'Lyger\Foundation\Env',
        'Lyger\Foundation\Path',
    ];

    foreach ($classes as $class) {
        if (class_exists($class)) {
            self::$loadedClasses[] = $class;
        }
    }
}
PHP’s class autoloader caches parsed classes. Once loaded, subsequent instantiations are nearly instant.

Request Handling

When a request arrives:
  1. Rust receives HTTP request (port 8000)
  2. Rust parses request (method, URI, headers, body)
  3. Rust calls PHP via FFI with request data
  4. PHP executes router (already in memory)
  5. PHP returns response to Rust
  6. Rust sends HTTP response to client
// From Lyger/Core/ServerManager.php
public static function handleRequest(string $uri, string $method, array $data = []): string
{
    if (self::$router === null) {
        return json_encode(['error' => 'No router configured']);
    }

    try {
        // Execute the router - instant since everything is in memory!
        $response = (self::$router)($uri, $method, $data);
        return $response;
    } catch (\Throwable $e) {
        return json_encode([
            'error' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine()
        ]);
    }
}

Performance Benefits

Startup Time Comparison

ScenarioTraditional PHPAlways-AliveImprovement
Framework boot~50ms~0ms (preloaded)
Class loading~20ms~0ms (cached)
Route compilation~10ms~0ms (compiled)
DB connection~5ms~1ms (pooled)5x
Total overhead~85ms~1ms85x
Your actual application code execution time remains the same. The improvement comes from eliminating startup overhead.

Real-World Impact

Traditional PHP:
85ms overhead + 15ms app code = 100ms total
Only 15% of time spent on your code!

Always-Alive:
1ms overhead + 15ms app code = 16ms total
94% of time spent on your code!

Result: 6.25x faster response time

Memory Considerations

Memory Usage

Traditional PHP per request:
- Process: ~8MB
- Framework: ~10MB
- Application: ~5MB
Total per concurrent request: ~23MB

Always-Alive:
- Single Process: ~8MB
- Framework: ~10MB (shared)
- Application: ~5MB (shared)
Total for all requests: ~23MB
Always-Alive uses less total memory because the framework is loaded once, not per request. However, memory leaks are permanent until server restart.

Memory Leaks

In traditional PHP, memory leaks are cleared after each request. With Always-Alive, you must be careful:
// ❌ Bad: Memory leak
class Controller {
    private static array $cache = [];
    
    public function handle($request) {
        self::$cache[] = $request; // Grows forever!
    }
}

// ✅ Good: Bounded memory
class Controller {
    public function handle($request) {
        $cache = $this->getCache(); // Instance variable, GC'd after request
        $cache->set('key', $value);
    }
}
  1. Avoid static arrays that grow - Use proper cache implementations
  2. Close resources - Database connections, file handles, etc.
  3. Unset large variables - Help PHP’s garbage collector
  4. Use dependency injection - Avoid globals and static state
  5. Monitor memory usage - Set up alerts for memory growth

Starting the Server

Command Line

php rawr serve
This starts the server on port 8000 by default.

Custom Port

php rawr serve --port=8080

Programmatic

use Lyger\Core\Engine;
use Lyger\Routing\Router;

$router = function($uri, $method, $data) {
    return Router::handle($uri, $method);
};

Engine::startServer($router, 8000);

Stopping the Server

Press Ctrl+C or send SIGTERM:
kill -TERM <pid>
Programmatically:
use Lyger\Core\Engine;

Engine::stopServer();

Development vs Production

Development Mode

In development, you’ll want to reload code changes:
# Restart on file changes (requires external tool)
ls *.php | entr -r php rawr serve
During development, restart the server when you change code. Use file watchers to automate this.

Production Mode

In production, the Always-Alive server shines:
# Run as a service (systemd example)
[Unit]
Description=Lyger Always-Alive Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php rawr serve
Restart=always

[Install]
WantedBy=multi-user.target

Comparing to Other Solutions

SolutionStartup CostMemoryComplexity
Traditional PHPEvery requestLowSimple
PHP-FPMPer processMediumMedium
RoadRunnerOnceHighComplex
SwooleOnceHighComplex
Lyger Always-AliveOnceMediumSimple
Lyger provides RoadRunner/Swoole-like performance with much simpler architecture thanks to Rust FFI.

Troubleshooting

Server Won’t Start

// Check FFI
if (!extension_loaded('ffi')) {
    echo "FFI extension not loaded\n";
}

// Check library
$engine = Engine::getInstance();
if ($engine->ffi === null) {
    echo "Rust library not found\n";
}

Port Already in Use

# Find process using port
lsof -i :8000

# Kill it
kill <pid>

# Or use a different port
php rawr serve --port=8080

Memory Growth

Monitor memory usage:
echo "Memory: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";
Set memory limit:
; php.ini
memory_limit = 256M

Next Steps

Architecture Overview

Understand the full system architecture

Rust FFI Integration

Learn how PHP talks to Rust

Zero-Copy Database

Optimize database operations

Routing

Build your application routes