PHP Applicato

tour sul codice reale di php_http_apps_todo (arkenidar) — stesso stile dei tour precedenti

1. Il punto d'ingresso

www/index.php
<?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.

2. Il vero front controller

www/router.php
@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.

3. Routing per nome di funzione

www/router.php
$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.

4. Registro delle rotte

routes/_router.php
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.

5. Una rotta GET semplice

routes/routes_todo.php
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.

6. La stessa rotta, due risposte

routes/routes_todo.php
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.

7. Utility di risposta

routes/response_utilities.php
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.

8. Autenticazione come guardia esplicita

routes/response_utilities.php
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.

9. Il manager: logica + persistenza

manager/todo_manager.php
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.

10. Controllo accessi a livello di riga

manager/todo_manager.php
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.

11. Bootstrap del database

db/red-bean-orm-use.php
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.

12. Template: funzione, non motore

templates/lib_template.php
$_ = 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.

13. Composizione: wrapper di template

templates/lib_template.php
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.

14. Un template concreto

templates/todo/template_todo_list.php
$_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.

15. Frontend leggero: fetch invece di submit

templates/todo/template_todo_list.php
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.

16. Mettere tutto insieme: il flusso di una richiesta

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.

17. Per chiudere

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