Introduction

Welcome to Aether-PHP, a lightweight, zero-dependency PHP framework built from scratch. This documentation will guide you through using the framework from basic to advanced levels.

Aether-PHP is designed with simplicity and clarity in mind. Every component is readable, typed, and explicit. No magic containers. No reflection-based dependency injection. No hidden conventions that silently alter your code's behavior.

Built for PHP 8.3+, Aether leverages modern language features like enums, typed properties, and readonly classes to provide a solid foundation for building real applications.

Requirements

Before you begin, ensure you have the following installed:

RequirementVersionNotes
PHP8.3+Enums, typed properties required
Web ServerAnyApache, Nginx, or PHP built-in
DatabaseOptionalMySQL or SQLite for database features
APCU extensionOptionalFor caching features

Installation

To get started with Aether-PHP:

01

Clone or download the framework

Terminal
git clone https://github.com/Aether-PHP/Aether-PHP
cd Aether-PHP
02

Point your web server to the public directory

Configure your web server's document root to point to the public directory.

03

Configure your .env file

Terminal
cp .env.example .env
04

Access your application

Visit your application through the web server. The entry point is /index.php which automatically loads everything.

The framework uses a custom autoloader, so no Composer is needed. No additional setup required!

Environment Variables

Aether-PHP uses a .env file for configuration. Create it in the project root and use the following example as a base:

.env
# Project
PROJECT_NAME=MyApp

# Database (MySQL)
DATABASE_ADDRESS=127.0.0.1
DATABASE_USERNAME=root
DATABASE_PASSWORD=root

# Authentication
AUTH_DATABASE_GATEWAY=aetherphp
AUTH_TABLE_GATEWAY=users

# Sessions and cookies
COOKIE_SESSION_TTL=10
SESSION_FOLDER_PATH=../storage/sessions
SESSION_HMAC=your_secret_key_at_least_32_characters_long

# Ratelimit (RatelimitMiddleware)
RATELIMIT_SECOND_INTERVAL=60
RATELIMIT_MAX_LIMIT=100

# Maintenance (MaintenanceMiddleware)
MAINTENANCE=false

SESSION_HMAC is used to sign all session data (HMAC). Use a long, random string for security. You can generate one with:

Terminal
openssl rand -hex 32
SESSION_HMAC and your .env file should never be committed in production.

Accessing Configuration

You can access configuration values anywhere in your application:

Accessing config values
$dbHost = Aether()->_config()->_get('DATABASE_ADDRESS');
$appName = Aether()->_config()->_get('PROJECT_NAME');

# Same as:
$dbHost = $_ENV["DATABASE_ADDRESS"];
$appName = $_ENV["PROJECT_NAME"];

The _get() method returns the value from $_ENV array. If the key doesn't exist, it returns null.

Understanding Routes

Aether-PHP uses annotation-based routing. Routes are defined using PHP DocBlock comments in your controller classes. The framework automatically scans controllers and registers routes.

Controllers are located in:

  • app/App/Controller/ for web controllers
  • app/App/Controller/Api/ for API controllers

Creating Your First Route

Let's create a simple route. Create a file app/App/Controller/HomeController.php:

app/App/Controller/HomeController.php
<?php
namespace App\Controller;

use Aether\Router\Controller\Controller;

class HomeController extends Controller {
    /**
     * [@method] => GET
     * [@route]  => /
     */
    public function index() {
        echo "Hello, Aether-PHP!";
    }
}

This creates a GET route at the root URL (/). When someone visits your site, this method will be called.

The annotations work as follows:

  • [@method] => HTTP method (GET, POST, PUT, DELETE)
  • [@route] => URL path
Note: The square brackets and @ symbols are literal - include them exactly as shown. Routes are case-sensitive.

Route Parameters

You can capture URL parameters using curly braces:

Single parameter
/**
 * [@method] => GET
 * [@route]  => /user/{id}
 */
public function showUser($id) {
    echo "User ID: " . $id;
}

When someone visits /user/123, the $id parameter will contain '123'. Parameters are automatically sanitized to prevent XSS attacks.

Multiple parameters

Multiple parameters
/**
 * [@method] => GET
 * [@route]  => /user/{userId}/post/{postId}
 */
public function showPost($userId, $postId) {
    # Parameters are passed in the order they appear in the route
    echo "User: $userId, Post: $postId";
}
Route parameters only work with GET requests. For POST/PUT/DELETE, use request body data instead.

Base Paths

For API controllers, you often want a base path prefix. Use the [@base] annotation on the class:

Using base paths
/**
 * [@base] => /api/v1
 */
class ApiController extends Controller {
    /**
     * [@method] => GET
     * [@route]  => /users
     */
    public function getUsers() {
        # This route becomes: GET /api/v1/users
    }
}

The base path is used for all routes in that controller.

Use base paths to version your APIs. You can have /api/v1 and /api/v2 controllers separately.

Controller Structure

All controllers must extend Aether\Router\Controller\Controller. The base Controller class provides helper methods for rendering views and accessing services.

Your controller methods can return:

  • Nothing (void) - output is sent directly
  • HTTP Response objects (for APIs)
  • View rendering (for web pages)

JSON Responses

For API endpoints, return JSON responses:

Returning JSON
/**
 * [@method] => GET
 * [@route]  => /api/users
 */
public function getUsers() {
    $users = [
        ['id' => 1, 'name' => 'John'],
        ['id' => 2, 'name' => 'Jane']
    ];

    return Aether()->_http()->_response()->_json($users, 200)->_send();
}

The _json() method takes two parameters:

  • First: the data to encode (array or object)
  • Second: HTTP status code (200, 201, 404, etc.)

Always call ->_send() at the end to output the response.

Common status codes

200 - OK 201 - Created 400 - Bad Request 404 - Not Found 500 - Internal Server Error

Other Response Formats

Aether-PHP supports multiple response formats:

Different response types
# HTML response
Aether()->_http()->_response()->_html('<h1>Hello</h1>', 200)->_send();

# XML response
Aether()->_http()->_response()->_xml('<root><item>data</item></root>', 200)->_send();

# Plain text response
Aether()->_http()->_response()->_text('Hello World', 200)->_send();

# PDF response
Aether()->_http()->_response()->_pdf($pdfContent, 200)->_send();
The framework automatically sets appropriate Content-Type headers for each format.

Rendering Views

For web pages, render views using the _render() method:

Controller rendering a view
/**
 * [@method] => GET
 * [@route]  => /
 */
public function home() {
    $this->_render('home', [
        'title' => 'Welcome',
        'user' => ['name' => 'John']
    ]);
}

The first parameter is the view name (without .php extension). The second parameter is an array of variables to pass to the view. Views are located in public/views/ directory.

View file

public/views/home.php
<h1><?php echo $title; ?></h1>
<p>Hello, <?php echo $user['name']; ?>!</p>
Views use PHP's extract() function, so array keys become variable names.

Connecting to Database

Aether-PHP supports MySQL and SQLite databases. Access the database through the ServiceManager:

Database connections
# MySQL connection
$db = Aether()->_db()->_mysql('database_name');

# SQLite connection
$db = Aether()->_db()->_sqlite('path/to/database.db');

The database name for MySQL should match your database name. For SQLite, provide the full path to the .db file.

Connections are cached, so calling _mysql() multiple times with the same database name returns the same connection.

Basic SELECT Queries

Use the QueryBuilder for type-safe database queries:

SELECT query
$users = $db->_table('users')
    ->_select('*')
    ->_send();

This executes: SELECT * FROM users. The _send() method executes the query and returns results.

Select specific columns

Specific columns
$users = $db->_table('users')
    ->_select('id', 'name', 'email')
    ->_send();

# Or pass an array:
$users = $db->_table('users')
    ->_select(['id', 'name', 'email'])
    ->_send();

WHERE Clauses

Add conditions using _where():

Single WHERE clause
$user = $db->_table('users')
    ->_select('*')
    ->_where('id', 1)
    ->_send();

This executes: SELECT * FROM users WHERE id = 1. Results are returned as an array of associative arrays.

Multiple WHERE clauses

Chaining WHERE clauses
$users = $db->_table('users')
    ->_select('*')
    ->_where('status', 'active')
    ->_where('role', 'admin')
    ->_send();

This executes: SELECT * FROM users WHERE status = 'active' AND role = 'admin'

All values are automatically parameterized to prevent SQL injection. In reality, such query would execute: SELECT * FROM `users` WHERE status = :status AND role = :role, with all key/value pairs inputted in prepared statements.

INSERT Queries

Insert data using _insert():

INSERT query
$db->_table('users')
    ->_insert('name', 'John Doe')
    ->_insert('email', 'john@example.com')
    ->_insert('password', 'hashed_password')
    ->_send();

This executes: INSERT INTO users (name, email, password) VALUES (:name, :email, :password). Chain multiple _insert() calls to add multiple columns.

The framework uses prepared statements, so your data is safe from SQL injection.

UPDATE Queries

Update records using _update() and _set():

UPDATE query
$db->_table('users')
    ->_update()
    ->_set('name', 'Jane Doe')
    ->_set('email', 'jane@example.com')
    ->_where('id', 1)
    ->_send();

This executes: UPDATE users SET name = :set_name, email = :set_email WHERE id = :id

Always include a _where() clause to avoid updating all rows!

DELETE Queries

Delete records using _delete():

DELETE query
$db->_table('users')
    ->_delete()
    ->_where('id', 1)
    ->_send();

This executes: DELETE FROM users WHERE id = :id

Always include a _where() clause! Forgetting it will delete ALL rows in the table.

Checking Existence

Check if a record exists using _exist():

Existence check
$exists = $db->_table('users')
    ->_exist()
    ->_where('email', 'test@example.com')
    ->_send();

This returns true if the record exists, false otherwise. Useful for validation before inserting new records.

JOIN Queries

Perform JOINs using _join():

JOIN query
$results = $db->_table('users')
    ->_select('users.name', 'posts.title')
    ->_join('posts', 'users.id = posts.user_id')
    ->_send();

This executes: SELECT users.name, posts.title FROM users INNER JOIN posts ON users.id = posts.user_id

You can chain multiple _join() calls for multiple JOINs.

Raw SQL Queries

For complex queries, use _raw():

Raw SQL
$results = $db->_raw('SELECT COUNT(*) as total FROM users WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 MONTH)');
Use _raw() sparingly and only when the QueryBuilder can't handle your query. Always validate and sanitize any user input used in raw queries.

User Registration

Aether-PHP provides built-in authentication. To register a new user:

User registration
use Aether\Auth\Gateway\RegisterAuthGateway;

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

if ($gateway->_tryAuth()) {
    # Registration successful
    echo $gateway->_getStatus(); # "user successfully signed up."
    # User is automatically logged in
} else {
    # Registration failed
    echo $gateway->_getStatus(); # "provided email is already used."
}

The RegisterAuthGateway:

  • Checks if the email is already in use
  • Hashes the password using Argon2ID
  • Inserts the user into the database
  • Automatically logs the user in, in a safe session environment

Make sure your database has a 'users' table with columns:

  • uid (primary key)
  • username
  • email
  • password_hash (or password)
  • perms (JSON array of permissions)

User Login

To log in a user:

User login
use Aether\Auth\Gateway\LoginAuthGateway;

$gateway = new LoginAuthGateway($email, $password);

if ($gateway->_tryAuth()) {
    # Login successful
    echo $gateway->_getStatus(); # "user successfully logged in."
} else {
    # Login failed
    echo $gateway->_getStatus(); # "user login failed."
}

The LoginAuthGateway:

  • Checks if the email exists
  • Verifies the password hash
  • Creates a UserInstance and safely stores it in the session environment

Checking Authentication Status

Check if a user is logged in:

Checking login status
use Aether\Auth\User\UserFactory;

if (UserFactory::_isLoggedIn()) {
    # User is logged in
} else {
    # User is not logged in
}

Or use the ServiceManager (much appreciated):

Using ServiceManager
if (Aether()->_session()->_auth()->_isLoggedIn()) {
    # User is logged in
}

Getting Current User

Retrieve the current logged-in user:

Getting user
use Aether\Auth\User\UserFactory;

$user = UserFactory::_fromSession();

if ($user) {
    echo $user->_getUid();
    echo $user->_getUsername();
    echo $user->_getEmail();
}

Or use the ServiceManager (much appreciated):

Using ServiceManager
$user = Aether()->_session()->_auth()->_getUser();

if ($user) {
    echo $user->_getUid();
    echo $user->_getUsername();
    echo $user->_getEmail();
}

User Permissions

Check user permissions:

Permission checks
if ($user->_hasPerm('PERM.ADMIN')) {
    # User has admin permission, same as $user->_isAdmin()
}

# Add a permission:
$user->_addPerm('PERM.ADMIN');
$user->_update(); # Save to database and session

# Remove a permission:
$user->_removePerm('PERM.ADMIN');
$user->_update();

# Check if user is admin:
if ($user->_isAdmin()) {
    # User is admin
}

# Get all permissions:
$permissions = $user->_getPerms(); # Returns array

User Logout

To log out a user:

Logout
use Aether\Auth\Gateway\LogoutAuthGateway;

$gateway = new LogoutAuthGateway();

if ($gateway->_tryAuth()) {
    # Logout successful
    echo $gateway->_getStatus();
}

This removes the user from the session.

Session Basics

Aether-PHP provides secure session management. Sessions are automatically started when the framework initializes.

Working with sessions
# Store values in session:
Aether()->_session()->_get()->_setValue('key', 'value');

# Retrieve values:
$value = Aether()->_session()->_get()->_getValue('key');

# Check if a key exists:
if (Aether()->_session()->_get()->_valueExist('key')) {
    # Key exists
}

# Remove a value:
Aether()->_session()->_get()->_removeValue('key');

Session Security

Aether-PHP automatically:

  • Signs session data with HMAC
  • Encodes data in base64
  • Validates data integrity on retrieval
Session security features
# Get session ID:
$sessionId = Aether()->_session()->_get()->_getSessId();

# Regenerate session ID (useful after login):
Aether()->_session()->_get()->_regenerateId();
Regenerate session ID after authentication to prevent session fixation attacks.

Reading POST/PUT Data

To read data from POST or PUT requests:

Reading request data
use Aether\Http\HttpParameterTypeEnum;

# JSON body (php://input)
$params = Aether()->_http()->_parameters(HttpParameterTypeEnum::PHP_INPUT);

$email = $params->_getAttribute('email');
$password = $params->_getAttribute('password');

The HttpParameterUnpacker (accessible through the HTTP service) automatically handles:

  • JSON request bodies
  • Form-data
  • URL-encoded data

Check if attribute exists

Validation
$email = $params->_getAttribute('email');
if ($email === false) {
    # Attribute doesn't exist
}

Making HTTP Requests

Make outgoing HTTP requests:

Outbound HTTP request
use Aether\Http\Methods\HttpMethodEnum;

$request = Aether()->_http()->_request(
    HttpMethodEnum::GET,
    'https://api.example.com/users'
);

$response = $request->_send();

Supported methods: GET, POST, PUT, DELETE. _send() returns a HttpResponse instance that exposes the HTTP status code, headers and body.

This is useful for calling external APIs or webhooks.

Understanding Middleware

Middleware runs before your controller methods. Aether-PHP includes several built-in middlewares:

  • MaintenanceMiddleware: Enables global maintenance mode when MAINTENANCE=true
  • RatelimitMiddleware: Limits requests per IP (by default 100 requests per 60 seconds, configurable in .env)
  • CsrfMiddleware: Protects against CSRF attacks
  • SecurityHeadersMiddleware: Adds security headers to responses

Middlewares are registered in app/App/App.php:

Middleware registration
private static $_middlewares = [
    MaintenanceMiddleware::class,
    RatelimitMiddleware::class,
    CsrfMiddleware::class,
    SecurityHeadersMiddleware::class
];

The order matters - middlewares execute in the order they're listed.

CSRF Protection

CSRF protection is automatic. For GET/HEAD/OPTIONS requests, the CSRF token is exposed in the X-CSRF-Token header.

In JavaScript (fetch API)

JavaScript example
// Get token from header (set automatically on GET requests)
const token = response.headers.get('X-CSRF-Token');

// Include in POST request
fetch('/api/users', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': token,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: 'John' })
});

In HTML forms

HTML form
<form method="POST">
    <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
    <!-- form fields -->
</form>

The middleware automatically verifies the token and returns 403 if invalid.

Creating Custom Middleware

Create your own middleware by implementing MiddlewareInterface:

Custom middleware
use Aether\Middleware\MiddlewareInterface;

class LoggingMiddleware implements MiddlewareInterface {

    public function _handle(callable $_next) {
        // Code before controller execution
        error_log('Request: ' . $_SERVER['REQUEST_URI']);

        $_next(); // Call next middleware or controller

        // Code after controller execution
        error_log('Response sent');
    }
}

Register it in app/App/App.php:

Registration
private static $_middlewares = [
    LoggingMiddleware::class,
    RatelimitMiddleware::class,
    // ... other middlewares
];
Use middleware for cross-cutting concerns like logging, authentication checks, or data transformation.

Route Middlewares

In addition to global middlewares defined in app/App/App.php, you can attach middlewares to a specific route using annotations.

Using the [@middlewares] annotation

Per-route middlewares
/**
 * [@method]      => GET
 * [@route]       => /dashboard
 * [@middlewares] => AuthMiddleware
 */
public function index() {
    # Only executed if AuthMiddleware::_handle() calls $_next()
}

You can chain several middlewares by separating them with commas:

Multiple middlewares
/**
 * [@method]      => POST
 * [@route]       => /admin/users
 * [@middlewares] => AuthMiddleware,AdminOnlyMiddleware
 */
public function storeUser() {
    # Executed only if both middlewares allow it to pass
}
Names in [@middlewares] are automatically resolved to Aether\Middleware\Stack\YourMiddleware. If the class does not exist, it is ignored.

Using APCU Cache

Aether-PHP supports APCU caching. Access the cache:

APCU operations
$cache = Aether()->_cache()->_apcu();

# Store a value:
$cache->_set('key', 'value', 3600); # TTL in seconds (1 hour)

# Retrieve a value:
$value = $cache->_get('key', 'default'); # Returns 'default' if key doesn't exist

# Check if key exists:
if ($cache->_has('key')) {
    # Key exists
}

# Delete a key:
$cache->_delete('key');

# Clear all cache:
$cache->_clear();

Cache Patterns

Common cache pattern - cache-aside:

Cache-aside pattern
$cache = Aether()->_cache()->_apcu();
$key = 'users_list';

# Try to get from cache
$users = $cache->_get($key);

if ($users === null) {
    # Not in cache, fetch from database
    $users = $db->_table('users')->_select('*')->_send();

    # Store in cache for 5 minutes
    $cache->_set($key, $users, 300);
}

# Use $users
Cache expensive operations like database queries or API calls.

HTTP Analytics Module

The Analytics module automatically logs every HTTP request into a dedicated SQLite database, giving you usage statistics without any external dependency.

Enabling the module

app/App/App.php
use Aether\Modules\Analytics\Analytics;

class App {

    /** @var array $_modules */
    private static array $_modules = [
        Analytics::class,
        // Add other modules here (I18n, CLI, etc.)
    ];

    public static function _init() : void {
        ModuleFactory::_load(self::$_modules);
        # ...
    }
}

Once enabled, the module creates (if needed) a SQLite file at ressources/db.sqlite and inserts one row per request.

Collected data

  • ip_addr: client IP address
  • user_agent: raw User-Agent string
  • protocol: http or https
  • domain: domain name (HTTP_HOST)
  • route: request path (without query string)
  • http_method: HTTP method (GET, POST, …)
  • http_data: GET params as JSON or raw request body
  • phptimestamp: PHP timestamp (seconds)

Reading statistics

Retrieve logs by IP
use Aether\Modules\Analytics\Provider\LogProvider;

$provider = new LogProvider('path/to/ressources/db.sqlite');

$entries = $provider->_retrieve('127.0.0.1');

# $entries contains an array of rows from the analytics table
The Analytics module relies on the internal QueryBuilder and can live alongside your own SQLite or MySQL connections without extra configuration.

Reading and Writing Files

Aether-PHP provides file operations through the IO service:

File operations
# Read a JSON file
$file = Aether()->_io()->_file('data.json', IOTypeEnum::JSON);
$data = $file->_readDecoded(); # Returns decoded JSON as array

# Write a JSON file
$file->_write(['key' => 'value']); # Automatically encodes to JSON

Supported file types

  • JSON: Automatically encodes/decodes JSON
  • ENV: Parses .env format
  • TEXT: Plain text files
  • PHP: PHP files

Working with Folders

Create and manipulate folders:

Folder operations
$folder = Aether()->_io()->_folder('path/to/folder');

# Check if exists
if (!$folder->_exist()) {
    $folder->_create();
}

# List files
$files = $folder->_listFiles('*.php');

# Create a file in folder
$newFile = $folder->_createFile('newfile.php');

Set folder permissions

Permissions
$folder->_setPerm(0755); # Read, write, execute for owner; read, execute for others

Stream Operations

For large files, use streams:

Stream operations
$stream = Aether()->_io()->_stream('largefile.txt', IOTypeEnum::TEXT);

# Read line by line with callback
$stream->_readEachLine(function($line) {
    echo $line . PHP_EOL;
});

Streams use file locking to prevent concurrent access issues.

Setting Up Translations

Aether-PHP includes an I18n module. Create translation files in lang/{code}/{code}_{UPPERCASE}.json:

lang/en/en_EN.json
{
    "welcome": "Welcome",
    "hello": "Hello, {name}!",
    "admin": {
        "users": "Users",
        "settings": "Settings"
    }
}
lang/fr/fr_FR.json
{
    "welcome": "Bienvenue",
    "hello": "Bonjour, {name}!",
    "admin": {
        "users": "Utilisateurs",
        "settings": "Paramètres"
    }
}

Using Translations

Use the global __() function to translate:

Translation examples
echo __('welcome'); # "Welcome" or "Bienvenue" depending on language

# With parameters:
echo __('hello', ['name' => 'John']); # "Hello, John!" or "Bonjour, John!"

# Nested keys:
echo __('admin.users'); # "Users" or "Utilisateurs"

# Force a specific language:
echo __('welcome', [], 'fr'); # Always returns French translation

The framework automatically detects language from Accept-Language header, or falls back to the default language.

The I18n module must be registered in app/App/App.php for translations to work.

Available Commands

Aether-PHP includes a CLI tool. Run commands using:

Terminal
php bin/aether [command]

Available commands

CommandDescription
make:controller Name Creates a new controller
make:file path/to/file.php Creates a new file
make:folder path/to/folder Creates a new folder
setup Runs setup commands from Aetherfile
setup:dev Runs setup commands from Aetherfile.dev
setup:prod Runs setup commands from Aetherfile.prod
source:script path/to/script.php Runs a custom script

Creating Controllers via CLI

Generate a controller quickly:

Terminal
php bin/aether make:controller UserController

This creates app/App/Controller/UserController.php with basic structure.

Setup Files

Create an Aetherfile in your project root with commands to run:

Aetherfile
echo "Setting up project..."
# Database migrations
php scripts/migrate.php
# Seed data
php scripts/seed.php
echo "Setup complete!"

Run it with: php bin/aether setup

Create environment-specific files:

  • Aetherfile.dev for development
  • Aetherfile.prod for production

Custom Scripts

Create custom CLI scripts by extending BaseScript:

Custom script
use Aether\Modules\AetherCLI\Script\BaseScript;

class MyScript extends BaseScript {
    public function _onLoad() {
        $this->_logger->_echo("Running custom script...");
    }

    public function _onRun() {
        // Your script logic here
        $this->_logger->_echo("Script completed!");
    }
}

Run it with: php bin/aether source:script path/to/MyScript.php

Advanced Routing

Organize routes by feature or domain:

Route Organization

app/App/Controller/UserController.php
/**
 * [@base] => /users
 */
class UserController extends Controller {
    /**
     * [@method] => GET
     * [@route]  => /
     */
    public function index() { /* List users */ }

    /**
     * [@method] => GET
     * [@route]  => /{id}
     */
    public function show($id) { /* Show user */ }
}

This creates routes:

  • GET /users
  • GET /users/{id}

RESTful API Structure

RESTful controller
/**
 * [@base] => /api/v1/users
 */
class UserApiController extends Controller {
    /**
     * [@method] => GET
     * [@route]  => /
     */
    public function index() { /* GET /api/v1/users */ }

    /**
     * [@method] => GET
     * [@route]  => /{id}
     */
    public function show($id) {
/* GET /api/v1/users/{id} */ }

    /**
     * [@method] => POST
     * [@route]  => /
     */
    public function store() { /* POST /api/v1/users */ }

    /**
     * [@method] => PUT
     * [@route]  => /{id}
     */
    public function update($id) { /* PUT /api/v1/users/{id} */ }

    /**
     * [@method] => DELETE
     * [@route]  => /{id}
     */
    public function destroy($id) { /* DELETE /api/v1/users/{id} */ }
}

Advanced Database Operations

Build complex queries by chaining methods:

Complex query example
$results = $db->_table('users')
    ->_select('users.name', 'users.email', 'posts.title')
    ->_join('posts', 'users.id = posts.user_id')
    ->_where('users.status', 'active')
    ->_where('posts.published', true)
    ->_send();

Database Transactions

Use raw SQL for transactions:

Transaction example
try {
    $db->_raw('START TRANSACTION');

    $db->_table('users')->_insert('name', 'John')->_send();
    $db->_table('posts')->_insert('title', 'Post')->_send();

    $db->_raw('COMMIT');
} catch (Exception $e) {
    $db->_raw('ROLLBACK');
    throw $e;
}
Consider wrapping transaction logic in a service class for reusability.

Multiple Databases

Work with multiple databases:

Multiple database connections
$mainDb = Aether()->_db()->_mysql('main_database');
$analyticsDb = Aether()->_db()->_mysql('analytics_database');

$users = $mainDb->_table('users')->_select('*')->_send();
$stats = $analyticsDb->_table('stats')->_select('*')->_send();

Advanced Authentication

Create custom authentication methods by extending AuthInstance:

Custom authentication gateway
use Aether\Auth\AuthInstance;
use Aether\Auth\Gateway\AuthGatewayEventInterface;

class OAuthGateway extends AuthInstance implements AuthGatewayEventInterface {
    public function _tryAuth(): bool {
        # Your authentication logic
        # Return true on success, false on failure
    }

    public function _onSuccess(array $_data): string {
        # Handle successful authentication
        # Create UserInstance and store in session
        return "Authentication successful";
    }

    public function _onFailure(): string {
        return "Authentication failed";
    }
}

Permission Management

Create a permission enum for type safety:

Permission enum
enum PermissionEnum: string {
    case ADMIN = 'PERM.ADMIN';
    case MODERATOR = 'PERM.MODERATOR';
    case USER = 'PERM.USER';
}

# Use it in your code:
if ($user->_hasPerm(PermissionEnum::ADMIN->value)) {
    # Admin only code
}

Protecting Routes

Create middleware to protect routes:

Auth middleware
class AuthMiddleware implements MiddlewareInterface {
    public function _handle(callable $_next) {
        if (!Aether()->_session()->_auth()->_isLoggedIn()) {
            http_response_code(401);
            Aether()->_http()->_response()->_json([
                'error' => 'Unauthorized'
            ], 401)->_send();
            return;
        }

        $_next();
    }
}

Code Organization

Organize your code following these principles:

  • Keep controllers thin - move business logic to service classes
  • Use repositories for database access patterns
  • Separate API controllers from web controllers
  • Group related routes in the same controller

Security

Security best practices:

  • Always validate and sanitize user input
  • Use parameterized queries (QueryBuilder does this automatically)
  • Regenerate session ID after login
  • Use strong SESSION_HMAC key
  • Keep APP_ENV=prod in production
  • Never expose sensitive data in error messages

Performance

Performance tips:

  • Use caching for expensive operations
  • Optimize database queries (avoid N+1 problems)
  • Use indexes on frequently queried columns
  • Cache database connections (already done automatically)
  • Minimize middleware overhead

Testing

Testing recommendations:

  • Test controllers with different HTTP methods
  • Test authentication flows
  • Test database operations
  • Test error handling
  • Use a test database for integration tests

Simple Blog API

Complete example - Blog API with authentication:

app/App/Controller/Api/PostApiController.php
/**
 * [@base] => /api/v1/posts
 */
class PostApiController extends Controller {
    /**
     * [@method] => GET
     * [@route]  => /
     */
    public function index() {
        $db = Aether()->_db()->_mysql('blog');
        $posts = $db->_table('posts')
            ->_select('*')
            ->_where('published', true)
            ->_send();

        return Aether()->_http()->_response()->_json($posts, 200)->_send();
    }

    /**
     * [@method] => POST
     * [@route]  => /
     */
    public function store() {
        # Check authentication
        if (!Aether()->_session()->_auth()->_isLoggedIn()) {
            return Aether()->_http()->_response()->_json([
                'error' => 'Unauthorized'
            ], 401)->_send();
        }

        # Get request data
        $params = new HttpParameterUnpacker();
        $title = $params->_getAttribute('title');
        $content = $params->_getAttribute('content');

        # Validate
        if (!$title || !$content) {
            return Aether()->_http()->_response()->_json([
                'error' => 'Title and content required'
            ], 400)->_send();
        }

        # Insert
        $db = Aether()->_db()->_mysql('blog');
        $db->_table('posts')
            ->_insert('title', $title)
            ->_insert('content', $content)
            ->_insert('user_id', Aether()->_session()->_auth()->_getUser()->_getUid())
            ->_insert('published', false)
            ->_send();

        return Aether()->_http()->_response()->_json([
            'message' => 'Post created'
        ], 201)->_send();
    }
}

User Dashboard

Complete example - User dashboard with views:

app/App/Controller/DashboardController.php
class DashboardController extends Controller {
    /**
     * [@method] => GET
     * [@route]  => /dashboard
     */
    public function index() {
        # Check authentication
        if (!Aether()->_session()->_auth()->_isLoggedIn()) {
            header('Location: /login');
            exit;
        }

        $user = Aether()->_session()->_auth()->_getUser();
        $db = Aether()->_db()->_mysql('app');

        # Get user's posts
        $posts = $db->_table('posts')
            ->_select('*')
            ->_where('user_id', $user->_getUid())
            ->_send();

        # Render view
        $this->_render('dashboard', [
            'user' => $user,
            'posts' => $posts
        ]);
    }
}

View file

public/views/dashboard.php
<h1>Welcome, <?php echo $user->_getUsername(); ?>!</h1>

<h2>Your Posts</h2>
<?php foreach ($posts as $post): ?>
    <div class="post">
        <h3><?php echo htmlspecialchars($post['title']); ?></h3>
        <p><?php echo htmlspecialchars($post['content']); ?></p>
    </div>
<?php endforeach; ?>

Common Issues

Common issues and solutions:

Routes not working

Solutions:

  • Check that controller is in app/App/Controller/ or app/App/Controller/Api/
  • Verify annotations are correct: [@method] and [@route]
  • Check web server is pointing to /index.php directory
  • Ensure autoload.php is loading correctly

Database connection fails

Solutions:

  • Verify .env file has correct database credentials
  • Check database server is running
  • Ensure database exists
  • Check PHP PDO extension is installed

CSRF token errors

Solutions:

  • Include CSRF token in POST/PUT/DELETE requests
  • Get token from X-CSRF-Token header on GET requests
  • Ensure session is working correctly

Views not rendering

Solutions:

  • Check view file exists in public/views/
  • Verify file has .php extension
  • Check variables are passed correctly to _render()

Getting Help

If you encounter issues:

  • Check the error messages (in dev mode)
  • Review the framework source code
  • Check PHP error logs
  • Verify all requirements are met
  • Visit the GitHub repository for updates and issues
The framework is designed to be readable and understandable. Don't hesitate to explore the source code to understand how things work.