Progetto Guidato — Creare una Piattaforma di Incontri con PHP e MySQL

Dalla struttura vuota al matching con messaggistica protetta, passo dopo passo · Marco Consiglio
Volta Institute · Napoli
A.A. 2026
0

Punto di partenza — cosa costruiamo

🎯 Obiettivo del progetto
Dal punto di vista tecnico, questo progetto risolve un problema ben definito di dati e relazioni: profili con preferenze dichiarate, un algoritmo che suggerisce profili compatibili, un meccanismo di interesse reciproco ("match"), e messaggistica privata sbloccata solo dopo un match confermato. Questa guida tratta esclusivamente l'architettura tecnica — schema dati, query, sicurezza — non genera contenuti di profilo né testi editoriali.
📁 Struttura delle cartelle del progetto
incontri/ ├── config.php // connessione al database (Passo 2) ├── login.php // autenticazione (Passo 5) ├── profilo.php // creare/modificare il proprio profilo (Passo 3) ├── suggerimenti.php // profili compatibili (Passo 4) ├── esprimi_interesse.php // azione di interesse verso un profilo (Passo 5) └── messaggi.php // messaggistica solo dopo match (Passo 6)
1

Il database — creiamo tutte le tabelle

🗄️ Schema completo, nell'ordine corretto
Ordine: profili (referenzia utenti) → preferenze (referenzia profili) → interessi_espressi (referenzia profili due volte) → messaggi_privati, blocchi, segnalazioni (indipendenti tra loro, referenziano solo profili).
CREATE TABLE profili ( id INT AUTO_INCREMENT PRIMARY KEY, utente_id INT UNIQUE, età TINYINT, città VARCHAR(100), bio TEXT, visibile BOOLEAN DEFAULT TRUE ); CREATE TABLE preferenze ( profilo_id INT PRIMARY KEY, età_minima TINYINT, età_massima TINYINT, città_preferita VARCHAR(100), FOREIGN KEY (profilo_id) REFERENCES profili(id) ); CREATE TABLE interessi_espressi ( id INT AUTO_INCREMENT PRIMARY KEY, da_profilo_id INT, verso_profilo_id INT, creato_il TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY un_interesse (da_profilo_id, verso_profilo_id) ); CREATE TABLE messaggi_privati ( id INT AUTO_INCREMENT PRIMARY KEY, mittente_id INT, destinatario_id INT, contenuto TEXT NOT NULL, inviato_il TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE blocchi ( id INT AUTO_INCREMENT PRIMARY KEY, profilo_bloccante_id INT, profilo_bloccato_id INT ); CREATE TABLE segnalazioni ( id INT AUTO_INCREMENT PRIMARY KEY, profilo_segnalante_id INT, profilo_segnalato_id INT, motivo TEXT, stato ENUM('in_attesa', 'revisionata') DEFAULT 'in_attesa' );
💡 UNIQUE KEY (da_profilo_id, verso_profilo_id) impedisce che uno stesso profilo esprima interesse due volte verso lo stesso altro profilo — un vincolo di unicità su DUE colonne insieme, diverso dal vincolo su singola colonna già visto nella guida MySQL Basi.
✅ Checkpoint — a questo punto hai
6 tabelle pronte, che modellano profili, preferenze, interesse reciproco, messaggistica e sicurezza (blocchi/segnalazioni) — nessun dato ancora.
2

Connessione e prima pagina di test

config.php
<?php $host = 'localhost'; $db = 'incontri_volta'; $user = 'root'; $password = getenv('DB_PASSWORD'); try { $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $password); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { die("Connessione fallita: " . $e->getMessage()); } ?>
profilo.php (versione di TEST, verrà sostituita al Passo 3)
<?php require 'config.php'; echo "Connessione riuscita al database $db!"; ?>
✅ Checkpoint — a questo punto hai: apri profilo.php. Messaggio di conferma = si può proseguire.
3

Funzionalità core — creare il proprio profilo

🎯 Cosa costruiamo in questo passo
La versione più semplice: un form salva età, città e bio in un profilo. Nessun collegamento a preferenze, nessun matching, nessun login ancora — solo il meccanismo base di salvataggio.
profilo.php (sostituisce la versione di test del Passo 2)
<?php require 'config.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $età = (int) ($_POST['età'] ?? 0); $città = $_POST['città'] ?? ''; $bio = $_POST['bio'] ?? ''; $stmt = $pdo->prepare("INSERT INTO profili (età, città, bio) VALUES (:età, :città, :bio)"); $stmt->execute(['età' => $età, 'città' => $città, 'bio' => $bio]); echo "Profilo salvato"; } ?> <form method="post"> <input type="number" name="età" required> <input type="text" name="città"> <textarea name="bio"></textarea> <button type="submit">Salva profilo</button> </form>
💡 utente_id resta NULL — lo popoleremo al Passo 5 quando introdurremo il login. Nessuna riga di preferenze viene creata ancora.
✅ Checkpoint — a questo punto hai
Un profilo salvabile con i tre campi base. Nessun modo ancora per vedere gli altri profili né per esprimere interesse.
4

Aggiungere le relazioni — preferenze e suggerimenti

🔗 Colleghiamo le preferenze e costruiamo i suggerimenti
profilo.php (ESTENSIONE: salvare anche le preferenze)
<?php // dopo l'INSERT in profili del Passo 3, aggiungiamo: $profiloId = $pdo->lastInsertId(); $stmt = $pdo->prepare("INSERT INTO preferenze (profilo_id, età_minima, età_massima, città_preferita) VALUES (:pid, :min, :max, :città)"); $stmt->execute([ 'pid' => $profiloId, 'min' => (int) ($_POST['età_minima'] ?? 18), 'max' => (int) ($_POST['età_massima'] ?? 99), 'città' => $_POST['città_preferita'] ?? null ]); ?> // il form del Passo 3 si estende con due nuovi input: età_minima, età_massima, città_preferita
suggerimenti.php (nuovo file)
🎯 Profili compatibili in base alle preferenze
<?php require 'config.php'; $profiloId = (int) ($_GET['profilo_id'] ?? 0); $stmt = $pdo->prepare(" SELECT p.id, p.città, p.età FROM profili p INNER JOIN preferenze pref ON pref.profilo_id = :profilo_id WHERE p.visibile = TRUE AND p.id != :profilo_id AND p.età BETWEEN pref.età_minima AND pref.età_massima AND (pref.città_preferita IS NULL OR p.città = pref.città_preferita) LIMIT 20 "); $stmt->execute(['profilo_id' => $profiloId]); $suggeriti = $stmt->fetchAll(PDO::FETCH_ASSOC); ?> <?php foreach ($suggeriti as $s): ?> <p>Profilo #<?= $s['id'] ?> — <?= htmlspecialchars($s['città']) ?>, <?= $s['età'] ?> anni</p> <?php endforeach; ?>
💡 p.id != :profilo_id esclude il proprio profilo dai risultati; IS NULL OR rende il filtro città opzionale se non specificato nelle preferenze — entrambi dettagli che presuppongono i dati salvati al Passo 3-4.
✅ Checkpoint — a questo punto hai: dato un profilo, la pagina suggerisce altri profili filtrati per età e (opzionalmente) città — ma senza ancora alcun controllo su chi può vedere questi suggerimenti, né modo di esprimere interesse.
5

Permessi e sicurezza — login, interesse reciproco, blocco

login.php
<?php session_start(); require 'config.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = $pdo->prepare("SELECT * FROM utenti WHERE email = :email"); $stmt->execute(['email' => $_POST['email']]); $utente = $stmt->fetch(PDO::FETCH_ASSOC); if ($utente && password_verify($_POST['password'], $utente['password_hash'])) { $_SESSION['utente_id'] = $utente['id']; header('Location: suggerimenti.php'); exit; } echo "Credenziali non valide"; } ?> <form method="post"> <input type="email" name="email" required> <input type="password" name="password" required> <button type="submit">Accedi</button> </form>
profilo.php e suggerimenti.php (MODIFICA: login obbligatorio ovunque)
<?php session_start(); if (!isset($_SESSION['utente_id'])) { header('Location: login.php'); exit; } require 'config.php'; // resto invariato rispetto ai Passi 3-4 ?>
⚠️ A differenza della Directory (dove la ricerca restava pubblica), qui OGNI pagina richiede login: profili, preferenze e suggerimenti sono dati sensibili che non devono essere accessibili senza autenticazione.
esprimi_interesse.php (nuovo file)
💛 Registrare l'interesse verso un profilo
<?php session_start(); if (!isset($_SESSION['utente_id'])) { header('Location: login.php'); exit; } require 'config.php'; $daProfiloId = (int) $_POST['da_profilo_id']; $versoProfiloId = (int) $_POST['verso_profilo_id']; $stmt = $pdo->prepare("INSERT INTO interessi_espressi (da_profilo_id, verso_profilo_id) VALUES (:da, :verso)"); $stmt->execute(['da' => $daProfiloId, 'verso' => $versoProfiloId]); ?>
💡 Se lo stesso profilo prova a esprimere interesse due volte verso lo stesso destinatario, il vincolo UNIQUE KEY scritto al Passo 1 blocca il duplicato — un errore da gestire con try/catch, coerentemente con la guida PDO.
🔗 Rilevare il match reciproco (auto-JOIN)
<?php function possonoMessaggiarsi(PDO $pdo, int $profiloA, int $profiloB): bool { $stmt = $pdo->prepare(" SELECT COUNT(*) AS totale FROM interessi_espressi a INNER JOIN interessi_espressi b ON a.da_profilo_id = b.verso_profilo_id AND a.verso_profilo_id = b.da_profilo_id WHERE a.da_profilo_id = :a AND a.verso_profilo_id = :b "); $stmt->execute(['a' => $profiloA, 'b' => $profiloB]); $risultato = $stmt->fetch(PDO::FETCH_ASSOC); return $risultato['totale'] > 0; } ?>
✅ Checkpoint — a questo punto hai: un AUTO-JOIN (la tabella interessi_espressi collegata a se stessa con alias a/b) che verifica la reciprocità — questa funzione sarà la porta d'accesso obbligatoria alla messaggistica del Passo 6.
6

Funzionalità avanzate — messaggistica, blocchi, segnalazioni

messaggi.php (nuovo file)
💬 Messaggistica protetta dal match
<?php session_start(); if (!isset($_SESSION['utente_id'])) { header('Location: login.php'); exit; } require 'config.php'; require 'matching.php'; // contiene possonoMessaggiarsi(), estratta dal Passo 5 $mioProfiloId = (int) $_GET['mio_profilo_id']; $altroProfiloId = (int) $_GET['altro_profilo_id']; if (!possonoMessaggiarsi($pdo, $mioProfiloId, $altroProfiloId)) { die("Potete scrivervi solo dopo un match reciproco"); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = $pdo->prepare("INSERT INTO messaggi_privati (mittente_id, destinatario_id, contenuto) VALUES (:m, :d, :c)"); $stmt->execute(['m' => $mioProfiloId, 'd' => $altroProfiloId, 'c' => $_POST['contenuto']]); } ?>
⚠️ Il controllo possonoMessaggiarsi() del Passo 5 è qui OBBLIGATORIO prima di qualsiasi INSERT — impedire contatti non richiesti è una funzione di sicurezza essenziale per questo tipo di piattaforma.
blocca.php (nuovo file)
🚫 Bloccare un profilo, con priorità sul match esistente
<?php session_start(); if (!isset($_SESSION['utente_id'])) { header('Location: login.php'); exit; } require 'config.php'; $stmt = $pdo->prepare("INSERT INTO blocchi (profilo_bloccante_id, profilo_bloccato_id) VALUES (:bloccante, :bloccato)"); $stmt->execute(['bloccante' => $_POST['profilo_bloccante_id'], 'bloccato' => $_POST['profilo_bloccato_id']]); ?>
suggerimenti.php (MODIFICA: escludere profili bloccati)
<?php // query del Passo 4, con l'aggiunta del filtro sui blocchi: $stmt = $pdo->prepare(" SELECT p.id, p.città, p.età FROM profili p INNER JOIN preferenze pref ON pref.profilo_id = :profilo_id WHERE p.visibile = TRUE AND p.id != :profilo_id AND p.età BETWEEN pref.età_minima AND pref.età_massima AND (pref.città_preferita IS NULL OR p.città = pref.città_preferita) AND p.id NOT IN (SELECT profilo_bloccato_id FROM blocchi WHERE profilo_bloccante_id = :profilo_id2) LIMIT 20 "); $stmt->execute(['profilo_id' => $profiloId, 'profilo_id2' => $profiloId]); ?>
✅ Checkpoint — a questo punto hai: un profilo bloccato non compare più nei suggerimenti (Passo 4 esteso); messaggi.php resta protetto da match reciproco (Passo 5); un blocco impedisce nuovi messaggi anche se un match preesisteva — la sicurezza dell'utente prevale sul match tecnico.
7

Integrazione finale — come tutto si tiene insieme

🧩 Il flusso completo, dall'inizio alla fine
PassoCosa abbiamo costruito
16 tabelle: profili, preferenze, interessi_espressi (con UNIQUE su coppia), messaggi_privati, blocchi, segnalazioni
2config.php: connessione condivisa
3profilo.php: salvataggio profilo minimo, senza preferenze
4Preferenze aggiunte al salvataggio; suggerimenti.php con filtro età/città
5login.php ovunque; esprimi_interesse.php; possonoMessaggiarsi() con auto-JOIN
6messaggi.php protetto dal match; blocca.php; suggerimenti.php esteso per escludere i bloccati
La funzione possonoMessaggiarsi() scritta al Passo 5 diventa la porta d'accesso obbligatoria a messaggi.php al Passo 6 — un principio di sicurezza preparato prima di avere ancora un posto dove applicarlo concretamente.
🚀 Da qui in avanti
Estensioni naturali non trattate qui: gestione delle segnalazioni da parte di un moderatore (riprendendo il pattern di stato visto in segnalazioni.stato), notifica di un nuovo match, "annullare" un interesse espresso prima che diventi un match.
🎓 Esercitazioni pratiche
1
Elenco dei match confermati
Crea match.php che, riusando l'auto-JOIN di possonoMessaggiarsi() (Passo 5), elenca TUTTI i profili con cui l'utente loggato ha un match reciproco, non solo verificarne uno specifico.
⏱ 35 min📦 1 nuovo file💻 Individuale
2
Gestione segnalazioni per un moderatore
Crea modera_segnalazioni.php che mostra le segnalazioni con stato = 'in_attesa' (tabella del Passo 1) e permette di aggiornarle a 'revisionata', riprendendo il pattern di controllo ruolo già visto nel CMS/Forum.
⏱ 30 min📦 1 nuovo file💻 Individuale
3
Impedisci messaggi verso un profilo bloccato
Estendi possonoMessaggiarsi() del Passo 5 aggiungendo un controllo sulla tabella blocchi: se esiste un blocco tra i due profili in una direzione o nell'altra, la funzione deve restituire false anche in presenza di un match reciproco.
⏱ 30 min📦 1 funzione modificata💻 Individuale