Documentation

AETHER is a PHP 8.3 framework. It runs in persistent memory, uses Fibers, and doesn't rely on Composer. It's fast and small. Read this before opening an issue.

PropertyValue
PHP Version8.3+
Core SizeUnder 240KB
DependenciesNone
RoutingRadix Tree, O(K)
ConcurrencyPHP Fibers
Runtime ReflectionNone (AOT)

Installation

Just clone it. We don't use Composer here.

git clone https://github.com/kisalnelaka/aether.git myapp
cd myapp

The autoloader handles everything. You don't need `vendor/autoload.php`.

Quick Start

Create a file. Register a route. Run it. It's not complicated.

<?php
declare(strict_types: 1);
require_once __DIR__ . '/aether.php';

use Aether\Kernel\Application;

$app = Application::create(__DIR__);
$app->get('/', 'HomeController@index');
$app->run();

Start the built-in PHP server:

php -S localhost:8080 index.php

Architecture

Normal PHP is wasteful. It boots, runs, and dies. AETHER boots once and stays alive.

ModuleResponsibilityKey Classes
AlphaPersistent Execution EngineWorkerManager, Container, SharedMemoryCache
BetaRadix Tree RouterRadixTree, Router, RouteCompiler
GammaFiber Async I/OScheduler, WebSocketServer, JobQueue, ConnectionPool
DeltaZero-Reflection SchemaAOT\Compiler, AOT\HydratorGenerator, AOT\ValidationGenerator

Request Lifecycle

  1. Worker receives raw HTTP data
  2. Immutable Request object is created
  3. Router matches the path via the Radix Tree
  4. Route parameters are injected into the Request
  5. Global middleware pipeline executes
  6. Route-specific middleware executes
  7. Controller method is invoked
  8. Response is returned up the pipeline
  9. Response is sent to the client
  10. Ephemeral services are reset

Memory Model

ScopeLifetimeExamples
PersistentEntire worker processDB pool, config, compiled routes
EphemeralSingle requestRequest data, auth context
TransientSingle resolve callDTOs, value objects

Routing

Regex routers are slow. I built a radix tree. Matching is O(K) where K is segment count. It's fast.

Defining Routes

// Manual registration
$app->get('/users', 'UserController@index');
$app->post('/users', 'UserController@store');
$app->put('/users/{id}', 'UserController@update');
$app->delete('/users/{id}', 'UserController@destroy');

Attribute-Based Routing

#[Controller(prefix: '/api')]
final class UserController
{
    #[Get(path: '/users/{id}', name: 'users.show')]
    public function show(Request $r): Response { /* ... */ }
}

Available attributes: #[Get], #[Post], #[Put], #[Delete], #[Patch], #[Route].

Dynamic Parameters

Use {name} syntax. Access via $request->getRouteParam('name').

Wildcard Catch-All

Use {name*} to capture the remainder of the path:

$app->get('/files/{path*}', 'FileController@serve');
// /files/docs/readme.md  =>  path = "docs/readme.md"

Route Groups

$app->group('/admin', function ($r) {
    $r->get('/dashboard', 'AdminController@dash');
}, middleware: ['AuthMiddleware']);

Named Routes and URL Generation

$url = $app->getRouter()->url('users.show', ['id' => '42']);
// Result: "/users/42"

Matching Priority

  1. Static segments (exact match) -- highest priority
  2. Dynamic parameters ({id})
  3. Wildcard catch-all ({path*}) -- lowest priority

Dependency Injection Container

The standard PHP container model is broken for long running apps. This container forces you to use scopes so you don't leak memory.

Registration

// Persistent: created once, survives across requests
$container->persistent(DbPool::class, fn() => new DbPool());

// Ephemeral: recreated per request
$container->ephemeral(AuthContext::class, fn($c) => new AuthContext($c->resolve(Request::class)));

// Transient: new instance every resolve
$container->transient(Validator::class);

// Instance: pre-built object
$container->instance(Config::class, new Config([...]));

// Alias: interface to concrete
$container->alias(LoggerInterface::class, FileLogger::class);

Resolution

$service = $container->resolve(UserService::class);

Circular Dependency Detection

If A needs B and B needs A, the container throws an error. Fix your architecture instead of relying on magic resolvers.

Ephemeral Reset

The Kernel calls $container->resetEphemerals() at the end of each request cycle, destroying all per-request services to prevent state leakage.

HTTP Layer

Request

The Request is immutable and built from raw data. Don't use superglobals.

$method = $request->getMethod();
$path   = $request->getPath();
$body   = $request->json();
$param  = $request->getRouteParam('id');
$header = $request->getHeader('Authorization');
$query  = $request->getQuery('page', '1');

Response

Response::json(['data' => $items]);
Response::json(['error' => 'Not found'], 404);
Response::html('<h1>Hello</h1>');
Response::redirect('/login');
Response::noContent();

Middleware

Middleware implements the MiddlewareInterface with a single handle method:

final class CorsMiddleware implements MiddlewareInterface
{
    public function handle(Request $req, callable $next): Response
    {
        $response = $next($req);
        return $response
            ->withHeader('Access-Control-Allow-Origin', '*');
    }
}

Register globally or per-route:

$app->middleware(CorsMiddleware::class); // global

Fibers

We use PHP 8.1 Fibers. They let us do non-blocking I/O without the callback hell.

Scheduler

$scheduler = new Scheduler();
$deferred = $scheduler->defer(function () {
    return "Result from fiber";
});
$scheduler->run();
$result = $deferred->await();

Async Database

$db = new AsyncDatabase($scheduler, [
    'driver' => 'mysql',
    'host' => '127.0.0.1',
    'database' => 'myapp',
    'username' => 'root',
]);

$users = $db->query('SELECT * FROM users WHERE active = ?', [1]);

Async HTTP Client

$http = new AsyncHttpClient($scheduler);
$resp = $http->get('https://api.example.com/data');
$resp = $http->postJson('https://api.example.com/hook', ['event' => 'test']);

AOT Compiler

Reflection at runtime is stupid. The AOT compiler scans your code during build and dumps plain PHP arrays.

What Gets Compiled

InputOutput
#[Route] attributescompiled_routes.php (Radix Tree array)
#[Inject] attributescompiled_hydrators.php (static factories)
#[Validate] attributescompiled_validators.php (flat if-statements)
#[Listener] attributescompiled_events.php (dispatch maps)
#[Entity] attributescompiled_entities.php (ORM hydrators)
#[Command] attributescompiled_commands.php (CLI map)
All PHP classescompiled_classmap.php (autoloader map)

Usage

php bin/aether aot:compile

Configure scan paths in config/aether.php:

'controllers' => [
    'App\\Controllers' => __DIR__ . '/../app/Controllers',
],
'services' => [
    'App\\Services' => __DIR__ . '/../app/Services',
],

Worker Manager

Workers boot the framework once and handle thousands of requests. Don't use `php -S` in production.

Configuration

SettingDefaultDescription
workers4Number of worker processes
worker_modestdioProtocol: stdio, roadrunner, socket
max_requests10000Requests before worker recycle

Signals

  • SIGTERM / SIGINT: Graceful shutdown
  • SIGUSR2: Hot-reload (kill and respawn all workers)

Shared Memory Cache

Workers share data via shmop. Don't hit the disk for everything.

$cache = new SharedMemoryCache(key: 0x4145, size: 1_048_576);
$cache->set('users:count', 42, ttl: 300);
$count = $cache->get('users:count');

CLI

It's a CLI tool. Use it.

CommandDescription
php bin/aether versionShow framework version and PHP info
php bin/aether routes:compileCompile route tree from attributes
php bin/aether aot:compileFull AOT compilation pipeline
php bin/aether aot:clearClear all compiled cache files
php bin/aether routes:listList all registered routes
php bin/aether container:diagContainer diagnostics
php bin/aether serveStart persistent worker server