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.
| Property | Value |
|---|---|
| PHP Version | 8.3+ |
| Core Size | Under 240KB |
| Dependencies | None |
| Routing | Radix Tree, O(K) |
| Concurrency | PHP Fibers |
| Runtime Reflection | None (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.
| Module | Responsibility | Key Classes |
|---|---|---|
| Alpha | Persistent Execution Engine | WorkerManager, Container, SharedMemoryCache |
| Beta | Radix Tree Router | RadixTree, Router, RouteCompiler |
| Gamma | Fiber Async I/O | Scheduler, WebSocketServer, JobQueue, ConnectionPool |
| Delta | Zero-Reflection Schema | AOT\Compiler, AOT\HydratorGenerator, AOT\ValidationGenerator |
Request Lifecycle
- Worker receives raw HTTP data
- Immutable Request object is created
- Router matches the path via the Radix Tree
- Route parameters are injected into the Request
- Global middleware pipeline executes
- Route-specific middleware executes
- Controller method is invoked
- Response is returned up the pipeline
- Response is sent to the client
- Ephemeral services are reset
Memory Model
| Scope | Lifetime | Examples |
|---|---|---|
| Persistent | Entire worker process | DB pool, config, compiled routes |
| Ephemeral | Single request | Request data, auth context |
| Transient | Single resolve call | DTOs, 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
- Static segments (exact match) -- highest priority
- Dynamic parameters (
{id}) - 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
| Input | Output |
|---|---|
| #[Route] attributes | compiled_routes.php (Radix Tree array) |
| #[Inject] attributes | compiled_hydrators.php (static factories) |
| #[Validate] attributes | compiled_validators.php (flat if-statements) |
| #[Listener] attributes | compiled_events.php (dispatch maps) |
| #[Entity] attributes | compiled_entities.php (ORM hydrators) |
| #[Command] attributes | compiled_commands.php (CLI map) |
| All PHP classes | compiled_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
| Setting | Default | Description |
|---|---|---|
| workers | 4 | Number of worker processes |
| worker_mode | stdio | Protocol: stdio, roadrunner, socket |
| max_requests | 10000 | Requests before worker recycle |
Signals
SIGTERM/SIGINT: Graceful shutdownSIGUSR2: 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.
| Command | Description |
|---|---|
php bin/aether version | Show framework version and PHP info |
php bin/aether routes:compile | Compile route tree from attributes |
php bin/aether aot:compile | Full AOT compilation pipeline |
php bin/aether aot:clear | Clear all compiled cache files |
php bin/aether routes:list | List all registered routes |
php bin/aether container:diag | Container diagnostics |
php bin/aether serve | Start persistent worker server |