Framework Internals
How the framework works under the hood
DALT.PHP's framework is intentionally simple and transparent. Every component is designed to be read and understood. Let's explore how each piece works.
Core Components
The framework consists of 6 main components:
- Router - URL-to-controller mapping
- Database - PDO wrapper with prepared statements
- Session - Session and flash data management
- Middleware - Request filtering pipeline
- Container - Dependency injection
- Migration - Raw SQL migration runner
1. Router
The Router matches incoming URLs to controller files and extracts parameters.
How It Works
// Register routes
$router->get('/posts/{id}', 'posts/show.php');
// When /posts/123 is requested:
// 1. Router loops through registered routes
// 2. Matches pattern /posts/{id} against /posts/123
// 3. Extracts parameter: ['id' => '123']
// 4. Injects into $_GET: $_GET['id'] = '123'
// 5. Requires controller: app/Http/controllers/posts/show.phpKey Methods
add($method, $uri, $controller) Registers a route:
$router->add('GET', '/posts', 'posts/index.php');matchUri($pattern, $actual) Matches URL patterns with parameters:
// Pattern: /posts/{id}/edit
// Actual: /posts/123/edit
// Returns: ['id' => '123']Uses regex to convert {id} to ([^/]+) and extract values.
route($uri, $method) Main routing logic:
public function route($uri, $method)
{
foreach ($this->routes as $route) {
// Check HTTP method
if (strtoupper($method) !== $route['method']) {
continue;
}
// Try to match URI
$params = $this->matchUri($route['uri'], $uri);
if ($params === false) {
continue;
}
// Run middleware
Middleware::resolve($route['middleware']);
// Inject parameters into $_GET
foreach ($params as $key => $value) {
$_GET[$key] = $value;
}
// Execute controller
return require $controllerPath;
}
// No match found
$this->abort(404);
}Why Route Order Matters
Routes are matched in registration order:
// WRONG - generic before specific
$router->get('/posts/{id}', 'posts/show.php');
$router->get('/posts/create', 'posts/create.php'); // Never matches!
// CORRECT - specific before generic
$router->get('/posts/create', 'posts/create.php');
$router->get('/posts/{id}', 'posts/show.php');The router matches /posts/create against /posts/{id} first, treating "create" as an ID.
2. Database
The Database class wraps PDO with a fluent interface for queries.
How It Works
// Initialize with config
$db = new Database([
'driver' => 'sqlite',
'database' => 'database/app.sqlite'
]);
// Query with prepared statements
$posts = $db->query('SELECT * FROM posts WHERE published = ?', [1])->get();
// Find single record
$post = $db->query('SELECT * FROM posts WHERE id = ?', [$id])->find();
// Find or fail (404 if not found)
$post = $db->query('SELECT * FROM posts WHERE id = ?', [$id])->findOrFail();Key Methods
query($sql, $params) Prepares and executes a query:
public function query($query, $params = [])
{
$this->statement = $this->connection->prepare($query);
$this->statement->execute($params);
return $this; // Fluent interface
}find() Fetches single row:
public function find()
{
return $this->statement->fetch(); // Returns array or false
}get() Fetches all rows:
public function get()
{
return $this->statement->fetchAll(); // Returns array of arrays
}findOrFail() Fetches or aborts with 404:
public function findOrFail()
{
$result = $this->find();
if (!$result) {
abort(); // 404 error
}
return $result;
}Why Prepared Statements?
Prepared statements prevent SQL injection:
// VULNERABLE - SQL injection possible
$id = $_GET['id'];
$query = "SELECT * FROM posts WHERE id = $id";
// Attacker sends: ?id=1 OR 1=1
// Query becomes: SELECT * FROM posts WHERE id = 1 OR 1=1
// SAFE - prepared statement
$id = $_GET['id'];
$query = "SELECT * FROM posts WHERE id = ?";
$db->query($query, [$id]);
// Parameter is escaped automaticallyDatabase Drivers
Supports SQLite and PostgreSQL:
// SQLite (default)
'driver' => 'sqlite',
'database' => 'database/app.sqlite'
// PostgreSQL
'driver' => 'pgsql',
'host' => 'localhost',
'port' => 5432,
'dbname' => 'myapp',
'username' => 'root',
'password' => 'secret'
// PostgreSQL
'driver' => 'pgsql',
'host' => 'localhost',
'port' => 5432,
'dbname' => 'myapp',
'username' => 'postgres',
'password' => 'secret'3. Session
The Session class provides a clean API for session and flash data.
How It Works
// Store data in session
Session::put('user_id', 123);
// Retrieve data
$userId = Session::get('user_id');
// Check if exists
if (Session::has('user_id')) {
// User is logged in
}
// Flash data (available for next request only)
Session::flash('success', 'Post created!');
// Retrieve flash data
$message = Session::get('success'); // Available onceKey Methods
put($key, $value) Stores data in session:
public static function put($key, $value)
{
$_SESSION[$key] = $value;
}get($key, $default) Retrieves data (checks flash first):
public static function get($key, $default = null)
{
return $_SESSION['_flash'][$key] ?? $_SESSION[$key] ?? $default;
}flash($key, $value) Stores flash data:
public static function flash($key, $value)
{
$_SESSION['_flash'][$key] = $value;
}unflash() Clears flash data (called automatically after each request):
public static function unflash()
{
unset($_SESSION['_flash']);
}Flash Data Lifecycle
Request 1:
Session::flash('success', 'Saved!')
→ $_SESSION['_flash']['success'] = 'Saved!'
Request 2:
Session::get('success') // Returns 'Saved!'
Session::unflash() // Clears flash data
Request 3:
Session::get('success') // Returns null (flash cleared)Flash data is perfect for one-time messages after redirects.
4. Middleware
Middleware filters requests before they reach controllers.
How It Works
// Register route with middleware
$router->get('/dashboard', 'dashboard.php')->only('auth');
// When /dashboard is requested:
// 1. Router matches route
// 2. Middleware::resolve('auth') runs
// 3. Auth middleware checks $_SESSION['user']
// 4. If not logged in, redirects to /
// 5. If logged in, controller executesBuilt-in Middleware
Auth - Requires authentication:
class Auth
{
public function handle()
{
if (!($_SESSION['user'] ?? false)) {
header('location: /');
exit();
}
}
}Guest - Requires NOT authenticated:
class Guest
{
public function handle()
{
if ($_SESSION['user'] ?? false) {
header('location: /');
exit();
}
}
}Csrf - Validates CSRF tokens:
class Csrf
{
public function handle()
{
$method = $_POST['_method'] ?? $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
return; // Skip GET requests
}
$sessionToken = $_SESSION['_csrf'] ?? null;
$formToken = $_POST['_token'] ?? null;
if (!hash_equals($sessionToken, $formToken)) {
http_response_code(419);
echo 'CSRF token mismatch';
exit;
}
}
}Middleware Resolution
public static function resolve($key)
{
if (!$key) {
return;
}
$middleware = static::MAP[$key] ?? false;
if (!$middleware) {
throw new \Exception("No matching middleware found for key '{$key}'");
}
(new $middleware)->handle();
}Multiple Middleware
// Single middleware
$router->post('/posts', 'posts/store.php')->only('auth');
// Multiple middleware
$router->post('/posts', 'posts/store.php')->only(['auth', 'csrf']);5. Container
The Container provides dependency injection for managing class instances.
How It Works
// Bind a class to the container
App::bind(Database::class, function() {
$config = require base_path('config/database.php');
return new Database($config['database']);
});
// Resolve (get instance)
$db = App::resolve(Database::class);Key Methods
bind($key, $resolver) Registers a binding:
public function bind($key, $resolver)
{
$this->bindings[$key] = $resolver;
}resolve($key) Resolves a binding:
public function resolve($key)
{
if (!array_key_exists($key, $this->bindings)) {
throw new \Exception("No Matching Binding Found For {$key}");
}
$resolver = $this->bindings[$key];
return call_user_func($resolver);
}Why Use a Container?
Without container:
// Every controller needs to create database
$config = require 'config/database.php';
$db = new Database($config['database']);
$posts = $db->query('SELECT * FROM posts')->get();With container:
// Database is resolved automatically
$db = App::resolve(Database::class);
$posts = $db->query('SELECT * FROM posts')->get();The container centralizes object creation and makes testing easier.
6. Migration System
The Migration class runs raw SQL migration files.
How It Works
// Create migration
php artisan make:migration create_posts_table
// Generates: database/migrations/20240315120000_create_posts_table.sql
// Run migrations
php artisan migrate
// Migration system:
// 1. Creates 'migrations' tracking table
// 2. Scans database/migrations/ for .sql files
// 3. Checks which migrations have run
// 4. Executes pending migrations in order
// 5. Records completed migrationsKey Methods
runMigrations() Main migration logic:
public function runMigrations()
{
$this->createMigrationsTable();
$files = glob(base_path('database/migrations/*.sql'));
sort($files);
$batch = $this->getNextBatch();
foreach ($files as $file) {
$migration = basename($file);
if (!$this->hasRun($migration)) {
$sql = file_get_contents($file);
$this->database->connection->exec($sql);
$this->markAsRun($migration, $batch);
}
}
}hasRun($migration) Checks if migration already ran:
public function hasRun($migration)
{
$result = $this->database->query(
'SELECT migration FROM migrations WHERE migration = ?',
[$migration]
)->find();
return $result !== false;
}Why Raw SQL?
DALT.PHP uses raw SQL migrations instead of PHP migration classes:
-- Raw SQL (DALT.PHP)
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);// PHP migration (Laravel)
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});For learning: Raw SQL teaches actual SQL syntax, not framework-specific abstractions.
Helper Functions
The framework includes helper functions for common tasks.
view($path, $attributes)
Renders a view template:
// In controller
view('posts/index.view.php', [
'posts' => $posts,
'title' => 'All Posts'
]);
// Extracts variables and requires view
function view($path, $attributes = [])
{
extract($attributes);
require base_path('resources/views/' . $path);
}dd($value)
Dump and die for debugging:
dd($posts); // Dumps $posts and stops execution
function dd($value)
{
echo '<pre>';
var_dump($value);
echo '</pre>';
die();
}abort($code)
Aborts with HTTP error:
abort(404); // Shows 404 page and stops
function abort($code = 404)
{
http_response_code($code);
require base_path("resources/views/status/{$code}.php");
die();
}base_path($path)
Gets absolute path from project root:
$config = require base_path('config/app.php');
function base_path($path = '')
{
return BASE_PATH . $path;
}Bootstrap Process
When a request arrives, here's what happens:
1. Entry Point (public/index.php)
<?php
define('BASE_PATH', __DIR__ . '/../');
require BASE_PATH . 'framework/Core/bootstrap.php';2. Bootstrap (framework/Core/bootstrap.php)
// Load Composer autoloader
require base_path('vendor/autoload.php');
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(base_path());
$dotenv->load();
// Load helper functions
require base_path('framework/Core/functions.php');
// Start session
session_start();
// Create container
$container = new Container();
App::setContainer($container);
// Bind database
App::bind(Database::class, function() {
$config = require base_path('config/database.php');
return new Database($config['database']);
});
// Create router
$router = new Router();
// Load routes
require base_path('routes/routes.php');
require base_path('.dalt/routes/routes.php'); // Platform routes
// Route the request
$uri = parse_url($_SERVER['REQUEST_URI'])['path'];
$method = $_POST['_method'] ?? $_SERVER['REQUEST_METHOD'];
$router->route($uri, $method);
// Clear flash data
Session::unflash();3. Controller Execution
// Controller (app/Http/controllers/posts/index.php)
<?php
$db = App::resolve(Database::class);
$posts = $db->query('SELECT * FROM posts')->get();
view('posts/index.view.php', [
'posts' => $posts
]);4. View Rendering
// View (resources/views/posts/index.view.php)
<?php foreach ($posts as $post): ?>
<h2><?= $post['title'] ?></h2>
<p><?= $post['body'] ?></p>
<?php endforeach; ?>Request Flow Diagram
1. Browser → public/index.php
↓
2. Load bootstrap.php
↓
3. Start session, load env, create container
↓
4. Create router, load routes
↓
5. Router::route($uri, $method)
↓
6. Match route pattern
↓
7. Run middleware (Auth, CSRF, etc.)
↓
8. Require controller file
↓
9. Controller resolves dependencies (Database)
↓
10. Controller queries database
↓
11. Controller calls view()
↓
12. View renders HTML
↓
13. Clear flash data
↓
14. Response sent to browserWhy This Architecture?
Simple and Readable
Every component is under 200 lines of code. You can read and understand the entire framework in an afternoon.
No Magic
No magic methods, no hidden behavior, no complex abstractions. What you see is what you get.
Educational
Designed for learning, not production. Each component teaches a concept:
- Router → URL matching and parameters
- Database → Prepared statements and SQL
- Session → State management
- Middleware → Request filtering
- Container → Dependency injection
- Migration → Database versioning
Transparent
Framework source code is in framework/Core/ - always available to read. When something breaks, you can trace through the code and understand why.
Read the Source: The best way to understand the framework is to read the actual code in framework/Core/. It's intentionally simple and well-commented.
Next Steps
- Project Structure - Where each file lives
- Architecture - How components fit together
- Building a Blog - Use the framework in practice