Guida completa alla progettazione, realizzazione e test di un'applicazione web moderna
Categoria: Sviluppo Web | Livello: Intermedio-Avanzato
Realizzare un'applicazione web oggi non significa solo scrivere codice. Significa progettare un ecosistema: stack tecnologico, architettura, sicurezza, SEO, containerizzazione, privacy e documentazione. Questa guida ti accompagna attraverso ogni fase del ciclo di vita di un progetto web moderno, sicuro e scalabile.
1. Scelta dello stack tecnologico
La scelta dello stack deve basarsi su requisiti concreti: tipo di applicazione, competenze del team, scalabilità prevista e costi di infrastruttura. Non esiste uno stack universalmente migliore: esiste lo stack giusto per il problema giusto.
Stack per tipologia di applicazione
| Tipologia applicazione | Stack consigliato | Perché |
|---|---|---|
| Blog o portfolio | LEMP + PHP + HTML/CSS vanilla | Massima semplicità, nessun framework frontend, deploy immediato, costo infrastruttura quasi zero |
| Gestionale o CRM | LEMP + PHP (Laravel/Symfony) + MariaDB + Tailwind | CRUD complessi, autenticazione, ruoli, report: i framework PHP offrono scaffolding pronto |
| E-commerce | LEMP + PHP (WooCommerce/Sylius) + MariaDB/PostgreSQL + Redis | Carrello, catalogo, pagamenti richiedono transazioni sicure e caching aggressivo |
| Single Page App (SPA) | Node.js (Express/Nest) + React/Vue + MongoDB o PostgreSQL | Tanta interattività lato client, API REST/GraphQL, stato condiviso in tempo reale |
| Realtime (chat, notifiche) | Node.js + Socket.io + Redis + PostgreSQL | WebSocket, eventi in tempo reale, pub/sub con Redis, I/O non bloccante |
| Data-heavy / Analytics | Python (Django/FastAPI) + PostgreSQL + Redis + Celery | Elaborazione dati, machine learning, task asincroni, librerie scientifiche mature |
| CMS o portale contenuti | WordPress/PHP + MariaDB + Nginx + Redis | Ecosistema plugin sterminato, SEO nativa, gestione contenuti immediata, community enorme |
| API pura / Backend service | Node.js (Express/Fastify) o Go + PostgreSQL + Redis | Leggero, veloce, tipi statici (Go), concurrente, ideale per microservizi |
| PWA / Mobile-web | LEMP + PHP + Service Worker + IndexedDB | Funziona offline, installabile sul dispositivo, push notification, no app store |
Stack consigliato per progetti didattici e gestionali (LEMP + PHP)
| Componente | Tecnologia | Motivazione |
|---|---|---|
| OS | Linux (Ubuntu LTS) | Stabilità, sicurezza, costo zero, ecosistema maturo |
| Web Server | Nginx | Performance elevate, event-driven, configurazione semplice, eccellente per file statici |
| Database | MariaDB | Open source, drop-in replacement di MySQL, performance migliori, engine InnoDB |
| Backend | PHP 8+ | Maturo, vastissimo ecosistema, tipizzazione forte, performance eccellenti con PHP 8 |
| Frontend | HTML5 + CSS3 + JS vanilla | Nessuna dipendenza heavy, payload minimo, accessibilità nativa |
| Container | Docker + Docker Compose | Riproducibilità, isolamento, IaC leggero |
Perché PHP 8+ oggi?
PHP 8 ha introdotto JIT compilation, named arguments, attributes, union types e match expression. La curva di apprendimento è bassa, la documentazione sterminata e l'ecosistema (Composer, Symfony, Laravel) maturo. Per applicazioni didattiche e gestionali di media complessità, PHP offre il miglior rapporto produttività/performance.
Criteri di scelta pratici
Quando valuti uno stack, poniti queste domande nell'ordine:
- Chi manterrà il codice tra 6 mesi? Se sei solo o in team piccolo, scegli tecnologie che conosci bene, non le ultime mode.
- Quanto traffico prevedi? Un LEMP ben configurato regge centinaia di migliaia di request/giorno. Non serve Kubernetes per un blog con 100 visitatori.
- Quanto è critico il dato? Transazioni finanziarie richiedono un database ACID (PostgreSQL o MariaDB). Per log e analytics va bene anche MongoDB.
- Devi integrarti con servizi esterni? Controlla che lo stack scelto abbia SDK o API client maturi per i servizi che userai (pagamenti, email, cloud storage).
- Qual è il costo di infrastruttura? PHP + MySQL si può ospitare su un VPS da 5€/mese. Node.js + MongoDB spesso richiede più risorse. Python + Celery + Redis può richiedere più servizi separati.
2. Fase di progettazione
Funzionalità principali e secondarie
Prima di scrivere una riga di codice, definisci il perimetro del progetto con una matrice MUST/SHOULD/COULD/WON'T (MoSCoW). Questo evita lo scope creep: la tendenza delle richieste a espandersi durante lo sviluppo, facendo slittare le tempistiche. Ogni funzionalità deve avere una risposta chiara: "è indispensabile per il lancio?" Se no, finisce in COULD o WON'T per la versione 1.
La tecnica MoSCoW funziona meglio se ogni funzionalità ha una stima di tempo associata. Quando il totale delle MUST supera le settimane disponibili, devi prioritizzare: taglia funzionalità, non tagliare qualità (test, sicurezza, documentazione).
MUST (MVP):
- Gestione utenti (registrazione, login, logout)
- CRUD delle risorse principali
- Ricerca e filtri
- Pagine responsive
SHOULD:
- Pannello admin
- API REST
- Cache lato server
- Export dati
COULD:
- i18n multi-lingua
- Tema chiaro/scuro
- Notifiche email
- Dashboard statistiche
WON'T (v1):
- Realtime chat
- Marketplace multi-tenant
Wireframe o mockup dell'interfaccia
Prima di scrivere CSS, realizza wireframe. Il wireframe serve a validare l'usabilità prima di investire tempo nel design grafico: è molto più economico spostare un bottone su carta che riscrivere CSS e JS dopo. Strumenti consigliati:
- Balsamiq — wireframe a bassa fedeltà, veloce. Utile per sessioni di brainstorming con stakeholder non tecnici.
- Figma — mockup interattivi ad alta fedeltà. Permette di simulare clic, transizioni e stati (hover, loading, errore) prima di scrivere codice.
- Penna e carta — per sessioni di brainstorming rapide. La via più veloce per buttare giù idee.
Un buon wireframe definisce:
- Gerarchia delle informazioni (header, main, sidebar, footer) — cosa sta sopra la piega, cosa richiede scroll
- Flussi di navigazione (click → pagina → feedback) — ogni azione deve avere un esito visibile
- Breakpoint responsive (mobile, tablet, desktop) — non aspettare la fine per pensare ai dispositivi
- Stati — carica, vuoto, errore, edge case. Una tabella senza dati non deve essere uguale a una tabella con dati.
- Pattern di input — form, filtri, ricerca: dove l'utente inserisce dati, il feedback deve essere immediato
Un errore comune è saltare i wireframe e partire direttamente dal codice. Il risultato è che il layout viene modificato decine di volte, generando CSS sempre più complessi e difficili da mantenere. Un'ora di wireframe risparmia giorni di refactoring CSS.
Diagrammi UML/ASCII per l'architettura
I diagrammi UML servono a comunicare la struttura del progetto a chiunque (incluso il te stesso del futuro). Non devono essere perfetti o UML-standard: devono essere chiari. Il principio è: "un'immagine vale mille parole di documentazione". Un diagramma ERD ti fa capire la struttura del database in 10 secondi: leggere 100 righe di SQL per ricostruire le relazioni richiede ore.
I tre diagrammi indispensabili per qualsiasi progetto web sono: use case (chi fa cosa), entità-relazioni (come sono organizzati i dati), e deployment (come è distribuita l'infrastruttura).
Diagramma dei casi d'uso (Use Case)
┌──────────────────┐
│ │
┌───────────┤ Visitatore ├──────────┐
│ │ │ │
│ └────────┬─────────┘ │
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Navigare │ │Cercare │ │Leggere │
│pagine │ │contenuti│ │articoli │
└─────────┘ └─────────┘ └─────────┘
┌──────────────────┐
│ │
┌─────────┤ Amministratore ├─────────┐
│ │ │ │
│ └──────────────────┘ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Gestione │ │Pubblica │ │Modera │ │Stats │
│utenti │ │contenuti│ │commenti │ │dashboard│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Diagramma entità-relazioni (ERD)
┌─────────────────┐ ┌───────────────────┐
│ utenti │ │ articoli │
├─────────────────┤ ├───────────────────┤
│ id (PK) │──┐ │ id (PK) │
│ nome │ │ │ titolo │
│ email (UNIQUE) │ └───>│ slug (UNIQUE) │
│ password_hash │ │ contenuto │
│ ruolo │ │ autore_id (FK) │
│ created_at │ │ categoria_id (FK) │
│ stato │ │ published_at │
└─────────────────┘ │ created_at │
└───────────────────┘
│
┌─────────┴──────────┐
│ │
┌──────────▼──────┐ ┌──────────▼──────┐
│ categorie │ │ tag │
├─────────────────┤ ├─────────────────┤
│ id (PK) │ │ id (PK) │
│ nome │ │ nome │
│ slug (UNIQUE) │ │ slug (UNIQUE) │
│ descrizione │ │ │
└─────────────────┘ └─────────────────┘
Diagramma di deployment (IaC)
┌─────────────────────────────────────────────────────┐
│ Cloudflare │
│ ┌─────────────────────────────────────────────┐ │
│ │ DNS, CDN, DDoS protection, SSL/TLS │ │
│ └─────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────┘
│ HTTPS
┌────────────────────────▼────────────────────────────┐
│ Reverse Proxy (Nginx) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Terminazione SSL, caching, routing │ │
│ └─────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────┘
│
┌────────────────────────▼────────────────────────────┐
│ Docker Compose Stack │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ Nginx │ │ PHP-FPM │ │ MariaDB │ │
│ │ (ro volume) │ │ (rw volume) │ │ (volume) │ │
│ └──────────────┘ └──────────────┘ │ dati │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────┘
3. Importanza di tecnologie moderne e responsive
Un'applicazione web deve funzionare su qualsiasi dispositivo. Non è un optional: Google penalizza i siti non mobile-friendly con un ranking più basso e, soprattutto, gli utenti abbandonano un sito che non funziona bene sul telefono. In Italia oltre il 60% del traffico web arriva da dispositivi mobili: ignorare il responsive significa ignorare più della metà dei tuoi potenziali visitatori.
CSS Grid e Flexbox
I layout basati su CSS Grid e Flexbox hanno sostituito i vecchi metodi basati su float, tabelle e framework ingombranti come Bootstrap. I vantaggi sono concreti: meno codice, più leggibilità, nessuna dipendenza esterna. Grid è ideale per la macro-struttura della pagina (header, sidebar, main, footer), mentre Flexbox eccede per i componenti interni (barre di navigazione, card allineate, form).
/* Layout responsive senza framework */
.app-layout {
display: grid;
grid-template-columns: 1fr; /* Mobile: singola colonna */
grid-template-rows: auto 1fr auto; /* Header auto, main elastico, footer auto */
min-height: 100vh; /* Sempre almeno tutta l'altezza della viewport */
}
@media (min-width: 768px) {
.app-layout {
grid-template-columns: 250px 1fr; /* Tablet/desktop: sidebar fissa + contenuto fluido */
}
}
La proprietà min-height: 100vh è fondamentale: senza di essa, se la pagina ha poco contenuto, il footer "galleggia" a metà schermo invece di stare in fondo. Con lo sticky footer reso da Grid, il main occupa tutto lo spazio rimanente.
Mobile-first design
Mobile-first significa progettare prima per lo schermo più piccolo, poi aggiungere complessità via via che lo schermo cresce. Perché? È più facile aggiungere elementi su schermi grandi che toglierli su schermi piccoli. In CSS si traduce in media query solo per breakpoint superiori (min-width), mai max-width:
/* Base: mobile — 100% larghezza, padding contenuto */
.card { width: 100%; padding: 1rem; }
/* Tablet */
@media (min-width: 768px) {
.card { width: 50%; } /* Due colonne */
}
/* Desktop */
@media (min-width: 1024px) {
.card { width: 33.333%; } /* Tre colonne */
}
I breakpoint non vanno scelti su dispositivi specifici (iPhone, iPad) ma sul contenuto: 768px è il punto in cui due colonne stanno comodamente, 1024px è dove ne entrano tre. Usare i pixel dei dispositivi come riferimento porta a breakpoint arbitrari.
Vantaggi concreti
- SEO: Google premia i siti responsive con ranking più alto; penalizza chi ha pagine mobili separate o assenti
- Mantenimento: un unico codice base invece di app separate (web + Android + iOS). Una modifica si riflette ovunque
- Accessibilità: lo stesso contenuto raggiungibile da tutti, indipendentemente dal dispositivo
- Costi: nessuna manutenzione separata per mobile/desktop. Un team mantiene un solo progetto
- Performance: un sito responsive ben fatto è più veloce di un'app nativa o di un sito con redirect mobile
4. Controllo versioni con Git
Git è il fondamento di qualsiasi progetto software professionale. Senza versionamento, ogni modifica è rischiosa: non puoi tornare indietro, non sai chi ha cambiato cosa, non puoi fare esperimenti senza paura di rompere tutto. Git risolve tutti questi problemi e, se usato bene, diventa il tuo strumento di documentazione più potente.
Perché Git non è solo un backup
Un errore comune è usare Git come semplice "copia di sicurezza" (git add && git commit && git push una volta al giorno). Git è molto di più: è uno strumento di comunicazione per il team, un diario di bordo delle decisioni tecniche e un meccanismo di isolamento per sviluppare senza paura.
La differenza tra un progetto con commit fatti bene e uno con commit fatti male è la stessa che c'è tra un libro indicizzato e una pila di fogli: nel primo caso trovi quello che cerchi in secondi, nel secondo perdi ore.
Flusso di lavoro consigliato (Git Flow semplificato)
main → produzione (stable)
├── develop → integrazione
│ ├── feat/login → nuova funzionalità
│ ├── feat/ricerca → nuova funzionalità
│ └── fix/header-css → bug fix
└── release/1.0.0 → preparazione rilascio
Il ramo main contiene solo codice testato e funzionante. Tutto lo sviluppo avviene su branch separati: ogni funzionalità (feat/) o bug fix (fix/) ha il suo ramo, che viene unito a develop solo quando è completo e testato. I branch release/ servono a preparare il rilascio: qui si fanno solo gli ultimi ritocchi (versione, changelog, bug fix minori) prima di unire a main e taggare. Questo flusso garantisce che main sia sempre deployabile.
Convenzione dei commit (Conventional Commits)
feat: aggiunta autenticazione JWT
fix: corretto errore nel calcolo del prezzo
docs: aggiornata guida installazione
style: formattazione CSS header
refactor: estratto helper validazione
test: aggiunti test per CRUD utenti
chore: aggiornata dipendenza PHPUnit
La convenzione Conventional Commits non è burocrazia: un messaggio di commit ben scritto risponde a tre domande: "Cosa è stato cambiato?", "Perché?", "Ci sono effetti collaterali?". Il prefisso (feat:, fix:, docs:) permette di generare automaticamente il changelog e di decidere il numero di versione successivo (major, minor, patch) seguendo il Semantic Versioning. Un commit con messaggio "fix: corretto errore nel calcolo del prezzo" è molto più utile di "fix roba" quando tra sei mesi cerchi nel log cosa ha risolto un bug.
File .gitignore essenziale
.env
/vendor/
node_modules/
*.log
.DS_Store
/backup/
/uploads/
/.opencode/
Il .gitignore serve a tenere pulito il repository da file che non devono essere versionati. La regola è: tutto ciò che è generato (compilato, scaricato, configurato localmente) va ignorato. I file sensibili (.env con password) non devono MAI finire in Git. I file di backup, i log, le dipendenze scaricate da Composer o npm sono rigenerabili e quindi inutili nel repository. Un repository pulito si clona veloce, si aggiorna senza conflitti e non espone segreti.
5. Rispetto delle regole SEO
La SEO non è un'aggiunta finale: va progettata dall'inizio. Google esamina centinaia di fattori per posizionare un sito, ma molti di questi si decidono in fase di sviluppo: la struttura degli URL, la velocità di caricamento, i meta tag, i dati strutturati. Aggiungere SEO dopo aver già scritto tutto il codice significa dover rifare routing, template e spesso anche la struttura del database.
Perché la SEO è responsabilità dello sviluppatore
Molti pensano che la SEO sia compito del marketer o del content creator. In realtà, i fattori tecnici (velocità, URL canonici, dati strutturati, mobile-friendliness) sono tutti sotto il controllo dello sviluppatore. Un sito tecnicamente perfetto parte avvantaggiato: poi i contenuti di qualità fanno il resto. Trascurare la SEO tecnica in fase di sviluppo significa costruire una macchina veloce ma senza volante.
Struttura URL RESTful
❌ /pagina.php?id=45
✅ /articoli/guida-seo-php
❌ /categoria.php?cat=3
✅ /categorie/programmazione
I motori di ricerca leggono l'URL per capire il contesto della pagina. Un URL come /pagina.php?id=45 non dice nulla né a Google né all'utente. /articoli/guida-seo-php comunica immediatamente: "questo è un articolo che parla di SEO in PHP". Inoltre, gli URL descrittivi migliorano il click-through rate: un utente è più propenso a cliccare un link se capisce dove lo porterà.
Meta tag dinamici
<title><?= e($pageTitle) ?> | MioSito</title>
<meta name="description" content="<?= e($pageDescription) ?>">
<link rel="canonical" href="<?= e($canonicalUrl) ?>">
<meta property="og:title" content="<?= e($ogTitle) ?>">
<meta property="og:description" content="<?= e($ogDescription) ?>">
I meta tag sono la prima cosa che Google legge quando indicizza una pagina. Il title è il titolo che appare nei risultati di ricerca; la description è il sottotitolo. Se non li imposti dinamicamente per ogni pagina, Google mostrerà frammenti casuali del tuo contenuto. Il tag canonical è fondamentale per evitare contenuti duplicati: dice a Google qual è la versione "ufficiale" di una pagina quando la stessa risorsa è accessibile da URL diversi (es. con e senza trailing slash, con e senza parametri di tracking).
I tag Open Graph (og:) controllano come il tuo sito appare quando viene condiviso su Facebook, LinkedIn, Telegram e altri social. Senza di essi, la condivisione mostra solo l'URL nudo: con i tag OG mostra titolo, descrizione e immagine, aumentando drasticamente il tasso di clic.
Sitemap XML e robots.txt
<!-- sitemap.xml -->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.example.com/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
# robots.txt
User-agent: *
Allow: /
Sitemap: https://www.example.com/sitemap.xml
La sitemap XML elenca tutte le pagine del sito che vuoi siano indicizzate. È particolarmente utile se il sito ha pagine nuove che non sono ancora state scoperte dai link interni, o se hai pagine "orfane" (senza link entranti). Il robots.txt dice ai crawler quali parti del sito possono scansionare. Non va usato per bloccare l'indicizzazione (per quello serve il meta tag noindex), ma solo per evitare che Google sprechi budget di scansione su pagine di servizio, admin o API.
Dati strutturati (JSON-LD)
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "<?= e($title) ?>",
"datePublished": "<?= $date ?>",
"author": {
"@type": "Person",
"name": "Mario Rossi"
}
}
</script>
I dati strutturati in formato JSON-LD (consigliato da Google) aggiungono un "cartellino informativo" che Google legge per capire esattamente cosa contiene la pagina. Il risultato sono i rich snippet: stelle di valutazione, prezzo, tempo di ricetta, eventi, FAQ. I rich snippet aumentano il tasso di clic fino al 30% perché occupano più spazio visivo nei risultati e comunicano più informazioni a colpo d'occhio. Ogni tipologia di contenuto ha uno schema specifico su schema.org: Article, Product, Recipe, Event, FAQPage, LocalBusiness. Scegli quello più pertinente e compila i campi obbligatori.
6. Rispetto delle prescrizioni GDPR
Il GDPR (Regolamento Generale sulla Protezione dei Dati, EU 2016/679) non è solo un obbligo legale: è una garanzia di fiducia per gli utenti. Un sito conforme al GDPR trasmette professionalità. Inoltre, le multe per non conformità possono arrivare fino al 4% del fatturato annuo globale.
Cosa implementare
- Cookie banner con consenso esplicito prima di caricare tracker. Il consenso deve essere "prestato liberamente, specifico, informato e inequivocabile": niente banner con "continuando a navigare accetti", niente caselle pre-spuntate.
- Informativa privacy chiara e accessibile da ogni pagina (footer link). Deve specificare: chi tratta i dati, quali dati raccoglie, per quanto tempo li conserva, con chi li condivide e quali diritti ha l'utente.
- Pulsante "Elimina account" e diritto all'oblio. L'utente deve poter cancellare i propri dati e la propria utenza con un'azione autonoma, senza dover scrivere una email.
- Log delle accettazioni del consenso (chi, quando, cosa). Per dimostrare al Garante che il consenso è stato validamente acquisito, devi conservare una registrazione di ogni accettazione.
- Crittografia delle password con bcrypt/Argon2. Le password non devono MAI essere salvate in chiaro o con hash deboli (MD5, SHA1). PHP offre
password_hash()con bcrypt di default e supporta Argon2 dal PHP 7.2. - Minimizzazione dei dati: raccogli solo i dati strettamente necessari. Non chiedere data di nascita se ti serve solo l'email per la newsletter.
Perché il consenso deve essere registrato
Il principio di responsabilizzazione (accountability) del GDPR richiede che tu sia in grado di dimostrare la conformità. Se il Garante ti chiede "mostrami quando l'utente ha accettato i cookie", devi poter produrre un log con timestamp e dettagli. La tabella cookie_consents serve esattamente a questo. Nota che salviamo l'hash dell'IP, non l'IP in chiaro: è una buona pratica di pseudonimizzazione.
Esempio di cookie banner minimal
function getCookieConsent() {
const consent = localStorage.getItem('cookie_consent');
if (!consent) {
document.getElementById('cookie-banner').style.display = 'block';
}
}
function acceptCookies(category) {
localStorage.setItem('cookie_consent', JSON.stringify({
necessary: true,
analytics: category === 'all',
marketing: category === 'all',
timestamp: new Date().toISOString()
}));
document.getElementById('cookie-banner').style.display = 'none';
}
Il cookie banner qui sopra separa i cookie in tre categorie: strettamente necessari (sempre attivi), analytics (facoltativi) e marketing (facoltativi). L'utente può scegliere se accettare solo i necessari o anche gli altri. Il consenso viene salvato in localStorage e letto a ogni pagina.
Database delle accettazioni
CREATE TABLE cookie_consents (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_hash VARCHAR(64) NOT NULL,
user_agent TEXT,
consent JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
La colonna ip_hash usa un hash SHA-256 dell'IP: non possiamo risalire all'IP originale, ma possiamo verificare che per quell'hash esista un consenso valido. Il campo consent salva in JSON l'esatta configurazione accettata. Con un indice su created_at, le query di controllo sono veloci anche con milioni di righe.
7. Contatore visite, statistiche e mappa di provenienza
Conoscere i propri visitatori è fondamentale per capire cosa funziona e cosa no. Google Analytics è la soluzione più comune, ma ha due problemi: richiede un cookie di terze parti (con implicazioni GDPR) e i dati sono di proprietà di Google. Costruire un sistema di tracciamento proprietario, anche semplice, ti dà il controllo completo dei dati e nessuna dipendenza esterna.
Privacy by design: hash dell'IP
Il sistema qui sotto non salva l'IP in chiaro ma un suo hash SHA-256. Questo permette di contare visitatori unici senza poter risalire all'indirizzo specifico. È un compromesso ragionevole tra utilità statistica e privacy: puoi sapere quante persone diverse visitano il sito, ma non puoi identificarle. Il campo country/city viene estratto tramite database GeoIP e permette di generare una mappa di provenienza.
Tracciamento semplice senza servizi esterni
CREATE TABLE page_visits (
id INT AUTO_INCREMENT PRIMARY KEY,
page_url VARCHAR(500) NOT NULL,
ip_hash VARCHAR(64),
country VARCHAR(4),
city VARCHAR(100),
user_agent TEXT,
referer TEXT,
visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_page (page_url(100)),
INDEX idx_date (visited_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Ogni visita viene registrata con la pagina visitata, la provenienza geografica, il referrer (da dove arriva l'utente) e il timestamp. Gli indici su page_url e visited_at sono essenziali per mantenere performanti le query di aggregazione quando la tabella raggiunge centinaia di migliaia di righe.
GeoIP con MaxMind GeoLite2
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
function getGeoLocation(string $ip): array
{
$reader = new Reader('/var/lib/GeoIP/GeoLite2-City.mmdb');
$record = $reader->city($ip);
return [
'country' => $record->country->isoCode,
'city' => $record->city->name,
'lat' => $record->location->latitude,
'lon' => $record->location->longitude,
];
}
?>
Il database gratuito GeoLite2 di MaxMind permette di ottenere paese, città, latitudine e longitudine da un indirizzo IP. La precisione non è perfetta (soprattutto per IP mobili o VPN), ma è più che sufficiente per generare una mappa a colori delle visite. Il database va scaricato e aggiornato periodicamente (MaxMind lo rilascia ogni primo martedì del mese). La latitudine e longitudine sono utili per visualizzare i visitatori su una mappa con clustering.
Dashboard statistiche
-- Visitatori unici giornalieri
SELECT DATE(visited_at) AS giorno, COUNT(DISTINCT ip_hash) AS visitatori
FROM page_visits
WHERE visited_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY giorno
ORDER BY giorno DESC;
-- Pagine più visitate
SELECT page_url, COUNT(*) AS hits
FROM page_visits
GROUP BY page_url
ORDER BY hits DESC
LIMIT 10;
Queste due query rispondono alle domande più comuni: "quante persone visitano il sito ogni giorno?" e "quali sono le pagine più popolari?". La prima usa COUNT(DISTINCT ip_hash) per contare visitatori unici, non visite totali. La seconda raggruppa per URL e conta gli hit per capire quali contenuti funzionano meglio. Aggiungendo una condizione WHERE referer LIKE '%google.%' puoi vedere quali pagine portano più traffico da Google, misurando così l'efficacia della SEO. Puoi anche creare query per paese (GROUP BY country) e per browser/OS (analizzando user_agent) per ottenere un quadro completo del tuo pubblico.
8. Containerizzazione, IaC, Cloudflare, reverse proxy e HTTPS
Containerizzare un'applicazione significa impacchettare il codice con tutte le sue dipendenze in un ambiente isolato e riproducibile. Il vantaggio principale è che funziona uguale su qualsiasi macchina: sul tuo portatile, sul server di produzione, sul computer di un collega. L'Infrastructure as Code (IaC) estende questo concetto all'intera infrastruttura: definisci in file di configurazione tutto ciò che serve (reti, volumi, servizi, variabili d'ambiente) invece di configurarlo a mano.
Perché containerizzare?
Senza container, le configurazioni dei server sono una fonte infinita di problemi: "sul mio computer funziona" è il mantra più odiato dagli sviluppatori. Con Docker, quello che funziona in locale funziona anche in produzione perché l'ambiente è identico. Inoltre, i container isolano le applicazioni: se un'applicazione ha un memory leak, non abbatte il server. Se devi aggiornare PHP per un progetto, non rischi di romperne un altro sulla stessa macchina.
Docker Compose (IaC)
services:
php:
build: .
volumes:
- ./www:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini:ro
environment:
DB_HOST: mariadb
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
mariadb:
condition: service_healthy
nginx:
image: nginx:1.27-alpine
volumes:
- ./www:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "80:80"
- "443:443"
depends_on:
- php
mariadb:
image: mariadb:10.11
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./setup/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
mysql_data:
Perché usare i volumi Docker
Nel file compose, nota la differenza tra volumi read-only (:ro) e read-write per i diversi servizi. Nginx riceve i file in sola lettura perché deve solo servirli: se un attaccante compromettesse Nginx, non potrebbe modificare i file. PHP-FPM invece ha bisogno di scrittura per gli upload e i log. MariaDB ha un volume dedicato (mysql_data) per i dati del database, separato dal codice: così puoi ricreare i container senza perdere dati.
Questa separazione segue il principio del privilegio minimo: ogni servizio ha solo i permessi che gli servono, niente di più.
File .env (mai committato)
# .env — Copia in .env.local e personalizza
APP_NAME="Mia Web App"
APP_ENV=production
APP_DEBUG=false
DB_HOST=mariadb
DB_PORT=3306
DB_NAME=myapp
DB_USER=myapp_user
DB_PASSWORD=s3cur3P@ss!
# Cloudflare
CLOUDFLARE_API_TOKEN=...
CLOUDFLARE_ZONE_ID=...
# Admin
ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme!
Cloudflare + Reverse Proxy
Cloudflare funge da reverse proxy tra l'utente e il tuo server. Quando un utente richiede https://example.com, la richiesta arriva prima a Cloudflare, che la analizza, applica regole di sicurezza e caching, e poi la inoltra al tuo server. Questo ha diversi vantaggi:
- Nasconde l'IP del server — un attaccante deve prima superare Cloudflare prima di arrivare al tuo server.
- Assorbe attacchi DDoS — Cloudflare ha un'infrastruttura globale in grado di assorbire attacchi da centinaia di Gbps.
- Caching intelligente — le risorse statiche (CSS, JS, immagini) vengono servite dai datacenter Cloudflare più vicini all'utente, riducendo la latenza.
- SSL/TLS automatico — Cloudflare fornisce certificati gratuiti e gestisce il rinnovo automaticamente.
- WAF — Web Application Firewall che blocca le richieste maligne prima che arrivino al tuo server.
La configurazione Nginx deve però essere adattata per funzionare con Cloudflare: è necessario configurare real_ip_header e set_real_ip_from per far sì che PHP veda l'IP reale del visitatore e non l'IP dei server Cloudflare.
Configurazione Nginx per Cloudflare:
server {
listen 443 ssl http2;
server_name example.com;
# Certificati da Cloudflare Origin CA o certbot
ssl_certificate /etc/ssl/certs/cloudflare.pem;
ssl_certificate_key /etc/ssl/private/cloudflare-key.pem;
# Restituisci IP reale del visitatore (non Cloudflare)
real_ip_header CF-Connecting-IP;
set_real_ip_from 173.245.48.0/20;
# ... (tutti gli IP Cloudflare)
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
HTTPS obbligatorio
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
9. Review della sicurezza
La sicurezza non è una feature che si aggiunge alla fine: è un requisito trasversale che deve guidare ogni decisione tecnica. Ogni vulnerabilità ha un costo: diretto (dati persi, multe) o indiretto (reputazione, fiducia degli utenti). La buona notizia è che la maggior parte degli attacchi più comuni si previene con pratiche semplici e consolidate, non con strumenti complessi.
Perché queste minacce sono pericolose
Ogni riga della checklist corrisponde a una classe di attacco reale e diffusa:
- SQL Injection: un malintenzionato inserisce codice SQL in un input (es. modulo di login) per manipolare il database. Può leggere, modificare o cancellare l'intero database. La prevenzione è semplice: usa sempre prepared statement (PDO) invece di concatenare stringhe nelle query.
- XSS (Cross-Site Scripting): un attaccante inietta script JavaScript in una pagina che altri utenti visualizzeranno. Può rubare cookie di sessione, reindirizzare a siti malevoli o modificare il contenuto della pagina. La difesa è unica:
htmlspecialchars()su ogni output di dati utente. - CSRF (Cross-Site Request Forgery): un attaccante induce un utente autenticato a eseguire un'azione indesiderata (es. cambiare email, trasferire soldi) cliccando un link o visitando una pagina. La difesa: un token univoco in ogni form, verificato lato server.
- Password deboli: password salvate in chiaro o con hash deboli (MD5, SHA1) sono recuperabili in secondi con attacchi a forza bruta o rainbow table. Usa sempre
password_hash()con Argon2id o bcrypt. - Session hijacking: un attaccante ruba il cookie di sessione e si finge l'utente. La difesa è nei flag del cookie:
HttpOnly(non accessibile via JS),Secure(solo su HTTPS),SameSite=Strict(non inviato su richieste cross-site).
Checklist di sicurezza minima
| Area | Controllo | Strumento |
|---|---|---|
| SQL Injection | Prepared statement PDO | PHP PDO |
| XSS | htmlspecialchars() su tutti gli output | Funzione e() custom |
| CSRF | Token per ogni form | Session token |
| Password | Argon2id o bcrypt | password_hash() |
| Sessioni | HTTP-only, Secure, SameSite Strict | session_set_cookie_params() |
| Headers | CSP, HSTS, X-Frame-Options | Nginx + PHP |
| File upload | Validazione MIME reale | finfo_file() |
| Rate limiting | Nginx limit_req | Nginx |
| Accesso admin | .htpasswd o IP whitelist | Nginx auth_basic |
Esempio di security headers in Nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Ogni header ha uno scopo preciso:
- X-Frame-Options: impedisce che il tuo sito venga caricato dentro un iframe di un altro sito (clickjacking).
- X-Content-Type-Options: impedisce ai browser di "indovinare" il tipo MIME di una risorsa (MIME sniffing).
- Referrer-Policy: controlla quanta informazione viene passata nell'header Referrer quando si clicca un link verso un altro sito.
- Content-Security-Policy: la difesa più potente contro XSS. Dice al browser quali sorgenti sono autorizzate per script, style, immagini, font. Se un attaccante inietta script inline da un dominio non autorizzato, il browser li blocca.
- Permissions-Policy: disabilita API del browser non necessarie (fotocamera, microfono, geolocalizzazione).
- Strict-Transport-Security (HSTS): dice al browser di connettersi sempre via HTTPS, anche se l'utente digita http://. Il parametro
preloadpermette di essere inclusi nella lista preload di Chrome/Edge/Firefox.
10. Script e operazioni per backup
Il backup è l'ultima linea di difesa contro la perdita di dati. Non è una questione di "se" succederà qualcosa, ma di "quando": un errore umano (DROP TABLE invece di SELECT), un attacco ransomware, un guasto hardware. Avere backup funzionanti e testati è l'unica assicurazione contro la catastrofe.
La regola 3-2-1
La strategia minima accettabile è la regola 3-2-1: almeno 3 copie dei dati, su almeno 2 supporti diversi, di cui almeno 1 offsite. Tradotto in pratica:
- Copie: una produzione + un backup locale + un backup remoto
- Supporti: backup su disco locale + backup su cloud (S3, Backblaze B2, rsync.net)
- Offsite: il backup remoto deve essere in una località fisica diversa (altrimenti un incendio/alluvione distrugge tutto)
Perché backup di database e file sono separati
Il database e i file (upload, log, configurazioni) hanno cicli di vita diversi e richiedono strategie di backup diverse. Il database è dinamico (cambia continuamente) e va backupato con mysqldump in modalità --single-transaction per avere uno snapshot coerente senza bloccare le scritture. I file cambiano meno frequentemente ma occupano più spazio: un backup incrementale (tar differenziale o rsync) è più efficiente di uno completo ogni giorno.
Backup del database
#!/bin/bash
# backup-db.sh — Eseguito giornalmente via cron
set -euo pipefail
source .env
BACKUP_DIR="/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
mkdir -p "$BACKUP_DIR"
docker exec lemp_mariadb mysqldump \
--single-transaction \
--routines \
--triggers \
--events \
-u "$DB_USER" \
-p"$DB_PASSWORD" \
"$DB_NAME" \
| gzip > "$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"
# Elimina backup più vecchi di RETENTION_DAYS
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completato: ${DB_NAME}_${DATE}.sql.gz"
Backup dei file
#!/bin/bash
# backup-files.sh
BACKUP_DIR="/backup/files"
DATE=$(date +%Y%m%d_%H%M%S)
SOURCE_DIR="/var/www/html"
tar czf "$BACKUP_DIR/www_${DATE}.tar.gz" \
--exclude="node_modules" \
--exclude="vendor" \
--exclude=".git" \
--exclude="backup" \
"$SOURCE_DIR"
# Upload su cloud storage (opzionale)
# rclone copy "$BACKUP_DIR/www_${DATE}.tar.gz" myremote:backups/
Cron giornaliero
# /etc/cron.d/backup-app
0 3 * * * root /usr/local/bin/backup-db.sh
0 4 * * * root /usr/local/bin/backup-files.sh
11. Internazionalizzazione (i18n)
L'internazionalizzazione (i18n, abbreviato perché tra i ed n ci sono 18 lettere) non è solo per app multilingua. Anche se il tuo sito è solo in italiano, separare tutto il testo dal codice ha vantaggi enormi: le stringhe sono centralizzate (se devi cambiare "Accedi" in "Entra" lo fai in un punto solo), il codice è più leggibile, e i testi possono essere gestiti da non sviluppatori. Questa separazione aiuta l'IaC perché i testi diventano dati, non codice: possono essere montati come volume Docker separato e modificati senza toccare l'immagine del container.
Perché i18n va progettata all'inizio
Aggiungere traduzioni a un progetto già sviluppato è molto più costoso che progettarlo con i18n dal giorno uno. Se in tutto il codice trovi scritte come <h1>Benvenuto</h1> invece di <h1><?= __('welcome') ?></h1>, devi cacciare centinaia di stringhe nel codice, una per una. Se parti con la funzione __() fin dall'inizio, ogni nuova pagina esce già pronta per la traduzione.
Struttura delle traduzioni
<?php
// lang/it.php
return [
'site_title' => 'Mia Applicazione Web',
'welcome_message' => 'Benvenuto, {name}!',
'login' => 'Accedi',
'logout' => 'Esci',
'error_404' => 'Pagina non trovata',
'error_403' => 'Accesso negato',
'form_submit' => 'Invia',
'form_cancel' => 'Annulla',
];
<?php
// lang/en.php
return [
'site_title' => 'My Web Application',
'welcome_message' => 'Welcome, {name}!',
'login' => 'Login',
'logout' => 'Logout',
'error_404' => 'Page not found',
'error_403' => 'Access denied',
'form_submit' => 'Submit',
'form_cancel' => 'Cancel',
];
Funzione helper
<?php
function __(string $key, array $params = []): string
{
static $lang = null;
if ($lang === null) {
$locale = $_SESSION['lang'] ?? 'it';
$file = __DIR__ . "/lang/{$locale}.php";
$lang = file_exists($file) ? require $file : [];
}
$text = $lang[$key] ?? $key;
foreach ($params as $k => $v) {
$text = str_replace("{{$k}}", $v, $text);
}
return $text;
}
?>
Uso nelle viste
<h1><?= __('welcome_message', ['name' => $user['nome']]) ?></h1>
<a href="/login"><?= __('login') ?></a>
Vantaggi per IaC
- Le traduzioni sono file JSON/php esterni → volume Docker separato
- Possono essere gestite da non sviluppatori (traduttori)
- Nessuna modifica al codice per cambiare testi
- Facilità di aggiungere nuove lingue senza toccare la logica
12. Documentazione approfondita
La documentazione non è un optional: è parte integrante del progetto. La differenza tra un progetto ben documentato e uno senza documentazione è la differenza tra un prodotto che può essere mantenuto e uno che verrà riscritto da capo dopo sei mesi. Documentare significa rispondere in anticipo alle domande che chiunque (incluso il te stesso del futuro) avrà.
I due tipi di documentazione
Ogni progetto ha bisogno di due tipi di documentazione: quella utente (come si usa l'applicazione) e quella tecnica (come funziona, come si installa, come si mantiene). La documentazione tecnica è quella che fa la differenza per gli sviluppatori. La regola è: "documenta il perché, non il cosa". Il "cosa" si vede dal codice. Il "perché" è la decisione che ha portato a scrivere quel codice in quel modo, e senza documentazione si perde.
Perché ogni file serve
- README.md — la prima cosa che chiunque legge. Deve rispondere in 30 secondi: "cos'è questo progetto?", "a cosa serve?", "come lo installo?".
- INSTALL.md — passo passo per far funzionare il progetto in locale. Niente assunzioni implicite. "Clona, copia .env.example, modifica, docker compose up".
- ARCHITECTURE.md — le decisioni architetturali più importanti: perché hai scelto PHP invece di Node.js, perché MariaDB invece di PostgreSQL, come sono organizzati i moduli.
- API.md — se il progetto espone API, ogni endpoint deve essere documentato con metodo, URL, parametri, esempio di request e response.
- DATABASE.md — schema ERD, spiegazione di ogni tabella, vincoli, indici, relazioni. Essenziale per chi deve fare query complesse o migrazioni.
- DEPLOY.md — procedura di rilascio: branch da usare, comandi, test da eseguire, rollback plan.
- TEST.md — come eseguire i test, cosa coprono, come aggiungerne di nuovi.
- SECURITY.md — policy di sicurezza, come segnalare vulnerabilità, controlli implementati.
- CHANGELOG.md — storico delle versioni con le modifiche principali. Generabile automaticamente dai commit Conventional Commits.
Struttura della documentazione
docs/
├── README.md # Panoramica del progetto
├── INSTALL.md # Guida all'installazione
├── ARCHITECTURE.md # Decisioni architetturali
├── API.md # Documentazione delle API
├── DATABASE.md # Schema ERD e spiegazione tabelle
├── DEPLOY.md # Procedura di deploy
├── TEST.md # Come eseguire i test
├── SECURITY.md # Policy di sicurezza
├── STYLEGUIDE.md # Convenzioni di codice
├── CHANGELOG.md # Storico modifiche
└── CONTRIBUTING.md # Linee guida per contribuire
README.md minimo
# Nome Progetto
Breve descrizione del progetto, del suo scopo e del contesto.
## Requisiti
- Docker 24+
- Docker Compose 2.20+
## Installazione rapida
git clone https://github.com/user/project.git cd project cp .env.example .env
Modifica .env con i tuoi valori
docker compose up -d
## Struttura del progetto
├── docker/ # Configurazioni Docker ├── www/ # Codice applicativo │ ├── assets/ # File statici (CSS, JS, img) │ ├── includes/ # Helpers e librerie │ ├── pages/ # Template delle pagine │ ├── data/ # Dati JSON (contenuti) │ └── index.php # Entry point ├── setup/ # Script di bootstrap ├── backup/ # Backup automatizzati └── docker-compose.yml
## Licenza
MIT
13. Sviluppo per fasi con milestone
Dividere il progetto in fasi riduce il rischio, permette test intermedi e fornisce visibilità continua. Il principio è: consegna valore in piccoli incrementi, non tutto alla fine. Ogni fase deve produrre qualcosa di funzionante e testabile, non solo codice "quasi finito".
Perché le milestone funzionano
Senza milestone definite, lo sviluppo segue la legge di Parkinson: "il lavoro si espande fino a occupare tutto il tempo disponibile". Una milestone è un contratto con te stesso: entro questa data, queste funzionalità devono essere complete e funzionanti. Se non ci arrivi, riduci l'ambito (non la qualità). Le milestone hanno anche un effetto psicologico positivo: completare una fase dà una sensazione di progresso e motiva a continuare.
Come definire buone milestone
Ogni milestone deve avere:
- Obiettivo chiaro: cosa deve essere vero al termine (es. "un utente può registrarsi e fare login")
- Criteri di accettazione: come si verifica che la milestone è completa (es. "test di registrazione + login passano, pagina profilo mostra i dati corretti")
- Stima di tempo: settimane o giorni, non mesi. Se una fase dura più di 3 settimane, dividila
- Test da eseguire: comandi concreti per verificare (es.
curl, test automatici, controllo manuale)
Esempio di milestone per un blog
FASE 1 — MVP (Settimana 1-2)
[ ] Setup Docker + Nginx + PHP + MariaDB
[ ] Routing base (index.php + .htaccess)
[ ] Pagina home static
[ ] Connessione al database
[ ] Milestone: "Hello World" in container
FASE 2 — Contenuti (Settimana 3-4)
[ ] CRUD articoli (admin)
[ ] Slug e URL parlanti
[ ] Pagina articolo singolo
[ ] Categorie e tag
[ ] Milestone: "Pubblicare un articolo" end-to-end
FASE 3 — Utenti (Settimana 5-6)
[ ] Registrazione e login
[ ] Ruoli (admin, autore, lettore)
[ ] Gestione profilo
[ ] Protezione route admin
[ ] Milestone: "Login funzionante"
FASE 4 — SEO & Performance (Settimana 7-8)
[ ] Meta tag dinamici
[ ] Sitemap XML
[ ] Dati strutturati JSON-LD
[ ] Caching Nginx + Redis
[ ] Milestone: "Lighthouse score > 90"
FASE 5 — i18n & GDPR (Settimana 9-10)
[ ] Sistema di traduzioni
[ ] Cookie banner con consenso
[ ] Informativa privacy
[ ] Diritto all'oblio
[ ] Milestone: "Conforme GDPR"
FASE 6 — Testing & Deploy (Settimana 11-12)
[ ] Test unitari (PHPUnit)
[ ] Test funzionali
[ ] Backup automatizzati
[ ] Deploy su server di produzione
[ ] Milestone: "Go live!"
Template per il tracking delle milestone
# Milestone: FASE X — Nome Fase
## Obiettivo
Breve descrizione di cosa deve essere vero al termine di questa fase.
## Checklist
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
## Criteri di accettazione
- L'utente può fare X
- Il test Y passa
- La pagina Z carica in < 2s
## Test da eseguire
Comandi per verificare il completamento
curl -sS https://example.com/ | grep "expected"
## Note
Eventuali decisioni o problemi incontrati.
14. Elementi aggiuntivi per un progetto web moderno
Oltre agli elementi fondamentali, ci sono accorgimenti che trasformano un progetto funzionante in un progetto professionale e manutenibile.
Perché questi elementi sono importanti
Health check, rate limiting, logging e pagine di errore sembrano dettagli, ma fanno la differenza in produzione. Un health check permette a Docker o a un orchestrator (Kubernetes) di sapere se l'app è viva e può morire e rinascere automaticamente. Il rate limiting impedisce a un singolo utente di saturare il server con richieste massive. Il logging strutturato trasforma messaggi di testo sparsi in dati analizzabili con strumenti come Grafana o ELK. Una pagina 404 personalizzata trasforma un errore frustrante in un'opportunità per tenere l'utente sul sito.
Health check e monitoring
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
<?php
// /health endpoint
header('Content-Type: application/json');
$health = [
'status' => 'ok',
'time' => date('c'),
'php' => PHP_VERSION,
'db' => checkDatabaseConnection() ? 'ok' : 'error',
'memory' => memory_get_usage(true),
'uptime' => file_get_contents('/proc/uptime'),
];
echo json_encode($health, JSON_PRETTY_PRINT);
?>
Rate limiting
# Nginx limit_req
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
location /login {
limit_req zone=login burst=10 nodelay;
try_files $uri $uri/ /index.php?$query_string;
}
Logging strutturato
<?php
function app_log(string $level, string $message, array $context = []): void
{
$entry = [
'time' => date('c'),
'level' => $level,
'message' => $message,
'context' => $context,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli',
];
file_put_contents(
__DIR__ . '/../var/log/app.log',
json_encode($entry) . "\n",
FILE_APPEND | LOCK_EX
);
}
?>
Pagina di errore 404 personalizzata
<?php
// pages/404.php
http_response_code(404);
?>
<section class="error-page">
<h1>404</h1>
<p>La pagina che cerchi non esiste.</p>
<a href="/" class="btn">Torna alla Home</a>
</section>
Conclusioni
Realizzare un'applicazione web moderna e professionale richiede molto più che saper scrivere codice. La vera differenza tra un progetto amatoriale e uno professionale sta in:
- Pianificazione — stack scelto con criterio, funzionalità priorizzate, milestone chiare
- Architettura — diagrammi, routing pulito, separazione delle responsabilità
- Sicurezza — GDPR, SQL injection, XSS, CSRF, HTTPS obbligatorio
- Infrastruttura — containerizzazione, IaC, Cloudflare, backup automatizzati
- Qualità — test, documentazione, logging, monitoring
- Manutenibilità — i18n, codice commentato, convenzioni chiare, Git
Ogni elemento di questa guida è stato applicato nei progetti reali ospitati su questo blog. La checklist qui sopra è un punto di partenza: adattala alle tue esigenze, migliorala con l'esperienza e, soprattutto, documenta ogni decisione.
Articolo pubblicato su filippobilardo.it — Tutti i diritti riservati