Skip to content

quillphp/quill

QuillPHP

High-performance PHP 8.3+ API framework — boot once, serve forever.

CI PHP License


The Quill Philosophy

QuillPHP is a binary-native API framework built for extreme low-latency environments. The key insight is simple: PHP never touches a socket.

The native Quill Core (Rust + Axum + Tokio) owns the entire I/O stack — TCP connections, route matching, DTO validation, and response serialisation. PHP is woken up only to run your handler, then goes back to polling. By strictly separating a one-time Boot Phase from a zero-overhead Hot Path, Quill reaches throughput that rivals compiled languages without leaving PHP.

Performance at Scale

Framework Throughput (req/s) Avg Latency Notes
Actix-web 4 (Rust) ~450,000 ~0.22 ms TFB R22 JSON, 4-core¹
Axum 0.7 (Rust / Tokio) ~330,000 ~0.30 ms TFB R22 JSON, 4-core¹
Go Fiber v2 (fasthttp) ~220,000 ~0.45 ms TFB R22 JSON, 4-core¹
QuillPHP (Native) 133,627 1.16 ms Direct measurement²
Go net/http (stdlib) ~115,000 ~0.87 ms TFB R22 JSON, 4-core¹
Node.js Fastify v4 ~68,000 ~1.47 ms TFB R22 JSON, 4-core¹
FrankenPHP (worker, NTS+JIT) ~30,000 ~3.33 ms Estimated³
Node.js Express v4 ~18,000 ~5.56 ms TFB R22 JSON, 4-core¹
FastAPI + Uvicorn (4 workers) ~11,000 ~9.09 ms TFB R22 JSON, 4-core¹
Laravel Octane (Swoole, bare) ~10,000 ~10.0 ms Bare route, no middleware⁴

¹ TFB R22 extrapolatedTechEmpower Round 22 JSON Serialization results (48-core AMD EPYC 7R13, 512 connections) scaled proportionally to 4-core equivalent for fair comparison. Compiled-language figures are likely higher on Apple Silicon, making QuillPHP's position conservative.

² Direct measurementwrk -t4 -c100 -d10s, QUILL_WORKERS=4, Apple M-series. PHP never touches the socket; Axum/Tokio owns all I/O.

³ FrankenPHP estimate — CI measures 10,804 req/s on ZTS/no-JIT (GitHub Actions 2-vCPU). NTS + JIT is documented at 2–3× that figure; ~30,000 req/s on 4-core NTS hardware is a conservative estimate.

Laravel Octane — Bare Route::get('/hello', fn() => [...]) with no sessions, DB, or auth middleware. A default laravel new skeleton measures ~354 req/s on the same runner.


Feature Highlights

  • Axum / Tokio HTTP Server — All TCP I/O runs inside a dedicated single-threaded Tokio runtime per worker, fully bypassing PHP's process model.
  • matchit Radix Trie Router — Routes are compiled into a native radix trie at boot; every request dispatches in O(log n) with zero PHP involvement.
  • Zero-Reflection Hot Path — Handler parameter maps are built once at boot via reflection and cached; the hot path does a single array lookup per argument.
  • Native DTO Validation — Schema checks run inside the Rust ValidatorRegistry before PHP is polled — invalid requests are rejected with a 400 without touching userland.
  • Multi-Worker via pcntl_fork — The TCP port is pre-bound once, then forked N times. Each worker owns an independent Rust heap with no shared state.
  • sonic-rs SIMD JSON — JSON compaction and encoding accelerated by sonic-rs across the FFI boundary.
  • OpenAPI 3.0 — Automatic Swagger UI generation directly from your route and DTO definitions.

Architecture

Quill enforces a hard boundary between the Boot Phase (reflection, compilation, registration) and the Hot Path (pure dispatch). The native core owns all I/O; PHP only runs your business logic.

Multi-Worker Model

Routes are compiled into a native manifest and the TCP port is pre-bound before pcntl_fork. Each worker independently re-initialises its Rust heap so there is zero shared state across processes.

flowchart TD
    A["routes.php"] -->|"$app->get / post / ..."| B["App::boot()"]
    B -->|"Router::compile()"| C["Route Manifest JSON"]
    B -->|"Validator::register()"| E["DTO Schema JSON"]

    C -->|"FFI → quill_router_build()"| RT[(matchit\nradix trie)]
    E -->|"FFI → quill_validator_register()"| VL[(ValidatorRegistry)]
    B -->|"FFI → quill_server_prebind(port)"| Sock[[Shared Socket fd]]

    Sock -.->|"dup(2) per worker"| W1 & W2 & WN

    subgraph W1 ["Worker 1 — parent process"]
        direction LR
        QC1["Quill Core\n(Axum / Tokio)"] <-->|"FFI bridge"| PH1[PHP Poll Loop]
    end
    subgraph W2 ["Worker 2 — pcntl_fork"]
        direction LR
        QC2["Quill Core\n(Axum / Tokio)"] <-->|"FFI bridge"| PH2[PHP Poll Loop]
    end
    subgraph WN ["Worker N — pcntl_fork"]
        direction LR
        QCN["Quill Core\n(Axum / Tokio)"] <-->|"FFI bridge"| PHN[PHP Poll Loop]
    end
Loading

Request Lifecycle

sequenceDiagram
    participant C  as Client
    participant QC as Quill Core (Axum / Tokio)
    participant RT as matchit Router
    participant VL as ValidatorRegistry
    participant PL as PHP Poll Loop
    participant RM as RouteMatch
    participant H  as Your Handler

    C->>+QC: HTTP Request

    QC->>RT: match_route(method, path)
    RT-->>QC: { handler_id, params, dto_class }

    opt dto_class present
        QC->>VL: validate(body_bytes)
        VL-->>QC: typed data  —or—  400 Bad Request
    end

    QC->>PL: quill_server_poll() → PendingRequest
    PL->>RM: RouteMatch::execute($request)
    RM->>RM: resolve args from param cache
    RM->>H: $handler(...$args)
    H-->>RM: array | HttpResponse
    RM-->>PL: result
    PL->>QC: quill_server_respond(id, json)

    QC->>QC: parse { status, headers, body }
    QC-->>-C: HTTP Response
Loading

Each worker's param cache is built once at boot via reflection and never touched again — zero reflection on the hot path.


Getting Started

1. Installation

composer create-project quillphp/quill my-api
cd my-api

2. Define Your Routes

// routes.php
use Handlers\User\ListUsersAction;
use Handlers\User\CreateUserAction;

/** @var \Quill\App $app */

// Simple closure — zero dependencies
$app->get('/hello', fn() => ['message' => 'hello', 'status' => 'ok']);

// Class-based handler — JIT-friendly, stable param-cache key
$app->get('/users',  [ListUsersAction::class, '__invoke']);
$app->post('/users', [CreateUserAction::class, '__invoke']); // auto-validates DTO

3. Serve

# Single worker
php -d ffi.enable=on bin/quill serve

# Multi-worker (recommended for production)
QUILL_WORKERS=4 php -d ffi.enable=on bin/quill serve --port=8080

In-Depth Guides

  • Architecture — Boot phase, hot path, and the FFI bridge in detail.
  • Routing — Verb mapping, groups, resource routes, and path parameters.
  • Validation — DTOs, PHP attributes, and native schema validation.
  • Benchmarks — Methodology, hardware specs, and full comparison results.

Contributing

We welcome contributions! Please see our Contributing Guide for local setup instructions.

License

QuillPHP is open-source software licensed under the MIT License.

About

Quill — High-performance PHP 8.3+ API framework. Boot once, serve forever.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors