php_http_apps_todo (arkenidar) — stesso stile dei tour precedenti<?php header('Location: router.php');
Tutta l'app entra da un unico punto: index.php rimanda subito a router.php con un redirect HTTP. Nessun framework, nessun bootstrap complesso — solo l'header Location.
@session_start();
try {
if (isset($_SERVER['REQUEST_URI']))
return serve_request($_REQUEST);
} catch (Exception $e) {
echo 'exception:' . $e->getMessage();
}
Una sola funzione, serve_request(), riceve tutta la richiesta ($_REQUEST = GET+POST). Il try/catch attorno a tutto è la rete di sicurezza dell'intera app.
$route = (string) @$request_variables['r'];
if (isset($routes[$route])) {
($routes[$route])($request_variables);
} else if (function_exists("routes_$route")) {
$function_name = "routes_$route";
$function_name($request_variables);
} else
echo 'RouteNotFound';
Idea chiave: la rotta arriva come parametro ?r=.... Niente regex di URL matching — il nome della rotta diventa direttamente il nome della funzione PHP da chiamare (routes_todo_list, routes_todo_add...). Minimale e diretto, nello spirito "esplicito, niente magia" che apprezzi.
require_once '../routes/routes_todo.php';
require_once '../routes/routes_user.php';
$routes[''] = function ($request_variables) {
redirect_to('todo_list');
};
Questo file aggrega i moduli di rotte e definisce le poche eccezioni esplicite (qui: rotta vuota → redirect a todo_list). Il resto delle rotte è implicito: basta che la funzione routes_<nome> esista.
function routes_todo_list(array $request_variables): void
{
require_login();
require_once '../manager/todo_manager.php';
$user_id = get_logged_in_user_id();
todo_render($user_id);
}
Pattern ripetuto in ogni rotta: autenticazione → manager → azione. La rotta non sa nulla di HTML o SQL: delega tutto al manager.
function routes_todo_list_json_get(array $request_variables): void
{
require_login();
require_once '../manager/todo_manager.php';
$user_id = get_logged_in_user_id();
json_response(todo_list($user_id));
}
Questo è il cuore del nome "php_http": la stessa azione (todo_list) può rispondere come HTML (routes_todo_list) o come JSON (routes_todo_list_json_get), semplicemente cambiando la funzione di risposta finale. Niente duplicazione della logica.
function json_response(mixed $data): void
{
header_content_type('application/json');
echo json_encode($data, JSON_PRETTY_PRINT);
}
function redirect_to(string $route): void
{
header_location("?r=$route");
}
Funzioni piccole e con un solo scopo: impostare header, fare redirect, serializzare JSON. Nessuna classe Response — solo funzioni che parlano direttamente l'HTTP.
function is_logged_in(): bool
{
return isset($_SESSION['user']);
}
function require_login(): void
{
if (!is_logged_in()) {
redirect_to('user_login_form');
exit();
}
}
Non c'è middleware: ogni rotta protetta chiama require_login() esplicitamente come prima riga. È più verboso di un middleware automatico, ma rende visibile, rotta per rotta, cosa richiede autenticazione.
function todo_add(string $description, int $user_id): void
{
$description = trim($description);
if ($description == '') return;
$todo = R::dispense('todos');
$todo->description = $description;
$todo->state = 0;
$todo->user_id = $user_id;
R::store($todo);
}
Il manager è lo strato che conosce il dominio ("todo") e parla con il database tramite RedBeanORM (R::dispense/R::store/R::load/R::trash). La rotta non vede mai SQL.
function todo_remove(int $id, int $user_id): void
{
$todo = R::load('todos', $id);
if ((int) $todo->user_id !== $user_id) {
return; // Unauthorized
}
R::trash($todo);
}
Ogni operazione su un todo verifica che appartenga all'utente loggato — un controllo di autorizzazione minimale ma presente ad ogni mutazione, non solo al login.
require_once $current_directory . 'rb-sqlite.php';
R::setup('sqlite:' . $current_directory . 'todo_db.sqlite');
// R::setup('pgsql:host=127.0.0.1;port=5432;dbname=database1;', ...);
Un solo punto di configurazione per il database. SQLite di default (zero installazione), con PostgreSQL pronto come alternativa commentata — coerente con la filosofia "minimal, dependency-free" quando possibile.
$_ = function ($name) use (&$template_variables) {
return htmlspecialchars($template_variables[$name]);
}; // variabile, escapata
$_u = function ($name) use (&$template_variables) {
return $template_variables[$name];
}; // variabile, non escapata
ob_start();
require $template_file;
$produced = ob_get_contents();
ob_end_clean();
Niente Twig/Blade: il "motore di template" è una funzione che usa output buffering puro di PHP più due helper ($_ per escape automatico, $_u per HTML non escapato quando serve). Esplicito e ispezionabile in poche righe.
if (isset($wrap) && count($wrap) > 0) {
$template_variables['wrapped_content'][] = $produced_template;
$produced_template = apply_template(array_pop($wrap), $template_variables);
}
Un template può dichiarare "avvolgimi con un altro template" ($_wrap_with('template_wrapper')). apply_template() si richiama ricorsivamente per comporre layout esterno + contenuto interno — un layout system fatto a mano, senza libreria.
$_wrap_with('template_wrapper');
...
<?php foreach ($_u('items') as $item) { ?>
<a href="?r=todo_detail&id=<?= $_e($item['id']) ?>">
...<?= $_e($item['description']) ?>
</a>
<?php } ?>
PHP puro mescolato a HTML — niente sintassi di template alternativa da imparare. L'escape ($_e) è sempre esplicito a livello di interpolazione, non automatico e invisibile.
checkbox.addEventListener('change', function(event) {
fetch('?r=todo_update_state', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'id=' + this.dataset.id + '&state=' + (this.checked ? 1 : 0)
})
});
Qui il client (JS vanilla, niente DHTML5 framework) chiama la stessa rotta PHP via fetch invece che con un form-submit a pagina intera. Stesso router, due modalità di interazione: navigazione classica e AJAX puntuale.
Per GET ?r=todo_list:
router.php
→ serve_request()
→ routes_todo_list() [routes_todo.php]
→ require_login() [response_utilities.php]
→ todo_render($user_id) [todo_manager.php]
→ todo_list($user_id) → R::findAll(...)
→ apply_template(...) [lib_template.php]
→ wrap con template_wrapper.php
Cinque strati, ciascuno con una responsabilità sola: routing, autorizzazione, dominio, persistenza, presentazione. Nessuno strato sa come è fatto quello sopra o sotto di lui — comunicano solo per parametri e valori di ritorno.
Quello che rende "applicato" questo esempio rispetto a un tour di sintassi: vedi perché certe scelte di PHP (funzioni globali, array associativi, closure, output buffering) diventano gli strumenti con cui si costruisce un'architettura — routing, MVC-lite, ORM minimale — senza bisogno di un framework.
Il repository: github.com/arkenidar/php_http_apps_todo