PDO (PHP Data Objects) è un'estensione standard di PHP che fornisce un'interfaccia per connettersi e gestire un database.

Nel file php.ini è possibile specificare i driver abilitati per i vari tipi di database

E' necessario definire quattro parametri fondamentali (meglio metterli nel file config.php):

  • $host = 'localhost';
  • $dbname = 'nome_database';
  • $username = 'nome_utente';
  • $password = 'password';
<?php

$host = 'localhost';
$dbname = 'nome_database';
$username = 'nome_utente';
$password = 'password';

try {
    $conn = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //si potevano passare nel costruttore come array associativo
    echo "Connessione al db riuscita!"; 
} catch (PDOException $e) {
    echo "Errore di connessione: . $e->getMessage()";
}

?>

@TODO fare approfondimento su SQL injection

Per creare una tabella si usa la funzione exec() e si passano come parametri gli statements per creare le tabelle

<?php

$statements = [
	'CREATE TABLE IF NOT EXISTS authors( 
        author_id   INT AUTO_INCREMENT,
        first_name  VARCHAR(100) NOT NULL, 
        last_name   VARCHAR(100) NULL,
        PRIMARY KEY(author_id)
    );',
	'CREATE TABLE IF NOT EXISTS books (
        book_id   INT AUTO_INCREMENT, 
        title VARCHAR(255) NOT NULL,
        PRIMARY KEY (book_id)
    )'];

$pdo = require 'connect.php';

foreach ($statements as $statement) {
	$pdo->exec($statement); // la exec ritorna il numero di righe se tutto è andato bene oppure false se c'è stato un errore
}

?>

Prepared Statements

Un prepared statement è un template per l'esecuzione di uno stesso statement con diversi valori. L'esecuzione della query avviene in due fasi, la preparazione e l'esecuzione vera e propria.

<?php

$pdo = require 'connect.php';

//primo metodo
$sql = 'insert into authors(first_name, last_name)
        values(?,?)'; // i due ? sono positional placeholders

$statement = $pdo->prepare($sql);
$statement->execute(['Mario', 'Rossi']);

//secondo metodo
$sql = 'insert into authors(first_name, last_name)
        values(:first_name,:last_name)'; //named placeholder

$statement = $pdo->prepare($sql);
$statement->execute([
	'first_name' => 'Mario',
	'last_name' => 'Rossi'
]);

?>

E' anche possibile usare la funzione bindValue() per impostare un valore da passare nella query. Il valore sarà quello definito al momento dell'utilizzo di bindValue e non al momento in cui viene chiamata la execute().

<?php

$pdo = require 'connect.php';

$sql = 'insert into authors(first_name, last_name)
        values(:first_name,:last_name)';

$statement = $pdo->prepare($sql);

$author = [
	'first_name' => 'Mario',
	'last_name' => 'Rossi',
];

$statement->bindValue(':first_name', $author['first_name']);
$statement->bindValue(':last_name', $author['last_name']);

$author['first_name'] = 'Giorgio';
$author['last_name'] = 'Verdi';

// la query verrà eseguita con Mario Rossi, i valori definiti nella bindValue
$statement->execute();

?>

Invece con bindParam i parametri sono passati per riferimento

<?php

$pdo = require 'connect.php';

$sql = 'insert into authors(first_name, last_name)
        values(:first_name,:last_name)';

$statement = $pdo->prepare($sql);

$author = [
	'first_name' => 'Mario',
	'last_name' => 'Rossi',
];

$statement->bindParam(':first_name', $author['first_name']);
$statement->bindParam(':last_name', $author['last_name']);

$author['first_name'] = 'Giorgio';
$author['last_name'] = 'Verdi';

// La query verrà eseguita con Giorgio Verdi, perché bindParam
// legge i valori delle variabili solo al momento dell'execute().
$statement->execute();

?>

Per creare un prepared statement in cui non sappiamo il numero di parametri possiamo usare la clausola IN

<?php

$pdo = require 'connect.php';

$list = [1, 2, 3];

$placeholder = str_repeat('?,', count($list) - 1) . '?'; //ripete la stringa '?,' per quanti elementi nell'array meno uno, poi aggiunge l'ultimo punto di domanda senza la virgola dopo

$sql = "SELECT book_id, title 
        FROM books 
        WHERE book_id IN ($placeholder)";

$statement = $pdo->prepare($sql);

$statement->execute($list);

//fetchAll ritorna tutti i risultati in una volta, come li ritorna dipende dal parametro, in questo caso un array associativo
//dove le chiavi sono le colonne della tabella. Altrimenti c'è FETCH_BOTH, FETCH_CLASS, FETCH_KEY_PAIR
$books = $statement->fetchAll(PDO::FETCH_ASSOC);

foreach ($books as $book) {
    echo '<h1>Titolo del libro: ' . $book['title'] . '</h1>';
}

//esempio con fetch invece di fetchAll. Fetch restituisce la riga corrente e sposta il cursore in avanti
//così carica in memoria una riga alla volta invece che tutte le righe insieme
//quando non ci sono più righe restituisce false
while ($book = $statement->fetch(PDO::FETCH_ASSOC)) {
    echo '<h1>Titolo del libro: ' . $book['title'] . '</h1>';
}

// esempio con FETCH_KEY_PAIR: richiede che la select ritorni esattamente due colonne
//la prima diventerà la chiave dell'array, la seconda il valore
$books = $statement->fetchAll(PDO::FETCH_KEY_PAIR);

echo "<select name="book" id="book">";
foreach ($books as $id => $title) {
    echo "<option value='$id'>$title</option>";
}
echo "</select>";

?>

Per fare sì che le righe restituite dalla query diventino oggetti della classe corrispondente si usa il metodo fetchObject:

<?php

class Publisher
{
    private $publisher_id;
    private $name;

    public function getName()
    {
        return $this->name;
    }
}

$pdo = require 'connect.php';

$sql = 'SELECT publisher_id, name 
        FROM publishers 
        WHERE publisher_id=:publisher_id';

$statement = $pdo->prepare($sql);
$statement->execute([':publisher_id' => 1]);

// il primo parametro è il nome della classe, se si omette di default è stdClass
//il secondo parametro è un array di valori da passare nel costruttore
$publisher = $statement->fetchObject('Publisher'); 

echo $publisher->name;

//altrimenti si può usare fetch() o fetchAll() settando il mode come FETCH_CLASS
$statement->setFetchMode(PDO::FETCH_CLASS, 'Publisher');

$publisher = $statement->fetch(); // Ritorna un oggetto Publisher
$altri = $statement->fetchAll();  // Ritorna un array di oggetti Publisher

?>

Per fare una select passando come parametro una parte di stringa possiamo usare la clausola LIKE 

<?php

$pdo = require 'connect.php';

$keyword = 'es';

$pattern = '%' . $keyword . '%';

$sql = 'SELECT book_id, title 
        FROM books 
        WHERE title LIKE :pattern'; //qui non usiamo il ?

$statement = $pdo->prepare($sql);

$statement->execute([':pattern' => $pattern]);

$books = $statement->fetchAll(PDO::FETCH_ASSOC);

foreach ($books as $book) {
    echo $book['title'] . '<br>';
}

?>

Esempio di select su una sola colonna:

<?php
$pdo = require 'connect.php';

$idDaCercare = 1;

$sql = "SELECT title, author FROM books WHERE book_id = :id";
$statement = $pdo->prepare($sql);

$statement->execute([':id' => $idDaCercare]);

//fetch ritornerebbe un array del tipo Array ( [title] => 'Il nome del libro',  [author] => 'Stephen King')
// fetchColumn ritorna solo il valore della prima colonna nella select come dato grezzo
// se si vuole avere un'altra colonna bisogna passare l'indice come parametro
$title = $statement->fetchColumn();
$author = $statement->fetchColumn(1);

if ($title) {
    echo "Il titolo del libro è: " . $title;
} else {
    echo "Nessun libro trovato con questo ID.";
}
?>

Per fare una select raggruppando i risultati in base al valore della prima colonna:

<?php
$pdo = require 'connect.php';

$publisherId = 1;

// La prima colonna sarà usata come chiave per il raggruppamento
$sql = "SELECT category, title 
        FROM books 
        WHERE publisher_id = :publisher_id";

$statement = $pdo->prepare($sql);
$statement->execute([':publisher_id' => $publisherId]);

// questo ritorna un array dove la chiave è la categoria
// e il valore è un array semplice di titoli
$books = $statement->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_COLUMN);

foreach ($books as $category => $titles) {
    echo "<h2>Categoria: " . $category . "</h2>";
    echo "<ul>";
    foreach ($titles as $title) {
        echo "<li>" . $title . "</li>";
    }
    echo "</ul>";
}

?>

Per eseguire una query di SELECT si può anche usare il metodo query() ma non è sicuro contro attacchi SQL injection.

Esempio di insert:

<?php

$pdo = require_once 'connect.php';

//insert di una riga
$name = 'Mondadori';

//insert di più righe
$names = [
	'Einaudi',
	'Rizzoli'
];
$sql = 'INSERT INTO publishers(name) VALUES(:name)';

$statement = $pdo->prepare($sql);

//caso insert di una riga
$statement->execute([
	':name' => $name
]);

/caso insert di più righe
foreach ($names as $name) {
	$statement->execute([
		':name' => $name
	]);
}

$publisher_id = $pdo->lastInsertId(); //ritorna l'ultimo id inserito

?>

Esempio di update

<?php

$pdo = require_once 'connect.php';

$publisher = [
	'publisher_id' => 1,
	'name' => 'Feltrinelli'
];

$sql = 'UPDATE publishers
        SET name = :name
        WHERE publisher_id = :publisher_id';

$statement = $pdo->prepare($sql);

$statement->bindParam(':publisher_id', $publisher['publisher_id'], PDO::PARAM_INT);
$statement->bindParam(':name', $publisher['name']);

//se qui cambiamo il valore di $publisher['name'] nella execute verrà passato il valore aggiornato

if ($statement->execute()) {
    if ($statement->rowCount() > 0) {
        echo 'Il record con ID ' . $publisher['publisher_id'] . ' è stato aggiornato con successo!';
    } else {
        echo 'Non sono stati eseguiti aggiornamenti. O il record con ID ' . $publisher['publisher_id'] . ' non esiste o il dato era già presente.';
    }
}

?>

Esempio di update con transaction, commit e rollback

<?php

$pdo = require_once 'connect.php';

$publisher = [
    'publisher_id' => 1,
    'name' => 'Feltrinelli'
];

$sql = 'UPDATE publishers
        SET name = :name
        WHERE publisher_id = :publisher_id';

try {
    $pdo->beginTransaction();

    $statement = $pdo->prepare($sql);
    $statement->execute([
        ':name'         => $publisher['name'],
        ':publisher_id' => $publisher['publisher_id']
    ]);

    // Conferma le modifiche
    $pdo->commit();

    if ($statement->rowCount() > 0) {
        echo 'Il record con ID ' . $publisher['publisher_id'] . ' è stato aggiornato con successo!';
    } else {
        echo 'Nessun aggiornamento eseguito.';
    }

} catch (Exception $e) {
    // in caso di errore esegui rollback delle modifiche
    $pdo->rollBack(); //rollback evita che la transazione rimanga appesa
    echo 'Errore: ' . $e->getMessage();
}

?>