Introduzione
Quando si lavora con PHP e MySQL/MariaDB, uno degli argomenti più utili nella pratica quotidiana è la gestione delle operazioni CRUD, cioè Create, Read, Update e Delete. In altre parole: inserire, leggere, modificare e cancellare dati in modo affidabile.
Questo tema è fondamentale perché quasi ogni applicazione web reale deve salvare informazioni in un database: utenti, ordini, prodotti, commenti, prenotazioni, ticket di assistenza e così via. Il punto non è solo far funzionare le query, ma farle funzionare bene, con attenzione a sicurezza, validazione dei dati e manutenzione del codice.
In questo tutorial vedremo un esempio completo e pratico di gestione CRUD con PDO, usando query preparate e controlli di base sui dati in input. L’obiettivo è costruire una piccola base riutilizzabile per un’applicazione reale.
Codice completo
Nel seguente esempio immaginiamo di gestire una tabella products con i campi: id, name, price, stock e created_at.
<?php
declare(strict_types=1);
/**
* Connessione al database tramite PDO
*/
$dsn = ´mysql:host=localhost;dbname=shop;charset=utf8mb4´;
$user = ´root´;
$password = ´´;
try {
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
die(´Errore di connessione al database.´);
}
/**
* Funzione di supporto per leggere e pulire i dati
*/
function getPostString(string $key): string
{
return trim((string)($_POST[$key] ?? ´´));
}
function getPostFloat(string $key): float
{
return (float)($_POST[$key] ?? 0);
}
function getPostInt(string $key): int
{
return (int)($_POST[$key] ?? 0);
}
/**
* CREATE: inserimento di un nuovo prodotto
*/
function createProduct(PDO $pdo, string $name, float $price, int $stock): int
{
$sql = "INSERT INTO products (name, price, stock, created_at)
VALUES (:name, :price, :stock, NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([
´:name´ => $name,
´:price´ => $price,
´:stock´ => $stock,
]);
return (int)$pdo->lastInsertId();
}
/**
* READ: lettura di tutti i prodotti
*/
function getAllProducts(PDO $pdo): array
{
$sql = "SELECT id, name, price, stock, created_at
FROM products
ORDER BY created_at DESC";
return $pdo->query($sql)->fetchAll();
}
/**
* READ: lettura di un prodotto singolo
*/
function getProductById(PDO $pdo, int $id): ?array
{
$sql = "SELECT id, name, price, stock, created_at
FROM products
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([´:id´ => $id]);
$product = $stmt->fetch();
return $product ?: null;
}
/**
* UPDATE: aggiornamento di un prodotto
*/
function updateProduct(PDO $pdo, int $id, string $name, float $price, int $stock): bool
{
$sql = "UPDATE products
SET name = :name, price = :price, stock = :stock
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
´:name´ => $name,
´:price´ => $price,
´:stock´ => $stock,
´:id´ => $id,
]);
return $stmt->rowCount() > 0;
}
/**
* DELETE: cancellazione di un prodotto
*/
function deleteProduct(PDO $pdo, int $id): bool
{
$sql = "DELETE FROM products WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([´:id´ => $id]);
return $stmt->rowCount() > 0;
}
/**
* Esempio di uso con un semplice router basato su POST
* In un progetto reale potresti usare controller e route dedicate.
*/
$action = $_POST[´action´] ?? ´´;
$message = ´´;
if ($action === ´create´) {
$name = getPostString(´name´);
$price = getPostFloat(´price´);
$stock = getPostInt(´stock´);
if ($name === ´´ || $price <= 0 || $stock < 0) {
$message = ´Dati non validi per la creazione.´;
} else {
$newId = createProduct($pdo, $name, $price, $stock);
$message = "Prodotto creato con ID: " . $newId;
}
}
if ($action === ´update´) {
$id = getPostInt(´id´);
$name = getPostString(´name´);
$price = getPostFloat(´price´);
$stock = getPostInt(´stock´);
if ($id <= 0 || $name === ´´ || $price <= 0 || $stock < 0) {
$message = ´Dati non validi per l’aggiornamento.´;
} else {
$ok = updateProduct($pdo, $id, $name, $price, $stock);
$message = $ok ? ´Prodotto aggiornato correttamente.´ : ´Nessuna modifica effettuata.´;
}
}
if ($action === ´delete´) {
$id = getPostInt(´id´);
if ($id <= 0) {
$message = ´ID non valido per la cancellazione.´;
} else {
$ok = deleteProduct($pdo, $id);
$message = $ok ? ´Prodotto eliminato correttamente.´ : ´Prodotto non trovato.´;
}
}
$products = getAllProducts($pdo);
?>
<!DOCTYPE html>
<html lang="it">
<body>
<h1>Gestione prodotti</h1>
<p><strong><?= htmlspecialchars($message) ?></strong></p>
<h2>Elenco prodotti</h2>
<ul>
<?php foreach ($products as $product): ?>
<li>
#<?= (int)$product[´id´] ?> -
<?= htmlspecialchars($product[´name´]) ?> -
€<?= number_format((float)$product[´price´], 2, ´,´, ´.´) ?> -
Stock: <?= (int)$product[´stock´] ?>
</li>
<?php endforeach; ?>
</ul>
</body>
</html> Spiegazione
Il codice è organizzato in modo da separare le operazioni sul database in funzioni dedicate. Questo approccio rende il progetto più leggibile e più facile da mantenere.
1. Connessione con PDO
La connessione usa PDO, una scelta molto solida perché supporta diversi database e permette di usare query preparate in modo uniforme. Le opzioni più importanti sono:
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION: gli errori generano eccezioni, quindi sono più facili da intercettare.
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC: i risultati vengono restituiti come array associativi.
- PDO::ATTR_EMULATE_PREPARES => false: si usano prepared statement reali, quando possibile.
2. Funzioni di supporto per l’input
Le funzioni getPostString, getPostFloat e getPostInt semplificano la lettura dei dati inviati dal form. Non sostituiscono una validazione completa, ma aiutano a centralizzare la logica di base.
Per esempio, trim() rimuove spazi inutili all’inizio e alla fine di una stringa. Questo evita problemi come nomi salvati con spazi accidentali.
3. CREATE: inserimento
La funzione createProduct usa una query preparata con parametri nominati. Questo è importante per due motivi:
- riduce il rischio di SQL injection;
- separa chiaramente il codice SQL dai dati.
In più, il campo created_at viene valorizzato direttamente dal database con NOW(), così la data di creazione è coerente e non dipende dall’orologio del server PHP.
4. READ: lettura dei dati
La funzione getAllProducts recupera tutti i prodotti ordinandoli per data di creazione decrescente. Per una lettura semplice e senza parametri, si può usare query() invece di prepare().
La funzione getProductById invece usa una query preparata perché riceve un parametro esterno, cioè l’ID del prodotto.
5. UPDATE: aggiornamento
La funzione updateProduct aggiorna i campi principali di un prodotto. Il valore di ritorno si basa su rowCount(), utile per capire se la query ha effettivamente modificato righe nel database.
Attenzione però: rowCount() può restituire 0 anche se la query è andata a buon fine ma i dati erano già uguali a quelli presenti. Per questo, il suo risultato va interpretato con cautela.
6. DELETE: cancellazione
La funzione deleteProduct elimina un record tramite ID. Anche qui la query preparata è la scelta corretta. In applicazioni reali, la cancellazione dovrebbe spesso essere protetta da controlli aggiuntivi, ad esempio permessi utente o conferma esplicita.
7. Output sicuro in HTML
Nel template, i dati stampati vengono passati a htmlspecialchars(). Questo evita che eventuali caratteri speciali o codice HTML inseriti nei dati compromettano la pagina o introducano vulnerabilità XSS.
Best practice
- Usa sempre query preparate quando inserisci valori provenienti dall’esterno.
- Valida i dati prima di scrivere nel database: controlla stringhe vuote, numeri negativi, formati errati.
- Separare logica e presentazione aiuta a mantenere il codice più pulito e testabile.
- Non fidarti mai dell’input utente: anche un campo apparentemente semplice può contenere dati non attesi.
- Escapa sempre l’output HTML con htmlspecialchars() quando stampi dati provenienti dal database.
- Usa charset utf8mb4 per supportare correttamente caratteri speciali ed emoji.
- Gestisci gli errori con eccezioni e mostra messaggi generici all’utente finale, senza esporre dettagli sensibili.
Riepilogo
Le operazioni CRUD sono la base di moltissime applicazioni PHP che interagiscono con MySQL o MariaDB. In questo tutorial abbiamo visto come implementarle in modo chiaro usando PDO, query preparate e controlli essenziali sui dati.
I concetti chiave da ricordare sono:
- CREATE per inserire nuovi record;
- READ per leggere uno o più record;
- UPDATE per modificare dati esistenti;
- DELETE per eliminare dati;
- validazione e escaping per sicurezza e robustezza.
Con una struttura come quella proposta, puoi costruire rapidamente una base solida per un pannello di amministrazione, un gestionale o una piccola applicazione web con dati persistenti.
Approfondisci con risorse ufficiali
- PHP Manual - PDO: documentazione ufficiale su connessione e query preparate.
- PHP Manual - htmlspecialchars(): per proteggere l’output HTML.
- MySQL Reference Manual: dettagli su tipi di dati, indici e funzioni SQL.
- MariaDB Knowledge Base: utile per differenze e compatibilità con MySQL.
- PHP Manual - filter_var(): per validare e filtrare input in modo più strutturato.
