Il punto di ingresso: index.php
Tutto inizia da index.php, un file di poche righe che fa tre cose: carica l'autoloader di Composer, legge la configurazione (file .env e directory config/), e istanzia Mvc passandogli la ConfigCollection. Poi chiama $mvc->run() e la richiesta prende vita.
Questa separazione tra costruzione e avvio e intenzionale. Il costruttore prepara gli oggetti fondamentali — Request, Response, View, Router — senza effetti collaterali. Il metodo run() attiva i provider, connette il database, e risolve la richiesta. Se qualcosa va storto nella fase di boot, il framework puo reagire prima di tentare di risolvere una route.
Il costruttore: preparare il terreno
Il costruttore di Mvc inizializza gli error handler per primi. NativeErrorProvider registra un handler PHP che intercetta errori e warning nativi e li scrive nel file app.log. Subito dopo, WhoopsProvider registra Whoops come exception handler — in modalita sviluppo, Whoops fornisce stack trace dettagliati e interattivi nel browser; in produzione viene disattivato a favore di pagine di errore pulite.
L'ordine conta: gli error handler devono essere attivi prima di qualsiasi altra inizializzazione. Se il database fallisce o una configurazione e errata, vogliamo un messaggio di errore leggibile, non un fatal error di PHP.
Poi vengono creati gli oggetti HTTP. Request incapsula la richiesta corrente: metodo, URI, parametri GET/POST, header, body. View riceve il riferimento a Mvc per accedere alla configurazione durante il rendering. Response riceve la View per poter renderizzare le pagine. Router riceve l'intero Mvc perche deve orchestrare la risoluzione delle route e il dispatch ai controller.
Infine, self::$mvc = $this rende l'istanza accessibile globalmente. La funzione helper mvc() restituisce Mvc::$mvc, permettendo a qualsiasi parte del codice di accedere a Request, Response, Config e agli altri servizi senza dependency injection esplicita. E un Service Locator — un compromesso pragmatico: non ha l'eleganza della pura DI, ma evita di passare sei parametri a ogni costruttore in un framework dove il container non e un full-blown DI container.
La sequenza di boot in run()
Il metodo run() avvia i servizi nell'ordine in cui dipendono l'uno dall'altro. Ogni step ha una ragione precisa per la sua posizione nella sequenza:
- SessionStorage — primo, perche tutto il resto (CSRF, auth, flash messages) dipende dalla sessione. Il singleton configura cookie sicuri (HttpOnly, Strict mode, Secure su HTTPS, SameSite=Lax) e avvia la sessione PHP
- EncryptionService — secondo, perche valida la
APP_KEYimmediatamente. Se manca o e malformata, l'applicazione si ferma qui con un messaggio chiaro, prima di qualsiasi query al database o rendering di view - DatabaseProvider — crea la connessione PDO attraverso il
DatabaseDriverFactory. Se la connessione fallisce, il provider gestisce l'errore in base all'ambiente: in debug mostra l'eccezione, in produzione redirige a una pagina di errore. Il PDO viene salvato in$this->pdoper essere accessibile globalmente - ORM Runtime — riceve il PDO e il nome del driver (mysql, pgsql, sqlite, mariadb). Questo registry e il punto di verita per tutti i componenti ORM: modelli, query builder, migrazioni. Separarlo dal DatabaseProvider permette di cambiare driver senza modificare il codice ORM
- CacheManager — inizializzato con la configurazione da
config/cache.php. Supporta cache file-based con invalidazione per tag (tabella). Deve venire dopo il database perche alcune operazioni di cache possono richiedere query - SmtpProvider — carica le credenziali SMTP dall'ambiente e crea il trasporto mail. Viene dopo il database perche in alcuni scenari i template email possono accedere a dati dal database
- CsrfService — ultimo prima del routing, genera il token CSRF se non ne esiste uno in sessione. Dipende da SessionStorage (per salvare il token) e da EncryptionService (per la chiave HMAC)
Risoluzione della route e dispatch
Dopo il boot dei provider, $this->router->resolve() prende il controllo. Il Router legge il metodo HTTP e l'URI dalla Request, cerca un match nella route collection (caricata dagli attributi dei controller o dalla cache), applica i middleware globali e poi quelli specifici della route, e infine chiama il metodo del controller.
L'intera risoluzione e avvolta in un try/catch per Throwable. Se il controller o un middleware lanciano un'eccezione, ExceptionHandler::handle() la intercetta, la mappa a un codice HTTP appropriato (404 per NotFoundException, 422 per ValidationException, 401 per UnauthorizedException), e genera la risposta di errore — JSON per le API, pagina HTML per il web.
Dopo la risoluzione (riuscita o fallita), $this->response->send() invia gli header HTTP e il body al client. La Response accumula header e contenuto durante tutto il ciclo, e li emette in un unico momento alla fine. Questo permette ai middleware di modificare la risposta anche dopo che il controller ha scritto il suo output.
Il pattern Provider
I Provider sono classi con un metodo register() che incapsulano la logica di inizializzazione di un servizio. DatabaseProvider gestisce la connessione PDO con error handling specifico per ambiente. SmtpProvider configura il trasporto mail. WhoopsProvider registra l'exception handler.
Il vantaggio del pattern Provider e l'isolamento: se la configurazione SMTP cambia, modifico solo SmtpProvider. Se aggiungo un nuovo servizio (un client Redis, un message broker), creo un nuovo Provider e lo inserisco nella sequenza di boot al punto giusto. Ogni provider sa come inizializzare il suo servizio e come gestire i fallimenti — il run() non deve preoccuparsi dei dettagli.
Perche non un vero DI Container
Framework come Laravel e Symfony usano container di dependency injection completi con binding, risoluzione automatica e autowiring. Soft PHP MVC usa un approccio piu semplice: un Service Locator (Mvc::$mvc) con provider manuali. La ragione e pragmatica: un DI container aggiunge complessita che si giustifica in applicazioni con centinaia di servizi e team di decine di sviluppatori. In un framework personale con una dozzina di servizi, sapere esattamente cosa viene inizializzato, in che ordine e perche, vale piu dell'eleganza del container automatico.
Il compromesso e consapevole: il codice dipende dall'helper mvc() invece che da interfacce iniettate. Se un giorno servira un container, la migrazione sara graduale — i provider gia incapsulano la logica di creazione, basterebbe registrarli in un container invece che chiamarli manualmente nel run().