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.

Design philosophy: Aether exposes complexity safely rather than hiding it. Every decision you make is traceable back to your code - not the framework's internals.

Core principles

01

Explicit over implicit

Routes, middleware, auth - all must be declared. Nothing is registered automatically or inferred from naming conventions.

02

Minimal footprint

Under 1MB of source. No vendor directory at runtime. Every class in the framework is there for a reason.

03

Security by structure

HMAC-signed sessions, strict deserialization, opt-in middleware. Security is enforced at the architecture level, not patched later.

04

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

RequirementVersionNotes
PHP8.3+Enums, typed properties required
PDO extensionAnyFor database features
URL rewriting-Apache mod_rewrite or Nginx

Clone & setup

Terminal
git clone https://github.com/Aether-PHP/Aether-PHP
        cd Aether-PHP
        cp .env.example .env

Apache configuration

.htaccess
RewriteEngine On
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule ^ index.php [QSA,L]

Nginx configuration

nginx.conf
location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

Quick Start

A working application in four steps.

01

Create a controller

Terminal
php bin/aether make:controller HomeController
02

Define a route via annotation

app/App/Controllers/HomeController.php
class HomeController extends Controller {

            /**
             * [@method] => GET
             * [@route]  => /
             */
            public function index() {
                $this->_render('home', [
                    'title' => 'Welcome to Aether'
                ]);
            }
        }
03

Create the view

app/App/Views/home.php

        <h1><?= $title ?></h1>
        <p>Aether is running.</p>
        
04

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.

.env
# 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
Never commit your .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

Controller annotation
/**
         * [@method] => GET
         * [@route]  => /hello
         */
        public function hello() {
            $this->_render('hello');
        }

Supported HTTP methods

GET POST PUT DELETE PATCH

REST API route

JSON endpoint
/**
         * [@method] => GET
         * [@route]  => /api/users
         */
        public function users_fetch() {
            $response = ResponseFactory::_create(
                HttpResponseFormatEnum::JSON,
                ['users' => $this->getUsers()],
                200
            );
            $response->_send();
        }
Route scanning happens once at boot inside 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

Using _render()
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

index.php - middleware pipeline
Aether::_init([
            CsrfMiddleware::class,
            RateLimitMiddleware::class,
            AuthMiddleware::class,
        ]);

Writing custom middleware

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

POST / GET input
$unpacker = new HttpParameterUnpacker();

        // From POST body or query string
        $email    = $unpacker->_getAttribute('email');
        $password = $unpacker->_getAttribute('password');

Making outbound HTTP requests

External API call
$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

ResponseFactory
$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

LoginAuthGateway
$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

RegisterAuthGateway
$gateway = new RegisterAuthGateway(
            $username, $email, $password
        );

        if ($gateway->_register()) {
            $this->_redirect('/login');
        }
Passwords are hashed with Argon2ID automatically. Do not hash before passing to the gateway.

Logout

LogoutAuthGateway
LogoutAuthGateway::_logout();
        $this->_redirect('/');

Sessions

SessionInstance centralizes all $_SESSION access. Session payloads are signed with HMAC to detect tampering.

Reading session data

SessionInstance
// 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);
Never write directly to $_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

UserInstance checks
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

DatabaseWrapper
$db = new DatabaseWrapper(
            'my_database',
            DatabaseDriverEnum::MYSQL
        );

CRUD operations

Select / Insert / Update / Delete
// 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

MySQL SQLite

Views

Views are plain PHP templates. No templating engine. No compilation step. No {{ }} syntax to learn.

Rendering from a controller

_render()
$this->_render('dashboard', [
            'title' => 'Dashboard',
            'items' => $items
        ]);

Rendering standalone

ViewInstance
ViewInstance::_make('dashboard', [
            'title' => 'Dashboard'
        ]);

Template file

app/App/Views/dashboard.php
<!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
ModuleFactory::_load([
            MyAnalyticsModule::class,
            CustomApiModule::class,
        ]);

Writing a module

Custom 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

CommandDescription
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

Terminal
# 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.