Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lento — PHP Microframework for Absolute Beginners

Lento PHP Framework — Cassava fritter mascot with laptop and lontong balap

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.

Features

  • 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 GatesrequireAuth() protects routes declaratively
  • Duplicate PreventionfailIfFound() stops double submissions
  • File Uploads & Remote Downloadsupload() and download() handle files in one chain
  • JSON APIsjson() responses and json_input() for modern frontends
  • AI Integrationopenai() 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

Quick Start

<?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();

Why Lento? (Native PHP vs Framework)

Lento makes web development approachable by handling the "boring stuff" for you. Here is how it compares to writing plain PHP.

1. Routing

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');

2. Database & Migrations

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']);

3. Form Handling & Security

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>

4. JSON APIs

Native PHP:

header('Content-Type: application/json');
echo json_encode(['status' => 'ok', 'data' => $users]);
exit;

Lento:

$app->get('/api/users')->db('users')->all()->json();

5. File Uploads

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();

6. Authentication & CSRF Protection

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>

Declarative Routing (No Closures!)

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();

Available Chain Methods

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}', ...])

Special Interpolation Tokens

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();

Security & Sessions

CSRF Protection

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>

Sessions & Auth

  • login([...]) stores user data in the session
  • logout() clears it
  • requireAuth('/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.

Escaping Guide

  • request(), get(), and post() escape input by default
  • Database data and variables passed to render() are raw
  • Escape template output manually with escape() or htmlspecialchars()
<li><?= escape($photo['title']) ?></li>

API & Remote Integrations

Lento works well for JSON APIs and modern frontends:

  • json() returns API responses
  • json_input() reads raw JSON bodies
  • openai() connects to OpenAI-compatible APIs declaratively
  • download() 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.

Traditional Routing (Still Supported!)

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);
});

HTTP Methods

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}', ...);// Delete

Dynamic Routes

Capture URL segments with curly braces:

$app->get('/user/{id}')->db('users')->where('id = ?', ['{id}'])->first()->json();

Installation

Just download and include. No Composer needed!

define('LENTO_ENTRY', true);
require 'src/Lento.php';

Start Server

From the root directory:

php -S 0.0.0.0:8005 index.php

Then open http://localhost:8005 in your browser.

Security

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.

Documentation

See index.php and the examples/ directory for complete working apps.

Examples

  • examples/url-shortener/ — Complete URL shortener with zero closures
  • examples/simple-gallery/ — Image gallery with file uploads and API
  • examples/secret-poll/ — Anonymous polling + secret admirer letters with admin dashboard
  • examples/ai-chatbot/ — ChatGPT-like interface with persistent SQLite history and OpenAI-compatible models

License

MIT

About

Lento - PHP Microframework for Absolute Beginners

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages