Lento is one of easiest PHP microframework ever built. Zero config. Zero SQL. Zero Composer. Perfect for beginners who want to build web apps in minutes.
- Declarative Routing — Build complete apps without writing a single
function() - Zero-Config Database — SQLite auto-creates file and tables, no SQL needed
- Template Rendering — Plain HTML with variables, no templating language to learn
- Session & Auth — Simple login/logout helpers
- CSRF Protection — Automatic token generation and validation
- Auth Gates —
requireAuth()protects routes declaratively - Duplicate Prevention —
failIfFound()stops double submissions - File Uploads & Remote Downloads —
upload()anddownload()handle files in one chain - JSON APIs —
json()responses andjson_input()for modern frontends - AI Integration —
openai()connects to OpenAI-compatible APIs declaratively - Static Files — Serve CSS/JS/images automatically
- Error Pages — Custom 404/500 pages, dev mode for debugging
- Security Built-in — Sensitive directories automatically blocked, no Linux config needed
<?php
define('LENTO_ENTRY', true);
require 'src/Lento.php';
$app = new Lento();
// 1. Plain text route
$app->get('/', 'Hello World!');
// 2. Declarative route (no closures!)
$app->get('/users')->db('users')->all()->json();
// 3. Template with variables
$app->get('/greet')->render('hello.html');
$app->run();Lento makes web development approachable by handling the "boring stuff" for you. Here is how it compares to writing plain PHP.
Native PHP:
$uri = $_SERVER['REQUEST_URI'];
switch ($uri) {
case '/':
echo "Home";
break;
case '/about':
echo "About";
break;
default:
http_response_code(404);
echo "Not Found";
}Lento (Declarative):
$app->get('/', 'Home');
$app->get('/about', 'About');Native PHP:
$pdo = new PDO('sqlite:data.db');
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->execute(['John']);Lento:
// No setup needed. Table and DB are created automatically!
db('users')->create(['name' => 'John']);Native PHP:
$name = $_POST['name'] ?? '';
echo "Hello " . htmlspecialchars($name, ENT_QUOTES, 'UTF-8');Lento:
// Request helpers escape input by default.
$app->post('/greet', function() {
return "Hello " . post('name');
});When rendering database or template data, escape it in your HTML:
<h1><?= escape($user['name']) ?></h1>Native PHP:
header('Content-Type: application/json');
echo json_encode(['status' => 'ok', 'data' => $users]);
exit;Lento:
$app->get('/api/users')->db('users')->all()->json();Native PHP:
if (isset($_FILES['avatar'])) {
$target = 'uploads/' . basename($_FILES['avatar']['name']);
if (!is_dir('uploads')) mkdir('uploads', 0777, true);
move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
}Lento (Declarative):
$app->post('/upload')->upload('photo', 'uploads/')->db('photos')->create([
'title' => '{title}',
'filename' => '{filename}',
'created_at' => '{now}'
])->json();Native PHP:
session_start();
if (!isset($_SESSION['user'])) {
header('Location: /login');
exit;
}
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
echo 'Invalid token';
exit;
}Lento (Declarative):
$app->get('/admin')->requireAuth()->render('dashboard.html');
$app->post('/admin/login')->assert('password', 'secret123')->login(['role' => 'admin'])->redirect('/admin');
<form method="POST" action="/admin/polls">
<?= $app->csrfField() ?>
</form>Lento now supports a revolutionary declarative syntax that lets you build complete apps without writing a single function(). Each route reads like an English sentence:
// URL Shortener - Complete app in 4 lines!
$app->get('/')->db('links')->order('created_at DESC')->limit(10)->all()->render('home.html', 'links');
$app->post('/')->validate('url', 'url')->db('links')->code('short_code')->create([
'short_code' => '{short_code}',
'original_url' => '{url}',
'clicks' => 0,
'created_at' => '{now}'
])->redirect('/');
$app->get('/stats')->db('links')->order('clicks DESC')->all()->render('stats.html', 'links');
$app->get('/{code}')->db('links')->where('short_code = ?', ['{code}'])->first()->failIfEmpty('Link not found')->increment('clicks')->redirect('original_url');// Secret Poll & Admirer — Complete app with admin dashboard, zero closures!
$app->get('/')->db('polls')->where('is_published = ?', [1])->all()->storeAs('polls')
->db('letters')->order('created_at DESC')->all()->storeAs('letters')->render('index.html');
$app->post('/vote')->validate('poll_id', 'numeric')->db('votes')
->where('ip_address = ? AND poll_id = ?', ['{ip}', '{poll_id}'])->first()
->failIfFound('You already voted!')->db('votes')->create([
'poll_id' => '{poll_id}', 'option_index' => '{option}',
'ip_address' => '{ip}', 'created_at' => '{now}'
])->redirect('/');
$app->post('/admin/login')->assert('password', 'secret123')->login(['role' => 'admin'])->redirect('/admin');
$app->get('/admin')->requireAuth()->db('polls')->all()->storeAs('polls')
->db('letters')->all()->storeAs('letters')->render('dashboard.html');// AI Chatbot — session-aware LLM workflow with zero closure business logic
$llmKey = 'YOUR_API_KEY_HERE';
$llmUrl = 'https://openrouter.ai/api/v1';
$app->post('/api/chat/{id}/message')
->validate('content', 'required')
->db('chats')->where('id = ? AND session_id = ?', ['{id}', '{session_id}'])->first()
->failIfEmpty('Chat not found')
->db('messages')->create([
'chat_id' => '{id}',
'role' => 'user',
'content' => '{content}',
'created_at' => '{now}'
])
->db('messages')->where('chat_id = ?', ['{id}'])->order('id ASC')->all()->storeAs('history_raw')
->openai([
'key' => $llmKey,
'url' => $llmUrl . '/chat/completions',
'model' => '{model}',
'messages' => '{history_raw}'
])->storeAs('ai_response')
->db('messages')->create([
'chat_id' => '{id}',
'role' => 'assistant',
'content' => '{ai_response}',
'created_at' => '{now}'
])->json();| Method | Description | Example |
|---|---|---|
db('table') |
Select database table | ->db('users') |
all() |
Get all records | ->db('users')->all() |
first() |
Get first record | ->db('users')->where('id = ?', [1])->first() |
create([...]) |
Insert record | ->db('users')->create(['name' => 'John']) |
update(id, [...]) |
Update record | ->db('users')->update(1, ['name' => 'Jane']) |
delete(id) |
Delete record | ->db('users')->delete('{id}') |
where('...', [...]) |
Filter records | ->db('users')->where('active = ?', [1]) |
order('...') |
Sort results | ->db('users')->order('name ASC') |
limit(n) |
Limit results | ->db('users')->limit(10) |
offset(n) |
Skip records | ->db('users')->limit(10)->offset(10) |
render('file', 'var') |
Render template | ->db('users')->all()->render('users.html', 'users') |
json() |
Return JSON | ->db('users')->all()->json() |
redirect('field') |
Redirect to URL | ->first()->redirect('url_field') |
validate('field', 'rule') |
Validate input | ->validate('email', 'email') |
code('field', length) |
Generate unique code | ->code('short_code', 6) |
increment('column') |
Increment counter | ->first()->increment('clicks') |
upload('field', 'dir') |
Handle file upload | ->upload('photo', 'uploads/') |
download('field', 'dir') |
Download from URL | ->download('image_url', 'uploads/') |
requireAuth('url') |
Protect route with auth | ->requireAuth('/login') |
csrf() |
Validate CSRF token | ->csrf() |
failIfFound('msg') |
Abort if record exists | ->first()->failIfFound('Already voted!') |
failIfEmpty('msg', status) |
Abort if nothing found | ->first()->failIfEmpty('Not found', 404) |
assert('f', 'val') |
Check field equals value | ->assert('password', 'secret') |
storeAs('name') |
Store result as variable | ->all()->storeAs('users') |
toggle('col') |
Toggle boolean column | ->first()->toggle('active') |
openai([...]) |
Call OpenAI-compatible API | ->openai(['model' => '{model}', ...]) |
When using declarative routes, these tokens are automatically replaced:
{param}— Route parameters (e.g.,{code}from/{code}){field}— POST data or JSON input (e.g.,{title}from form field){now}— Current timestamp (e.g.,2026-04-22 14:30:00){ip}— Client IP address (e.g.,192.168.1.1){session_id}— Current PHP session ID for anonymous user-specific data
$app->post('/users')->db('users')->create([
'name' => '{name}', // From POST/JSON
'email' => '{email}', // From POST/JSON
'created_at' => '{now}' // Auto-generated timestamp
])->json();$app->get('/chat/{id}')->db('chats')
->where('id = ? AND session_id = ?', ['{id}', '{session_id}'])
->first()->failIfEmpty('Chat not found')->json();Protect state-changing routes with ->csrf() and include the token in your form:
$app->post('/admin/polls')->requireAuth()->csrf()->db('polls')->create([
'title' => '{title}',
'created_at' => '{now}'
])->redirect('/admin');<form method="POST" action="/admin/polls">
<?= $app->csrfField() ?>
<input type="text" name="title">
<button type="submit">Create</button>
</form>login([...])stores user data in the sessionlogout()clears itrequireAuth('/login')protects a route declaratively{session_id}lets you isolate guest data without a full auth system
$app->get('/dashboard')->requireAuth()->render('dashboard.html');
$app->post('/login')->assert('password', 'secret123')->login(['role' => 'admin'])->redirect('/dashboard');assert() is great for demos, prototypes, and internal tools. For production apps, use proper password hashing and stronger auth logic.
request(),get(), andpost()escape input by default- Database data and variables passed to
render()are raw - Escape template output manually with
escape()orhtmlspecialchars()
<li><?= escape($photo['title']) ?></li>Lento works well for JSON APIs and modern frontends:
json()returns API responsesjson_input()reads raw JSON bodiesopenai()connects to OpenAI-compatible APIs declarativelydownload()can fetch remote assets into your app
$data = json_input();
$app->post('/api/photos')->db('photos')->create([
'title' => '{title}',
'created_at' => '{now}'
])->json();$llmKey = 'YOUR_API_KEY_HERE';
$llmUrl = 'https://openrouter.ai/api/v1';For local testing, you can set the URL directly in index.php and leave the key empty if your local LLM does not require authentication.
Requirements: openai() and remote download() rely on allow_url_fopen, so make sure it is enabled in php.ini.
Prefer closures? No problem. Lento still supports the classic syntax:
$app->route('/', function() {
return 'Hello World!';
});
$app->route('/user/{id}', function($id) {
$user = db('users')->where('id = ?', [$id])->first();
return json_resp($user);
});Lento supports all standard HTTP methods:
$app->get('/users', ...); // Read
$app->post('/users', ...); // Create
$app->put('/users/{id}', ...); // Update
$app->patch('/users/{id}', ...);// Partial update
$app->delete('/users/{id}', ...);// DeleteCapture URL segments with curly braces:
$app->get('/user/{id}')->db('users')->where('id = ?', ['{id}'])->first()->json();Just download and include. No Composer needed!
define('LENTO_ENTRY', true);
require 'src/Lento.php';From the root directory:
php -S 0.0.0.0:8005 index.phpThen open http://localhost:8005 in your browser.
Lento automatically blocks access to sensitive directories (src/, views/, data/, tests/, vendor/). No .htaccess or Linux permissions needed — everything is handled by PHP.
Your main entry file should define LENTO_ENTRY before loading the framework.
See index.php and the examples/ directory for complete working apps.
examples/url-shortener/— Complete URL shortener with zero closuresexamples/simple-gallery/— Image gallery with file uploads and APIexamples/secret-poll/— Anonymous polling + secret admirer letters with admin dashboardexamples/ai-chatbot/— ChatGPT-like interface with persistent SQLite history and OpenAI-compatible models
MIT
