Quando si lavora con PHP e MySQL, uno degli obiettivi principali è avere un sistema che sia rapido, leggibile e sicuro. Molti sviluppatori partono dai metodi nativi di MySQLi, ma dopo poco si accorgono che anche una semplice query richiede diverse righe di codice, controlli ripetuti e una gestione manuale dei parametri.
Per questo motivo può essere utile creare una classe database leggera, pensata per PHP 8.x, capace di semplificare le query preparate e di rendere il codice più pulito. L’idea è la stessa delle classi minimal viste in molti tutorial: scrivere meno, ma mantenere sicurezza e ordine.
In questo articolo vediamo come costruire una classe MySQLi moderna, compatibile con PHP 8.x, migliorata rispetto alle versioni più vecchie e pronta da usare nei progetti reali.
Perché aggiornare una classe database a PHP 8.x
Con PHP 8.x conviene rivedere il codice legacy per diversi motivi:
- uso di typed properties e firme più chiare;
- migliore gestione delle eccezioni;
- codice più leggibile e manutenibile;
- meno ambiguità su valori nulli e tipi dinamici;
- maggiore compatibilità con ambienti moderni.
Una buona classe database oggi dovrebbe offrire:
- connessione semplice;
- query preparate automatiche;
- metodi rapidi per recuperare un record o più record;
- supporto a insert, update e delete;
- conteggio righe, affected rows e last insert id;
- chiusura pulita delle risorse.
Obiettivo della classe
L’obiettivo non è sostituire un ORM completo, ma creare un livello intermedio molto veloce da integrare. In pratica:
- scrivi una query con i placeholder
?; - passi i parametri in modo diretto;
- la classe si occupa di prepare, bind ed execute;
- recuperi il risultato con un metodo intuitivo.
Questo approccio è ideale nei progetti custom, nei piccoli CMS, nei plugin WordPress standalone o nei gestionali PHP dove si vuole mantenere tutto semplice e controllato.
La classe MySQLi aggiornata per PHP 8.x
Qui sotto trovi una versione moderna della classe, riscritta per PHP 8.x con proprietà tipizzate, controlli più chiari e una struttura più pulita.
<?php
final class DB
{
private mysqli $connection;
private ?mysqli_stmt $stmt = null;
private bool $showErrors;
private bool $queryClosed = true;
public int $queryCount = 0;
public function __construct(
string $host = 'localhost',
string $user = 'root',
string $pass = '',
string $name = '',
string $charset = 'utf8mb4',
bool $showErrors = true
) {
$this->showErrors = $showErrors;
mysqli_report(MYSQLI_REPORT_OFF);
$this->connection = new mysqli($host, $user, $pass, $name);
if ($this->connection->connect_errno) {
$this->error('Connessione MySQL fallita: ' . $this->connection->connect_error);
}
if (!$this->connection->set_charset($charset)) {
$this->error('Impossibile impostare il charset: ' . $this->connection->error);
}
}
public function query(string $sql, mixed ...$params): self
{
if (!$this->queryClosed && $this->stmt instanceof mysqli_stmt) {
$this->stmt->close();
}
$this->stmt = $this->connection->prepare($sql);
if (!$this->stmt) {
$this->error('Errore nella prepare: ' . $this->connection->error);
}
if (!empty($params)) {
$flatParams = $this->flattenParams($params);
$types = '';
$bindValues = [];
foreach ($flatParams as $param) {
$types .= $this->detectType($param);
$bindValues[] = $param;
}
if ($types !== '') {
$this->stmt->bind_param($types, ...$bindValues);
}
}
$this->stmt->execute();
if ($this->stmt->errno) {
$this->error('Errore durante execute: ' . $this->stmt->error);
}
$this->queryClosed = false;
$this->queryCount++;
return $this;
}
public function fetchAll(): array
{
if (!$this->stmt instanceof mysqli_stmt) {
return [];
}
$result = $this->stmt->get_result();
$rows = $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
$this->stmt->close();
$this->queryClosed = true;
return $rows;
}
public function fetchArray(): array
{
if (!$this->stmt instanceof mysqli_stmt) {
return [];
}
$result = $this->stmt->get_result();
$row = $result ? ($result->fetch_assoc() ?: []) : [];
$this->stmt->close();
$this->queryClosed = true;
return $row;
}
public function numRows(): int
{
if (!$this->stmt instanceof mysqli_stmt) {
return 0;
}
$result = $this->stmt->get_result();
return $result ? $result->num_rows : 0;
}
public function affectedRows(): int
{
return $this->stmt instanceof mysqli_stmt ? $this->stmt->affected_rows : 0;
}
public function lastInsertId(): int|string
{
return $this->connection->insert_id;
}
public function close(): bool
{
if ($this->stmt instanceof mysqli_stmt) {
$this->stmt->close();
$this->queryClosed = true;
}
return $this->connection->close();
}
private function flattenParams(array $params): array
{
$flat = [];
foreach ($params as $param) {
if (is_array($param)) {
foreach ($param as $value) {
$flat[] = $value;
}
} else {
$flat[] = $param;
}
}
return $flat;
}
private function detectType(mixed $value): string
{
return match (true) {
is_int($value) => 'i',
is_float($value) => 'd',
is_string($value) => 's',
is_null($value) => 's',
default => 'b',
};
}
private function error(string $message): never
{
if ($this->showErrors) {
exit($message);
}
exit('Errore database.');
}
}
Cosa migliora rispetto a molte classi vecchie
1. Proprietà tipizzate
Le proprietà come mysqli, ?mysqli_stmt, bool e int rendono il codice più leggibile e riducono gli errori.
2. Charset moderno
Il default è utf8mb4, quindi è più adatto ai progetti attuali rispetto a utf8, soprattutto se devono gestire caratteri speciali, simboli o emoji.
3. Gestione parametri più pulita
La funzione query() accetta parametri variadici, quindi puoi scrivere codice molto semplice senza impazzire con array complessi.
4. Uso di get_result()
Per il recupero dei dati, questa versione usa get_result() quando disponibile, rendendo fetchAll() e fetchArray() molto più leggibili.
5. Compatibilità migliore con PHP 8.x
La classe è pensata con una sintassi moderna e con un flusso più lineare rispetto alle implementazioni nate anni fa.
Come connettersi al database
Una volta salvata la classe in un file, ad esempio DB.php, puoi inizializzarla così:
<?php
require_once 'DB.php';
$db = new DB('localhost', 'root', '', 'example_db');
Da questo momento puoi usare la stessa istanza per tutte le query del progetto.
Esempi pratici di utilizzo
Recuperare un singolo record
<?php
$user = $db->query(
'SELECT * FROM users WHERE email = ? AND status = ?',
'mario@example.com',
'active'
)->fetchArray();
print_r($user);
Recuperare più record
<?php
$users = $db->query('SELECT id, name, email FROM users ORDER BY id DESC')->fetchAll();
foreach ($users as $user) {
echo $user['name'] . '<br>';
}
Insert con parametri
<?php
$db->query(
'INSERT INTO users (name, email, status) VALUES (?, ?, ?)',
'Mario Rossi',
'mario@example.com',
'active'
);
echo $db->lastInsertId();
Update
<?php
$update = $db->query(
'UPDATE users SET status = ? WHERE id = ?',
'inactive',
5
);
echo $update->affectedRows();
Delete
<?php
$delete = $db->query('DELETE FROM users WHERE id = ?', 8);
echo $delete->affectedRows();
Contare il numero di query eseguite
<?php echo $db->queryCount;
Perché una classe così può essere utile
Una struttura di questo tipo è utile soprattutto quando vuoi:
- evitare di riscrivere sempre
prepare,bind_paramedexecute; - mantenere il progetto leggero;
- non introdurre dipendenze esterne;
- avere un codice più ordinato nei file PHP;
- ridurre il rischio di SQL injection grazie alle query preparate.
In molti casi è la soluzione ideale per pannelli admin custom, piccoli CRM, tool interni, e-commerce artigianali o plugin sviluppati su misura.
Best practice da seguire comunque
Anche se la classe semplifica molto il lavoro, ci sono alcune regole che restano fondamentali:
Escape dell’output
Le query preparate proteggono il database, ma non proteggono l’output HTML. Quando mostri a video dati provenienti dal database, usa funzioni come htmlspecialchars().
Validazione dei dati in ingresso
Prima ancora di eseguire la query, conviene validare email, interi, date e valori attesi. Sicurezza non significa solo prepare statement.
Gestione errori più evoluta
In un progetto grande potresti sostituire exit() con eccezioni personalizzate e logging su file, così da non mostrare errori sensibili in produzione.
Separazione della configurazione
Le credenziali di accesso al database andrebbero tenute in un file di configurazione dedicato o in variabili d’ambiente.
Quando usare questa classe e quando no
Questa classe va benissimo se vuoi velocità di sviluppo e controllo diretto sulle query. Se invece hai un’applicazione molto grande, con relazioni complesse, migrazioni, repository e astrazioni avanzate, allora può avere più senso valutare strumenti più strutturati.
Detto questo, per tantissimi progetti PHP custom una classe del genere rappresenta ancora oggi un compromesso eccellente tra prestazioni, leggibilità e facilità d’uso.
Conclusione
Aggiornare una classe MySQLi in stile “super-fast” a PHP 8.x significa prendere un’idea semplice e renderla più solida, più moderna e più adatta agli standard attuali. Il vantaggio principale non è solo scrivere meno codice, ma farlo in modo più pulito e con una base già pronta per query sicure.
Se stai sviluppando un gestionale, un backend custom o un progetto PHP senza framework pesanti, questa struttura può diventare un ottimo punto di partenza. È leggera, chiara da leggere e abbastanza flessibile da essere estesa con metodi aggiuntivi come transazioni, debug SQL, logger o query helper personalizzati.
La semplicità, quando è costruita bene, resta ancora una delle soluzioni migliori.
