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:
Installation
To get started with Aether-PHP:
Clone or download the framework
git clone https://github.com/Aether-PHP/Aether-PHP
cd Aether-PHP
Point your web server to the public directory
Configure your web server's document root to point to the public directory.
Configure your .env file
cp .env.example .env
Access your application
Visit your application through the web server. The entry point is /index.php which automatically loads everything.
Environment Variables
Aether-PHP uses a .env file for configuration. Create it in the project root and use the following example as a base:
# 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:
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:
$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.
Framework Architecture
Aether-PHP is organized around a very small core, an application layer, and optional modules. Understanding this structure makes it easier to navigate the source and extend the framework safely.
High-level layout
public/– front controller (index.php), assets and public viewssrc/Aether/– framework core (routing, HTTP layer, middleware, database, auth, IO, cache, sessions, modules)app/App/– your application code (controllers, configuration of middlewares, modules, etc.)src/Aether/Modules/– optional modules (CLI, i18n, analytics, ...)
The heart of the framework is the \Aether\Aether class. It is responsible for booting the environment,
loading configuration and sessions, initializing your App\App class, running global middlewares, and finally
dispatching the request to the right controller via the router.
public/index.php, then jump into \Aether\Aether and App\App to
follow the complete flow of a request inside the framework.
Request Lifecycle
Every HTTP request goes through a predictable pipeline: environment bootstrap, middleware execution, routing, controller, and finally a typed response.
Step-by-step flow
- Front controller –
public/index.phploadsautoload.phpand creates\Aether\Aether. - Core boot –
\Aether\Aether->_run()enables dev mode if needed, loads.envwithProjectConfigand boots sessions. - Application init –
App\App::_init()registers modules and the list of global middlewares. - Middleware pipeline – all global middlewares are executed in order using the
Pipelinecomponent. - Routing – the
RouterandControllerGatewaylocate the correct controller method based on annotations. - Controller execution – your controller method runs, can talk to services, database, views, etc.
- Response – a typed
HttpResponse(JSON, HTML, XML, ... ) is sent back to the client.
If no route matches the current URL, the router automatically sends a 404 Not Found response. If a controller or
annotation is invalid, a dedicated framework exception is thrown (for example RouterControllerException).
Service Manager & Hubs
Instead of a hidden container, Aether-PHP exposes a single Service Manager entry point that gives you access to strongly-typed service hubs.
Anywhere in your code, you can call the global Aether() helper (or use \Aether\Aether::_getServices())
to retrieve the Service Manager. From there, you access dedicated hubs:
_db()– database hub (MySQL / SQLite) with the fluent QueryBuilder_http()– HTTP hub (request + response objects)_session()– sessions and authentication gateway_cache()– APCU-based cache layer (and future adapters)_io()– files and folders abstraction layer_config()– read-only access to configuration values
This pattern keeps all framework services discoverable and makes refactoring easier: you do not have to guess where things are injected, you follow the service hubs from the Aether entry point.
Modules System Overview
Modules are optional, isolated packages that extend the core framework without adding Composer dependencies.
A module is simply a PHP class extending Aether\Modules\AetherModule, placed under src/Aether/Modules/YourModule/,
usually alongside a module.yml file that describes the module. During App::_init(), the ModuleFactory
instantiates each module and calls its _onLoad() hook.
Typical module structure
src/Aether/Modules/AetherCLI/– CLI commands and toolingsrc/Aether/Modules/I18n/– translation layer and helper functionssrc/Aether/Modules/Analytics/– HTTP analytics & statistics (documented below)
To enable a module, add its class to the $_modules array in App\App. Modules can internally register
their own services, commands, or listeners while still using the same Service Manager as the rest of your app.
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 controllersapp/App/Controller/Api/for API controllers
Creating Your First Route
Let's create a simple route. Create a file 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
Route Parameters
You can capture URL parameters using curly braces:
/**
* [@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
/**
* [@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";
}
Base Paths
For API controllers, you often want a base path prefix. Use the [@base] annotation on the class:
/**
* [@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.
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:
/**
* [@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
Other Response Formats
Aether-PHP supports multiple response formats:
# 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();
Rendering Views
For web pages, render views using the _render() method:
/**
* [@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
<h1><?php echo $title; ?></h1>
<p>Hello, <?php echo $user['name']; ?>!</p>
Connecting to Database
Aether-PHP supports MySQL and SQLite databases. Access the database through the ServiceManager:
# 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.
Basic SELECT Queries
Use the QueryBuilder for type-safe database queries:
$users = $db->_table('users')
->_select('*')
->_send();
This executes: SELECT * FROM users. The _send() method executes the query and returns results.
Select 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():
$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
$users = $db->_table('users')
->_select('*')
->_where('status', 'active')
->_where('role', 'admin')
->_send();
This executes: SELECT * FROM users WHERE status = 'active' AND role = 'admin'
SELECT * FROM `users` WHERE status = :status AND role = :role, with all key/value pairs inputted in prepared statements.
INSERT Queries
Insert data using _insert():
$db->_table('users')
->_insert('name', 'John Doe')
->_insert('email', '[email protected]')
->_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.
UPDATE Queries
Update records using _update() and _set():
$db->_table('users')
->_update()
->_set('name', 'Jane Doe')
->_set('email', '[email protected]')
->_where('id', 1)
->_send();
This executes: UPDATE users SET name = :set_name, email = :set_email WHERE id = :id
_where() clause to avoid updating all rows!
DELETE Queries
Delete records using _delete():
$db->_table('users')
->_delete()
->_where('id', 1)
->_send();
This executes: DELETE FROM users WHERE id = :id
_where() clause! Forgetting it will delete ALL rows in the table.
Checking Existence
Check if a record exists using _exist():
$exists = $db->_table('users')
->_exist()
->_where('email', '[email protected]')
->_send();
This returns true if the record exists, false otherwise. Useful for validation before inserting new records.
JOIN Queries
Perform JOINs using _join():
$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():
$results = $db->_raw('SELECT COUNT(*) as total FROM users WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 MONTH)');
_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:
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
- password_hash (or password)
- perms (JSON array of permissions)
User Login
To log in a user:
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:
use Aether\Auth\User\UserFactory;
if (UserFactory::_isLoggedIn()) {
# User is logged in
} else {
# User is not logged in
}
Or use the ServiceManager (much appreciated):
if (Aether()->_session()->_auth()->_isLoggedIn()) {
# User is logged in
}
Getting Current User
Retrieve the current logged-in 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):
$user = Aether()->_session()->_auth()->_getUser();
if ($user) {
echo $user->_getUid();
echo $user->_getUsername();
echo $user->_getEmail();
}
User Permissions
Check user permissions:
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:
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.
# 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
# Get session ID:
$sessionId = Aether()->_session()->_get()->_getSessId();
# Regenerate session ID (useful after login):
Aether()->_session()->_get()->_regenerateId();
Reading POST/PUT Data
To read data from POST or PUT requests:
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
$email = $params->_getAttribute('email');
if ($email === false) {
# Attribute doesn't exist
}
Making HTTP Requests
Make outgoing HTTP requests:
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.
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:
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)
// 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
<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:
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:
private static $_middlewares = [
LoggingMiddleware::class,
RatelimitMiddleware::class,
// ... other middlewares
];
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
/**
* [@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:
/**
* [@method] => POST
* [@route] => /admin/users
* [@middlewares] => AuthMiddleware,AdminOnlyMiddleware
*/
public function storeUser() {
# Executed only if both middlewares allow it to pass
}
[@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:
$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 = 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
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
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 addressuser_agent: raw User-Agent stringprotocol:httporhttpsdomain: 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 bodyphptimestamp: PHP timestamp (seconds)
Reading statistics
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
Building a Module from Scratch
Modules let you package reusable features (CLI tools, integrations, background jobs, etc.) without coupling them to a single project. This guide walks you through creating a minimal Aether module step by step.
1. Create the folder structure
Create a new directory for your module under src/Aether/Modules:
src/
Aether/
Modules/
MyModule/
module.yml
src/
MyModule.php
Service/
MyService.php
The only hard requirement is the main PHP class extending Aether\Modules\AetherModule. The rest of the structure
is up to you, but keeping a src/ folder per module keeps things clean.
2. Describe the module (module.yml)
Add a simple module.yml file to document your module:
name: my-module
description: Custom business features for my app
version: 1.0.0
This file is not required by the core, but it is used by existing modules and is a good convention to follow.
3. Implement the module class
Create your main module class extending AetherModule:
<?php
declare(strict_types=1);
namespace Aether\Modules\MyModule;
use Aether\Modules\AetherModule;
final class MyModule extends AetherModule
{
public static function _make() : self
{
# Factory method used by ModuleFactory
return new self();
}
public function _onLoad() : void
{
# This is called once when the module is loaded at app boot.
# Register services, hooks, or perform initialization here.
}
}
The _make() factory is used by ModuleFactory::_load() to instantiate your module. The _onLoad()
method is your entry point to plug into the rest of the framework.
4. Register the module in your App
In app/App/App.php, add your module class to the $_modules array:
use Aether\Modules\MyModule\MyModule;
class App {
private static array $_modules = [
MyModule::class,
// Analytics::class, I18N::class, ...
];
public static function _init() : void {
ModuleFactory::_load(self::$_modules);
# Other boot logic...
}
}
On the next request, your module will be instantiated and its _onLoad() method will be executed once.
5. Expose functionality from your module
Inside _onLoad(), you typically:
- Register additional routes (by adding controllers under
app/App/Controllerorapp/App/Controller/Api) - Register CLI commands (if you integrate with the CLI module)
- Initialize databases or storage used by your module
- Attach middlewares or listeners using existing framework hooks
Aether()->_db(), Aether()->_io(), Aether()->_cache(), ...) from inside your module.
Reading and Writing Files
Aether-PHP provides file operations through the IO service:
# 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 = 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
$folder->_setPerm(0755); # Read, write, execute for owner; read, execute for others
Stream Operations
For large files, use streams:
$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:
{
"welcome": "Welcome",
"hello": "Hello, {name}!",
"admin": {
"users": "Users",
"settings": "Settings"
}
}
{
"welcome": "Bienvenue",
"hello": "Bonjour, {name}!",
"admin": {
"users": "Utilisateurs",
"settings": "Paramètres"
}
}
Using Translations
Use the global __() function to translate:
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.
Available Commands
Aether-PHP includes a CLI tool. Run commands using:
php bin/aether [command]
Available commands
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:
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:
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.devfor developmentAetherfile.prodfor production
Custom Scripts
Create custom CLI scripts by extending BaseScript:
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
/**
* [@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
/**
* [@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:
$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:
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;
}
Multiple Databases
Work with multiple databases:
$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:
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:
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:
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:
/**
* [@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:
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
<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