ZealPHP - An Open Source PHP Framework Powered by OpenSwoole

ZealPHP is a lightweight, high-performance PHP framework that brings modern web development paradigms to PHP using OpenSwoole's asynchronous I/O capabilities. It offers features comparable to frameworks like Next.js and allows developers to build efficient web applications with PHP.

Features

OpenSwoole vs Node.js - Achieving True Parallelism

OpenSwoole enables PHP to achieve true parallelism through multi-threading and multi-processing, allowing applications to utilize multiple CPU cores effectively. It provides asynchronous, non-blocking I/O operations and implements coroutines at the C level, enabling high concurrency with minimal overhead.

In contrast, Node.js operates on a single-threaded event loop. While it handles I/O-bound tasks efficiently using asynchronous operations, it may struggle with CPU-intensive tasks without additional configuration. Node.js can use worker threads for parallel execution, but this adds complexity and is not default behavior.

Comparison Table

Aspect OpenSwoole (with ZealPHP) Node.js
Parallelism True parallelism with multiple processes/threads, solves C1000K without additional configuration Single-threaded; parallelism via worker threads, cant solve C1000K without additional configuration
Concurrency Model Coroutines and event-driven architecture Event loop with asynchronous callbacks/promises
Performance High performance for both I/O and CPU-bound tasks Efficient for I/O-bound tasks; additional setup for CPU-bound tasks
Scalability Utilizes multiple CPU cores; scales vertically and horizontally Scales horizontally using multiple instances

Why is ZealPHP lightweight?

ZealPHP doesn't load everything into memory, it does dynamic injection on demand, encouraging SSR operations with the powerful dynamic in-memory nested templating engine to support Progressive Enhancement and Graceful Degredation concepts, thus preserves the natural flow of web.

Serving the Public Folder

ZealPHP serves the public folder by automatically mapping URLs to PHP files within the public directory, enabling support for traditional PHP applications without needing explicit routes. For example, a request to /home will serve the file public/home.php if it exists.

By default, ZealPHP serves the public/index.php file for the root URL. All the .php extensions are removed from the URL. This behavior can be overrided for extended support. Applications reling on .htaccess or nginx URL rewriting can easily run within ZealPHP's public directory with correct routes programmed.

Defining APIs

To define APIs, place your PHP files inside the api directory. ZealPHP provides implicit routing for these API endpoints. For instance, the file api/test.php defines an API endpoint at /api/test:

<?php
// File: api/test.php
$test = function () {
    // API logic here or render HTML with coroutines and async I/O and just output them to STDIO
};

Rendering Templates

To render templates, use the App::render method to include template files from the template directory. This allows for reusable components and layouts. In public/home.php, you can render a master template like this:

<?php
use ZealPHP\App;

App::render('_master', [
    'title' => 'ZealPHP',
    'description' => 'A simple PHP framework for OpenSwoole',
]);

Getting Started with ZealPHP

To start using ZealPHP, you need to install OpenSwoole and set up your development environment. ZealPHP leverages OpenSwoole to provide an asynchronous, high-performance PHP server.

Installing OpenSwoole

  1. Install dependencies:
    # Install required packages
    sudo apt install gcc php-dev openssl libssl-dev curl libcurl4-openssl-dev libpcre3-dev build-essential
  2. Install OpenSwoole:
    sudo pecl install openswoole
    During installation, enable coroutine sockets, OpenSSL support, HTTP2 protocol, coroutine MySQL, and coroutine CURL when prompted.
  3. Add OpenSwoole extension to php.ini:
    echo "extension=openswoole.so" | sudo tee -a /etc/php/8.3/cli/conf.d/99-zealphp-swoole.ini
    echo "short_open_tag=on" | sudo tee -a /etc/php/8.3/cli/conf.d/99-zealphp-swoole.ini
  4. Verify installation:
    php -m | grep openswoole

Setting Up a ZealPHP Project

  1. Install Composer if not already installed:
    sudo apt install composer
  2. Create a new ZealPHP project:
    composer create-project --stability=dev sibidharan/zealphp-project my-project
    cd my-project
    composer update
    php app.php
  3. Your ZealPHP server should now be running, and you can start developing your application.

Understanding ZealPHP's Architecture

ZealPHP is designed to integrate seamlessly with OpenSwoole, allowing developers to write asynchronous PHP applications with ease. It provides a familiar development experience while leveraging the power of coroutines and asynchronous I/O.

Minimal Code to serve public and api endpoints

<?php
// File: app.php

require 'vendor/autoload.php';

use ZealPHP\App;

$app = App::init('0.0.0.0', 8080);

# Define routes here

$app->route('/hello/{name}', function($name){
    echo "Hello, $name!";
});

# Additional routes can be added in `route` directory also

$app->run();

Sample Route Definitions

Below is an example of how additional user definded routes are defined in ZealPHP:

<?php
// In app.php or inside route directory
$app->route('/quiz/{page}', function($page) {
    echo "<h1>This is quiz: $page</h1>";
});

use function ZealPHP\response_add_header;
use function ZealPHP\response_set_status;

$app->route("/suglobal/{name}", [
    'methods' => ['GET', 'POST']
],function($name) {

    response_add_header('X-Prototype',  'buffer');
    response_set_status(202);
    // $g = G::instance();
    if(App::$superglobals){
        if (isset($GLOBALS[$name])) {
            print_r($GLOBALS[$name]);
        } else{
            echo "Unknown superglobal";
        }
    } else {
        //This works both ways with, with App::superglobal(true), this will write to actual superglobal
        $g = G::instance();
        if (isset($g->$name)) {
            print_r($g->$name);
        } else{
            echo "Unknown global";
        }
    }
});

// You can add namespaces to routes for better management
$app->nsRoute('watch', '/get/{key}', function($key){
    echo $_GET[$key] ?? null; //with App::superglobal(true);
    
    // use ZealPHP\G; # the superglobal implementation that works both ways
    $g = G::instance();
    $g->get['key'] = $key;
    echo $g->get['key'];
});

// This is also the implicit implementation of API routes
$this->nsPathRoute('api', "{module}/{rquest}", [
    'methods' => ['GET', 'POST', 'PUT', 'DELETE']
    ], function($module, $rquest, $response, $request){
    $api = new ZealAPI($request, $response, self::$cwd);
    try {
        return $api->processApi($module, $rquest);
    } catch (\Exception $e){
        $api->die($e);
    }
});

//Returning PSR Responses
$app->route("/coglobal/set/session", [
    'methods' => ['GET', 'POST']
],function($name) {
    //Out buffer will be ignored when returning PSR responses
    echo "Hello World";
    $G = G::instance();
    $G->session['name'] = $name;
    return new Response('Session set', 300, 'success', ['Content-Type' => 'text/plain', 'X-Test' => 'test']);
});

// Matches any URL starting with /raw/
$app->patternRoute('/raw/(?P.*)', ['methods' => ['GET']], function($rest) {
    echo "You requested: $rest";
    return 202; // return status code
});

# Implicit route for ignoring PHP extensions, requesting OpenSwoole response with Dynamic Injection
# In such cases, the STDIO buffer will be ignored and $response->write takes precedence
$this->patternRoute('/.*\.php', ['methods' => ['GET', 'POST']], function($response) {
    $response->status(403);
    $response->write("403 Forbidden");
});

# Implicit route for index.php
$this->route('/', function($response){
    $g = G::instance();
    $file = 'index';
    $g->server['PHP_SELF'] = '/'.$file.'.php';

    // Not needed since G$ writes to actual superglobal when App::superglobal(true) is set
    // if(self::$superglobals){
    //     $_SERVER['PHP_SELF'] = $g->server['PHP_SELF'];
    // }

    $abs_file = self::$cwd."/public/".$file.".php";
    if(file_exists($abs_file)){
        include $abs_file;
    } else {
        //TODO: Can load user page here if file not found
        echo("404 Not Found");
        return(404);
    }
});

Note: Implicit rules are provided with the least priority, you can override them at app.php or from route directory.

Route Inclusion

All files inside route folder are included prior to the implicit routes letting the user override the implicit routes on demand. For example, `route/contact.php` may look like

<?php
// File: route/contact.php

use ZealPHP\App;

$app = App::instance();

$app->route('/data/{id}', function($id) {
    echo "This is example api to receive ID: $id";
});

$app->route('/contact', function() {
    App::render('contact');
});

Contributions

All contributions are welcome. Feel free to submit issues or pull requests on GitHub to help improve ZealPHP.

Additional Resources

For more information, visit the ZealPHP GitHub repository and explore the documentation and examples.