ReVo Blog

Lavorare con il Database

« Older   Newer »
  Share  
.ReVo.
view post Posted on 1/8/2014, 17:12




Prima o poi capita a tutti di voler lavorare con un Database per salvare i dati degli utenti online, e all'inizio siamo tutti tentati dal realizzare le connessioni, le richieste e la gestione dei dati nella pagina PHP stessa e con codice del genere:

CODICE
$connessione = new MySQLi("localhost", "root", "hello", "world");
$result = $connessione->query("SELECT nome, cognome FROM users");
while ($row = $result->fetch_row()) {
  echo "Utente - Nome: " . $row[0] " . - " . $row[1] . ";<br />";
}
$result->close();
$connessione->close();


In effetti questo codice funziona, ottimo!

Ora immaginiamo che la nostra applicazione stia diventando sempre più grande (yup!): i file PHP aumentano, i dati da leggere e scrivere aumentano, le query diventano più lunghe e faticose ed un semplice errore può causare gravi danni!

La nostra applicazione è diventata cosi grande e fa qualcosa di utile che la vogliamo hostare su altervista (o dove volete) e prima mazzata:

CITAZIONE
Warning: mysqli_connect() [function.mysqli-connect]: Can't connect to MySQL server on 'localhost' (10061) in \myhost\index.php on line 2 Could not connect: Can't connect to MySQL server on 'localhost' (10061)

(l'errore sarà simile a questo, ho preso il primo che è capitato!)

Boom! Vediamo questo errore PHP ovunque, in ogni pagina e non funziona più niente!

Perchè? Beh, i nostri dati connessione

CODICE
$connessione = new MySQLi("localhost", "root", "hello", "world");


(questa parte!)

andavano bene per il nostro server casalingo, ma il nostro hosting online ci ha fornito altri dati per connetterci e per colpa di questo adesso vi ritrovare a modificare ben 40 file php! (numero a caso, potrebbero essere di più!).

Aiuto!



Questa faticata si poteva evitare con una piccola accortezza: avete mai provato a girare per il sorgente di WordPress? C'è un piccolo file (config.php) che viene creato dall'installer di WordPress e contiene 4 linee che potevano salvarci la faticata:

CODICE
define("HOST", "localhost");
define("USER", "root");
define("DATABASE", "world");
define("PASSWORD", "hello");


(Questo file può essere utilizzato per salvare tutti i dati che credete possano servire in giro per tutta la vostra applicazione (es. directory dove salvare dati))

Prima di tornare al nostro esempio, cos'è define?

CODICE
define(name, value);


Non è altro che una funzione che ci aiuta a definire una constante all'interno della pagina PHP, sono praticamente identiche alle constanti in altri linguaggi di programmazione... un esempio in Java:

CODICE
public abstract class DatabaseData {
   public static final String HOST = "localhost";
   public static final String USER = "root";
   public static final String DATABASE = "world";
   public static final String PASSWORD = "hello";
}


Per accedere una una constante non è necessario il simbolo del dollaro ($), quindi se abbiamo una constante di nome HOST e la stampiamo:

CODICE
define("HOST", "localhost");
echo HOST;


vedremo sullo schermo "localhost".

Importante: Le constanti sono case sensitive! Quindi Host è diverso da HOST come è a sua volta diverso da HoSt! L'unico modo per renderle case-insensitive è utilizzare il terzo parametro della funzione define passandogli true:

CODICE
define("HOST", "localhost", true);
echo host;


(stesso output di sopra)

Credo che adesso le idee siamo piuttosto chiaro riguardo al define e credo abbiate anche capito in che modo queste constanti ci possono aiutare a migliorare il nostro codice di sopra.

Modifichiamo il nostro codice di sopra per utilizzare le costanti:

CODICE
require_once("config.php");

$connessione = new MySQLi(HOST, USER, PASSWORD, DATABASE);
$result = $connessione->query("SELECT nome, cognome FROM users LIMIT 5");
while ($row = $result->fetch_row()) {
  echo "Utente - Nome: " . $row[0] " . - " . $row[1] . ";<br />";
}
$result->close();
$connessione->close();


Cosa è cambiato?

CODICE
require_once("config.php");


e

CODICE
$connessione = new MySQLi(HOST, USER, PASSWORD, DATABASE);


config.php è il file dove sono dichiarate le nostre constanti, nulla di speciale.

Neanche l'ultima modifica è nulla di speciale, il risultato è lo stesso del primo esempio con la differenza che se adesso avete un codice più flessibile... perchè? Beh, adesso quando passerete online vi basterà modificare un file per avere l'intera applicazione di nuovo online in pochi secondi!

Con questo sistema otterrete un codice più flessibile dato che nessuno vi vieta di scrivere qualcosa del genere:

CODICE
define("TESTING", true);

if (TESTING == true) {
  define("HOST", "localhost");
  define("USER", "root");
  define("DATABASE", "world");
  define("PASSWORD", "hello");

} else {

  define("HOST", "myHost");
  define("USER", "hostUser");
  define("DATABASE", "host_db_user");
  define("PASSWORD", "ahah");

}


In questo modo vi basterà cambiare una linea per passare alla configurazione testing/hosting!




Perfetto, funziona tutto alla grande.

Adesso però, vi domando...

  1. Non credete che le vostre pagine PHP si occupino di troppe cose? Parlo di quelle pagine che leggono i dati e le stampano sullo schermo... Appena fate una sola modifica all'interno del database dovrete girare l'intera applicazione per cercare tutte le parti danneggiate dalla modifica.

  2. Ogni volta che volete fare qualcosa dovete scrivere require_once("config.php"); $connessione = new MySQLi(HOST, USER, PASSWORD, DATABASE);... noioso, no? Che succede se un giorno volete smettere di utilizzare MySQLi e passare ad SQLite?

  3. Cosa fate se state scrivendo una nuova funzionalità e vi scordate come si chiama una tabella? O il nome di una colonna?

  4. Nel codice ad inizio post, ho utilizzato $row[0] e $row[1] ma se altero la query e mi dimentico di modificare tutto? Anche se usavo fetch_assoc se il nome di una colonna cambia bisogna cercare ovunque si faccia riferimento a quella colonna!


Devo essere sincero, questi problemi si ripresentano anche con la soluzione che vi voglio proporre tra poco ma con una differenza: si sa dove andare a cercare!

Tutto questo grazie all'uso delle classi infatti, invece di far gestire manualmente la connessione ad ogni pagina è questa "super classe" che si occupa di connettersi e di offrire metodi per interrogare il database... Si esatto! Invece di lavorare manualmente con $connessione->query, $result->fetch_row() o $row[0] lasciamo che sia questa classe ad occuparsene e noi ci occupiamo solamente di stampare i dati!

Un esempio?

CODICE
<?php

class Database {

   private $host = "localhost";
   private $user = "root";
   private $database = "database";
   private $password = "password";

   /**
    * @var mysqli
    */
   private $connection;

   public function __construct() {
       $this->connection = new MySQLi($this->host, $this->user, $this->password, $this->database);
   }

   function __destruct() {
       $this->connection->close();
   }
   
}


(L'ho chiamata Database, ma siete liberi di dargli qualunque nome!)

Alcune cose da notare

  1. Non abbiamo più bisogno dei define, e neanche del require_once

  2. Ma abbiamo bisogno di includere questa classe

  3. La creazione è simile al MySQLi con la differenza che non forniamo nessun tipo di dato


Anche questa classe è ancora troppo limitata dato che, se cambiamo database siamo costretti a cambiare tutti i metodi, in questo caso PDO vi può venire in soccorso.

Al momento questa classe non ci è di nessun aiuto, anzi non ci permette di fare niente.

Tra i punti di sopra ho spiegato la difficoltà di mantenere tutte le pagine aggiornate per eventuali modifiche al database, beh se utilizziamo la nostra classe Database (... e non solo!) questo problema non comparirà più.

Immaginiamo di avere nel nostro Database una tabella con tutti gli utenti registrati nel nostro sito, qualcosa del genere

nomecognomedatareg
ReVoLawson1
JohnLawson2


(1 e 2 sono timestamp.... okay sono numeri a caso.)

Nell'esempio di sopra dovevamo lavorare manualmente con tutto: query, selezionare e stampare.

Vediamo come la classe Database ci può tornare utile:

CODICE
public function getAllUsers() {
       $array = array();

       if ($result = $this->connection->query("SELECT nome, cognome FROM users")) {
           while ($row = $result->fetch_assoc()) {
               $array[] = array(
                   "nome" => $row["nome"],
                   "cognome" => $row["cognome"]
               );
           }
       }
       
       return $array;
   }


Questo metodo ci permette di leggere gli utenti presenti nel database e li ritorna sotto forma di array nel formato:

CODICE
[index] => { nome: x, cognome: x }


Se vi state chiedendo perché non ho scritto direttamente

CODICE
$array[] = $row;


A questo punto cosa cambiava da questo codice e quello iniziale? Se si cambiava nome alla colonna nome tutto il codice delle altre pagine non trovava più l'index nome generando un warning.

Ma possiamo fare ancora meglio! Perché ritornare un array quando possiamo rappresentare l'utente nel database utilizzando una classe?

CODICE
final class User {

   private $name;
   private $cognome;
   private $datareg;

   function __construct($nome, $cognome, $datareg) {
       $this->name = $nome;
       $this->cognome = $cognome;
       $this->datareg = $datareg;
   }

   /**
    * @return string
    */
   public function getCognome() {
       return $this->cognome;
   }

   /**
    * @return int
    */
   public function getDatareg() {
       return $this->datareg;
   }

   /**
    * @return string
    */
   public function getName() {
       return $this->name;
   }

}


Il nuovo getAllUsers:

CODICE
public function getAllUsers() {
       $array = array();

       if ($result = $this->connection->query("SELECT nome, cognome, datareg FROM users")) {
           while ($row = $result->fetch_assoc()) {
               $array[] = new User($row["nome"], $row["cognome"], $row["datareg"]);
           }
       }

       return $array;
   }


Adesso nel resto del codice non ci ritroviamo più a lavorare con un array di array, ma con un array di User... i vantaggi?

  1. Non si rischia di scrivere "name" o "nome" perchè adesso per accederci basta utilizzare il get getName()

  2. Il codice risulta più ordinato e flessibile, l'uso di getAllUsers diventa più elementare perchè basta sapere la classe User

  3. Le classi sono più chiare rispetto ad un array

  4. Sei libero di modificare la classe User ed avere le modifiche disponibili ovunque


Con il nostro getAllUser compare un nuovo problema che, ad essere sinceri, ho affrontato durante la realizzazione di un applicazione PHP di dimensioni medie...

CODICE
$array[] = new User($row["nome"], $row["cognome"], $row["datareg"]);


cosa succede se aggiungiamo un nuovo parametro obbligatorio? Questa riga darà errore! E come questa, ovunque!

Un mio consiglio è quello di cercare di "centralizzare" il tutto il più possibile... invece di utilizzare direttamente il construttore, uno static factory può aiutarvi a limitare le modifiche solo alla classe User e non a tutte le altre che la utilizzano.

Come è cambiato il nostro codice sulla nostra index dopo le ultime modifiche?

CODICE
require_once("Database.php");
$database = new Database();
while ($user = $database->getAllUsers()) {
 echo "User - " . $user->getName() . " - " . $user->getCognome();
}


Questi erano solo degli esempi, un altro consiglio che vi do è quello di cercare di rappresentare una tabella del database in una classe in modo da avere un codice più elegante e preciso.

Lo scopo di questo articolo era quello di insegnarvi come rendere le pagine PHP (il cui compito è quello di stampare i dati) totalmente ignoranti riguardo a come ricevono i dati.

Aaaspetta!



È impossibile non pensare a quando ci saranno 10 tabelle nel database, 10 classi nel codice e 350 metodi all'interno della classe Database beh, per ovviare a questo problema:

  1. Rendere la classe Database totalmente ignorante riguardo alle tabelle, quindi semplicemente offrire metodi come select, update e cosi via... magari con qualche supporto

  2. Creare "Classi responsabili della tabella X"


L'ultimo approccio è presente solo nella mia mente (sto pensando a come si potrebbe realizzare) e consiste nel realizzare delle classi che si occupano di lavorare direttamente con una tabella specifica e di rendere Database un "tramite"... credo che un esempio aiuti di più:

CODICE
class Database {

   private $host = "localhost";
   private $user = "root";
   private $database = "database";
   private $password = "password";

   /**
    * @var mysqli
    */
   private $connection;

   public function __construct() {
       $this->connection = new MySQLi($this->host, $this->user, $this->password, $this->database);
   }

   function __destruct() {
       $this->connection->close();
   }

   public function user() {
       return new UserDatabase($this->connection);
   }

}

class UserDatabase {

   /**
    * @var mysqli
    */
   private $connection;

   /* il construttore non dovrebbe essere pubblico, ma non so se il php ha qualcosa tipo "package-private" */
   public function __construct($connection) {
       $this->connection = $connection;
   }

   public function getAllUsers() {
       $array = array();

       if ($result = $this->connection->query("SELECT nome, cognome FROM users")) {
           while ($row = $result->fetch_assoc()) {
               $array[] = new User($row["nome"], $row["cognome"], $row["datareg"]);
           }
       }

       return $array;
   }
}


In questo modo, si potrebbe realizzare un overload del metodo user di questo tipo:

CODICE
user(ID_USER)
user(NULL)


Se si passa l'ID dell'utente la classe carica le informazioni riguardo a quell'utente, mentre user(NULL) da accesso ai metodi statici solamente.

I lati negativi:

  1. Due classi: User e UserDatabase... sono davvero necessarie? Non fanno altro che aumentare il codice inutilmente

  2. Se UserDatabase e User si uniscono, User deve prevedere il caso in cui vengano chiamati metodi privati per l'utente...



Tags:
php
 
Top
0 replies since 1/8/2014, 17:12   8 views
  Share