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.
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', '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.
UPDATE Queries
Update records using _update() and _set():
$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
_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', '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():
$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
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