Introduction
Aether is a PHP 8.3+ framework built from scratch with one principle: you should understand everything that runs in your application.
No magic containers. No reflection-based dependency injection. No hidden conventions that silently alter your code's behavior. Every layer of Aether is readable, typed, and explicit.
It is not designed to compete with Laravel or Symfony. It is designed for developers who want a structured, auditable foundation - lightweight enough to understand fully, solid enough to build real products on.
Core principles
Explicit over implicit
Routes, middleware, auth - all must be declared. Nothing is registered automatically or inferred from naming conventions.
Minimal footprint
Under 1MB of source. No vendor directory at runtime. Every class in the framework is there for a reason.
Security by structure
HMAC-signed sessions, strict deserialization, opt-in middleware. Security is enforced at the architecture level, not patched later.
OOP-first
Interfaces, typed properties, enums, readonly classes. Aether is built for PHP 8.3+, not retrofitted to support older versions.
Installation
Clone the repository and point your server to the project root. No build step. No package manager.
Requirements
Clone & setup
git clone https://github.com/Aether-PHP/Aether-PHP
cd Aether-PHP
cp .env.example .env
Apache configuration
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
Nginx configuration
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Quick Start
A working application in four steps.
Create a controller
php bin/aether make:controller HomeController
Define a route via annotation
class HomeController extends Controller {
/**
* [@method] => GET
* [@route] => /
*/
public function index() {
$this->_render('home', [
'title' => 'Welcome to Aether'
]);
}
}
Create the view
<h1><?= $title ?></h1>
<p>Aether is running.</p>
Point your browser to /
Your route is live. No registration needed - ControllerGateway scans and loads annotations automatically at boot.
Configuration
All configuration lives in .env. No PHP config arrays. No XML. No YAML.
# Application
APP_ENV=production
APP_KEY=your-secret-key-here
# Database
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=aether_db
DB_USER=root
DB_PASS=
# Session
SESSION_LIFETIME=86400
.env file. It is already included in .gitignore by default.
Routing
Routes are defined via docblock annotations directly on controller methods. No route files, no arrays, no registration calls.
How it works
ControllerGateway reflects over all controllers in app/App/Controllers/, extracts [@route] and [@method] annotations, and registers them at boot time.
Basic route
/**
* [@method] => GET
* [@route] => /hello
*/
public function hello() {
$this->_render('hello');
}
Supported HTTP methods
REST API route
/**
* [@method] => GET
* [@route] => /api/users
*/
public function users_fetch() {
$response = ResponseFactory::_create(
HttpResponseFormatEnum::JSON,
['users' => $this->getUsers()],
200
);
$response->_send();
}
Aether::_init(). There is no caching mechanism - the overhead is minimal given the small controller surface area that Aether is designed for.
Controllers
All controllers extend Controller and live in app/App/Controllers/.
Rendering a view
public function dashboard() {
$this->_render('dashboard', [
'user' => SessionInstance::_getUser(),
'title' => 'Dashboard'
]);
}
Available base methods
_render(string $view, array $data = [])
Renders a PHP view template from app/App/Views/ with the given data.
_redirect(string $url)
Issues a 302 redirect and terminates execution.
_abort(int $code)
Sends an HTTP error response and terminates.
Middleware
Middleware is opt-in, composable, and runs sequentially through a single pipeline before the router dispatches.
Built-in middleware
CsrfMiddleware
Validates CSRF tokens on POST, PUT, and DELETE requests. Tokens are generated automatically and injected into forms.
RateLimitMiddleware
Limits requests per IP per time window. Configurable via .env.
AuthMiddleware
Checks session validity and redirects unauthenticated users.
Registering middleware
Aether::_init([
CsrfMiddleware::class,
RateLimitMiddleware::class,
AuthMiddleware::class,
]);
Writing custom middleware
class LogMiddleware implements AetherMiddleware {
public function handle(Request $request, callable $next): void {
// Before dispatch
error_log('→ ' . $request->getPath());
$next($request);
// After dispatch
}
}
Requests
Incoming HTTP data is handled via HttpParameterUnpacker. Outgoing HTTP calls use RequestFactory.
Reading input
$unpacker = new HttpParameterUnpacker();
// From POST body or query string
$email = $unpacker->_getAttribute('email');
$password = $unpacker->_getAttribute('password');
Making outbound HTTP requests
$request = RequestFactory::_create(
HttpMethodEnum::GET,
'https://api.example.com/data'
);
$response = $request->_send();
var_dump($response);
Responses
ResponseFactory creates typed HTTP responses. Supported formats: JSON, XML, TEXT.
JSON response
$response = ResponseFactory::_create(
HttpResponseFormatEnum::JSON,
['status' => 'ok', 'data' => $payload],
200
);
$response->_send(); // sends headers + body, terminates
Response formats
HttpResponseFormatEnum::JSON
Sets Content-Type: application/json and encodes the data array.
HttpResponseFormatEnum::XML
Sets Content-Type: application/xml and converts the array to XML.
HttpResponseFormatEnum::TEXT
Sets Content-Type: text/plain.
Authentication
Auth is built around the gateway pattern. Three gateways handle login, registration, and logout - each wrapping the full logic for that operation.
Login
$gateway = new LoginAuthGateway($username, $password);
if ($gateway->_tryAuth()) {
// Session is set - user is authenticated
$this->_redirect('/dashboard');
} else {
$error = $gateway->_getStatus();
$this->_render('login', ['error' => $error]);
}
Registration
$gateway = new RegisterAuthGateway(
$username, $email, $password
);
if ($gateway->_register()) {
$this->_redirect('/login');
}
Logout
LogoutAuthGateway::_logout();
$this->_redirect('/');
Sessions
SessionInstance centralizes all $_SESSION access. Session payloads are signed with HMAC to detect tampering.
Reading session data
// Get the current user object
$user = SessionInstance::_getUser();
// Get an arbitrary session value
$value = SessionInstance::_get('cart_id');
// Set a value
SessionInstance::_set('cart_id', $cartId);
$_SESSION. Always use SessionInstance - direct writes bypass the HMAC signing mechanism.
Authorization
Aether does not provide an automatic authorization system. Authorization is the developer's explicit responsibility.
Checking login state
if (!UserInstance::_isLoggedIn()) {
$this->_redirect('/login');
}
$user = SessionInstance::_getUser();
if ($user->_isAdmin()) {
// Admin-only logic
}
if ($user->_hasPerm('edit_posts')) {
// Permission-gated logic
}
Available UserInstance methods
UserInstance::_isLoggedIn(): bool
Returns true if there is a valid authenticated session.
$user->_isAdmin(): bool
Returns true if the user has the admin flag set.
$user->_hasPerm(string $perm): bool
Checks against the user's JSON-encoded permission array.
Database
DatabaseWrapper provides a thin, type-safe abstraction over PDO. Prepared statements are used everywhere - SQL injection is structurally not possible through the standard API.
Connecting
$db = new DatabaseWrapper(
'my_database',
DatabaseDriverEnum::MYSQL
);
CRUD operations
// Select with conditions
$users = $db->_select('users', '*', ['active' => 1]);
// Insert a row
$db->_insert('users', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => password_hash($pass, PASSWORD_ARGON2ID)
]);
// Update
$db->_update('users',
['active' => 0],
['id' => $userId]
);
// Delete
$db->_delete('users', ['id' => $userId]);
Supported drivers
Views
Views are plain PHP templates. No templating engine. No compilation step. No {{ }} syntax to learn.
Rendering from a controller
$this->_render('dashboard', [
'title' => 'Dashboard',
'items' => $items
]);
Rendering standalone
ViewInstance::_make('dashboard', [
'title' => 'Dashboard'
]);
Template file
<!DOCTYPE html>
<html>
<head>
<title><?= htmlspecialchars($title) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($title) ?></h1>
</body>
</html>
Modules
Modules are optional extensions loaded at boot. Each module implements AetherModule and receives a _onLoad() hook.
Loading modules
ModuleFactory::_load([
MyAnalyticsModule::class,
CustomApiModule::class,
]);
Writing a module
class MyAnalyticsModule implements AetherModule {
public function _onLoad(): void {
// Runs once at framework boot
Analytics::track($_SERVER['REQUEST_URI']);
}
}
CLI Tools
Aether ships with a minimal CLI via php bin/aether. No Node.js, no npm, no build tooling.
Available commands
make:controller <Name>
Generates a new controller class in app/App/Controllers/
setup
Runs the initial setup wizard - DB, .env, directory scaffold
source:script <path>
Executes a PHP script in the Aether bootstrap context
Examples
# Generate a controller
php bin/aether make:controller ProductController
# Run initial setup
php bin/aether setup
# Run a migration script
php bin/aether source:script scripts/migrate.php
Security Model
Security in Aether is structural. Every protection mechanism is explicit, composable, and enforced at the architecture level - not added as an afterthought.
HMAC-signed sessions
All session payloads carry an HMAC signature. Any tampered session is rejected before reaching application code. Never bypass SessionInstance.
Strict deserialization
Only explicitly whitelisted classes can be reconstructed from session data. PHP object injection is structurally prevented.
CSRF protection
CsrfMiddleware validates tokens on all mutating requests. Token generation and validation is automatic when the middleware is registered.
Prepared statements
The DatabaseWrapper API uses PDO prepared statements exclusively. SQL injection through the standard data layer is not possible.
Argon2ID passwords
Registration and password changes use password_hash($pass, PASSWORD_ARGON2ID). Do not pre-hash passwords before passing to auth gateways.
Explicit authorization
There is no automatic role system. Every permission check is a line of code you write. Authorization cannot be silently bypassed by a framework assumption.