W tym artykule wprowadzę Cię do obsługi baz danych w PHP za pośrednictwem PDO. PDO to skrót od (PHP Data Object). Wpis dedykuję osobom, które znają podstawy PHP i baz danych SQL. Choć nie będę tłumaczył elementarnych pojęć, postaram się niczego nie pomijać dlatego ewentualne braki mogą być szybko nadrobione. Kurs ma zachęcić do dalszej nauki i nowoczesnego korzystania z baz danych za pośrednictwem PDO i paradygmatu obiektowego.

Dlaczego PDO a nie mysqli? Jeżeli opanujemy PDO API, będziemy mogli korzystać także z innych baz danych – różnica może dotyczyć potem tylko składni zapytań. PDO jest wygodniejszy, bezpieczniejszy i nowszy. Automatycznie zabezpiecza przed SQL Injection i zwalania z konieczności filtrowania zmiennych z wykorzystaniem odpowiednika mysqli_real_escape_string(); Wiele książek dotyczących programowania w środowisku PHP wykorzystuje zarówno paradygmat obiektowy jak i PHP Data Objects.

Po skończeniu tego kursu nikt Ci nie zabrania łączyć się z bazą danych za pomocą mysqli. Sam pisałem kod proceduralny i łączyłem się za pomocą mysqli i tworzyłem użyteczne programy, które służą klientom do dziś. Uważam jednak, że każdy profesjonalny programista PHP powinien znać (niekoniecznie zawsze używać) oba API aby swobodnie programować i posługiwać się bazami danych w różnych projektach.

Spis treści – kurs PHP od podstaw

Ten ciągle aktualizowany kurs jest podzielony na 8 rozdziałów.

  1. PHP – podstawy programowania
  2. Programowanie obiektowe PHP 
  3. Bazy danych SQL (PDO API) < jesteś tutaj
  4. Interfejsy API i integracje
  5. Wzorce projektowe i frameworki
  6. Wydajność
  7. Skalowalność
  8. Bezpieczeństwo

2. Bazy danych MySQL w PHP (PDO API)

Bazy danych w PHP są stosowane po to aby trwale przechowywać pewne dane. Ponieważ sieć HTTP z natury ma bezstanowy charakter, przy każdym odświeżeniu aktualnej podstrony, wszelkie wartości zmiennych i wprowadzone przez użytkownika dane są wyczyszczone z pamięci. Aby symulować sytuację, że serwer „coś pamięta” z poprzednich żądań, musi wykorzystać którąś z technik do przechowywania danych:

  • cookies (po stronie użytkownika),
  • sesje (czyli cookies po stronie serwera),
  • system plików,
  • relacyjna baza danych (MySQL, PostreSQL),
  • wszelkie bazy NoSQL (memcached, Redis, MongoDB)

Choć wszystkie te techniki mają swoje zastosowania i przypadki użycia, baza danych z pośród tych wszystkich technik daje ogólnie największe możliwości. Nadaje się do przechowywania dużego wolumenu danych, zapewnia współbieżność pozwalając wielu użytkownikom korzystać z bazy jednocześnie i umożliwia wygodnie wyciągać dane za pomocą języka SQL.

W kontekście programowania obiektowego przyda nam się możliwość korzystania z bazy danych a w szczególności z rozszerzenia PDO (PHP Data Object).

W ramach tego kursu PHP, stworzymy mini-serwis z bazą filmów. Jeżeli interesuje was inne tematy, bardzo łatwo można przerobić ten przykład na bazę z grami, książkami albo muzyką. Tabele można „wyklikać” w phpMyAdmin lub użyć poniższego kodu.

CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR( 255 ) NOT NULL)
CREATE TABLE movies ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR( 255 ) NOT NULL, description TEXT, category_id INT, year INT(4), created DATETIME)

Jeżeli nie wiesz co oznacza INT lub VARCHAR sprawdź osobny artykuł z listą wszystkich typów danych w MySQL.

Dodajmy przykładowe kategorie filmów:

INSERT INTO categories (name) VALUES ('Akcja');
INSERT INTO categories (name) VALUES ('Sci-Fi');
INSERT INTO categories (name) VALUES ('Horror'); 
INSERT INTO categories (name) VALUES ('Fantasy');
INSERT INTO categories (name) VALUES ('Komedia');
INSERT INTO categories (name) VALUES ('Animacja');
INSERT INTO categories (name) VALUES ('Muzyczny'); 

Teraz dodajmy jakieś filmy:

INSERT INTO movies (name, description, category_id, year, created) VALUES ('Interstellar', 'Zmiany klimatyczne zmuszają ludzi do szukania nowego domu we Wszechświecie.', 2, '2014', NOW());

INSERT INTO movies (name, description, category_id, year, created) VALUES ('Resident Evil: The Final Chapter', 'Alicja wyrusza w podróż aby zniszczyć Cezrwoną Królową.', 3,'2016', NOW());

INSERT INTO movies (name, description, category_id, year, created) VALUES ('A Star Is Born', 'Historia romansu pomiędzy gwiazdą muzyki country a nieznaną piosenkarką.', 7,'2018', NOW());

Tak powinna wyglądać zawartość tabeli:

Zawartość tabeli movies

Rozszerzenie PDO w PHP

Jeżeli do tej pory korzystałeś z mysql w PHP używałeś zapewne funkcji typu mysqli_connect(). Nowszym i lepiej zgranym z programowaniem orientowanym obiektowo jest jest PDO. PDO ujednolica łączenie się z wieloma różnymi bazami danych i to jest jego największa zaleta.

PDO jest kolejną warstwą abstrakcji i jest etapem pośrednim pomiędzy kodem PHP a kodem SQL. Dzięki temu jest też rozwiązaniem nieco bezpieczniejszym utrudniając ataki SQL injection. Wady i zalety PDO omówimy sobie kiedyś w osobnym artykule.

Aby połączyć się z naszą bazą danych, należy utworzyć obiekt PDO:

$dbhost = "localhost";
$dbname = "kursphp";
$dbuser = "root";
$dbpassword = "";
$db_conn = new PDO("mysql:host=".$dbhost.";dbname=".$dbname, $dbuser, $dbpassword);

Pobieranie danych (SELECT)

Teraz możemy pobrać listę filmów za pomocą obiektu klasy PDOStatement. Klasa ta odzwierciedla zapytanie i umożliwia pobranie wyników. Do wykonania nieskomplikowanego żądania można użyć metody PDO::query();

$stmt = $db_conn->query('SELECT name, year FROM movies'); 

Wyniki wyświetlimy za pomocą:

while($row = $stmt->fetch()) {  echo $row['name'] . ': rok ' . $row['year'] . "\n"; } 
Wynik powyższego zapytania

Pętla while pokazuje kolejne wyniki, ponieważ każde kolejne wywołanie metody fetch zwraca nastepny wynik. Można pobrać wszystkie na raz za pomocą fetchAll().

PDO pozwala nam korzystać z kilku trybów pobierania danych i obie metody – fetch() i fetchAll() – mogą przyjmować argument fetch_style:

  • PDO::FETCH_ASSOC – zwraca tablicę z kluczami według nazw kolumn,
  • PDO::FETCH_NUM zwraca tablicę z kluczami liczbowymi,
  • PDO::FETCH_BOTH: kombinacja dwóch powyższych,
  • PDO::FETCH_CLASS – zwraca obiekt a dane są wartościami w polach nazwanych według naz kolumn,

Za pomocą znanej nam już instrukcji print_r() można sobie sprawdzić jak wygląda struktura danych w różnych trybach, np:

print_r($result = $stmt->fetchAll(PDO::FETCH_CLASS));

Zwróci nam tablicę obiektów:

Instrukcje preparowane

Instrukcje preparowane (ang. prepared statements) pozwalają na wykorzystanie zmiennych wewnątrz zapytania. Dzieli zapytanie na to co znajduje się pomiędzy SELECT a WHERE oraz to co znajduje się po słowie WHERE:

$sql = "SELECT name, description, year  FROM movies WHERE id = :movie_id";
$stmt = $db_conn->prepare($sql);
$stmt->execute(array(
   "movie_id" => 1)
);
$movie = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($movie);

Zauważ, że tymczasowa etykieta „movie_id” jest dowolna. Zapytanie złożone z dwóch zmiennych będzie wyglądać tak:

$sql = "SELECT name, description, year, category_id FROM movies WHERE year = :movie_year AND category_id = :movie_category_id";
$stmt = $db_conn->prepare($sql);
$stmt->execute(array("movie_year" => 2014, "movie_category_id" => 2));

Tymczasowe nazwy movie_id, movie_category_id mogą być pominięte na rzecz pytajników (bez dwukropka) wtedy nie trzeba ponownie wpisywać etykiet a tylko zachowujemy kolejność w tablicy z parametrami – ta technika choć może być na początku mniej czytelna znacznie skraca kod przy złożonych zapytaniach.

$sql = "SELECT name, description, year, category_id FROM movies WHERE year = ? AND category_id = ?";
$stmt = $db_conn->prepare($sql);
$stmt->execute(array(2014, 2));

Jest jeszcze jedna zaleta korzystania z instrukcji preparowanych. Argumenty wprowadzane do metody prepare są już odpowiednio filtrowane. Kiedy używasz proceduralnego mysqlI_query pewnie używałeś instrukcji mysqli_real_escape_string(). Tutaj nie jest to wymagane – filtrowanie tego typu dzieje się automatycznie!

Jest jeszcze jedna technika łączenia wartości z instrukcjami preparowanymi, która przyda się przy wielokrotnym używaniu podobnych zapytań. Służy do tego metoda bindValue();

$sql = "SELECT name, description, year, category_id FROM movies WHERE year = :movie_year AND category_id = :movie_category_id";
 $stmt = $db_conn->prepare($sql);
 $stmt->bindValue(':movie_year', 2014);
 
 $stmt->bindValue(':movie_category_id', 2);
 $stmt->execute();
 $scifi = $stmt->fetch();
 print_r($scifi);
 
$stmt->bindValue(':movie_category_id', 1);
 $stmt->execute();
 $action = $stmt->fetch();
 print_r($action);

To rozwiązanie wygląda na jeszcze wygodniejsze, jeżeli posłużymy się zmienną i metodą bindParam:

$sql = "SELECT name, description, year, category_id FROM movies WHERE year = :movie_year AND category_id = :movie_category_id";
 $stmt = $db_conn->prepare($sql);
 $stmt->bindValue(':movie_year', 2014);
$stmt->bindParam(':movie_category_id', $cat);
$cat = 2;
 $stmt->execute();
 $scifi = $stmt->fetch();
 print_r($scifi);
$cat = 1;
 $stmt->execute();
 $action = $stmt->fetch();
 print_r($action);

Dodawanie danych (INSERT)

Nauczyliśmy się odczytywać dane za pomocą PDO teraz czas na zapis.

$sql = 'INSERT INTO movies (name, description, year, created)
 VALUES (:name, :description, :year, NOW())';
$stmt = $db_conn->prepare($sql);
$stmt->execute(array(
 ':name' => 'Oblivion',
 ':description' => 'Akcja dzieje się w 2077 roku. Jack Harper (Tom Cruise) odkrywa prawdę która zmienia jego światopogląd.',
 ':year' => 2013)
 );

Za pomocą metody rowCount(); możemy sprawdzić ile wierszy zostało zmodyfikowanych.

 echo 'Zmieniono '.$stmt->rowCount().' rekordów'; 

Edycja danych (UPDATE)

Zapomnieliśmy o dodaniu kategorii. Oblivion to film z gatunku Sci-Fi dlatego dodajemy odpowiednią kategorię:

 $sql = 'UPDATE movies SET category_id = :category_id  WHERE id = :movie_id'; 
$stmt = $db_conn->prepare($sql); $stmt->execute(array(':category_id' => 2, ':movie_id' => 5)); 
echo 'Zmieniono '.$stmt->rowCount().' rekordów';

Ponowne uruchomienie tego skryptu zwróci nam informację że zmieniono 1 rekord. Dzięki temu widać jak działa metoda rowCount();

Usuwanie danych (DELETE)

$stmt = $db_conn->prepare('DELETE FROM movies WHERE name = :name'); 
$stmt->execute(array(':name' => 'Oblivion')); 
echo 'Usunięto '.$stmt->rowCount().' rekordów';

Obsługa błędów w PDO API

Baza danych jest serwisem zewnętrznym, taką czarną skrzynką – czymś co może nie zadziałać z jakiegoś powodu. Aby zapewnić wysoką dostępność, jakość i niezawodność aplikacji, w której korzystamy z baz danych należy odpowiednio obsługiwać błędy. Baza danych udziela nam częściowych informacji o ewentualnych błędach dlatego nie raz trzeba „na własną rękę” sprawdzać czy komunikacja wykonuje się prawidłowo.

Na początek powróćmy na chwilę do samego połączenia, bo te dobrze jest otoczyć w klamry try-catch:

$dbhost = "localhost";
$dbname = "kursphp";
$dbuser = "root";
$dbpassword = "";
try{
$db_conn = new PDO("mysql:host=".$dbhost.";dbname=".$dbname, $dbuser, $dbpassword);
} catch (PDOException $e){
echo "Błąd połączenia z bazą danych";
}

Dzięki takiej instrukcji wyłapiemy błąd kiedy baza nie będzie z jakiegoś powodu odpowiadać lub nie będą zgadzać się dane uwierzytelniające.

Kiedy używaliśmy metody prepare() to też wierzyliśmy, że zwróci ona obiekt klasy PDO statement. Jeżeli wystąpi jakiś problem zwróci false dlatego każde użycie metody prepare można otoczyć instrukcją if:

try {
  $stmt = $db_conn->prepare($sql);
  if($stmt) {
  $stmt->execute(array(
  "movie_id" => 1)
  );
  $movie= $stmt->fetch();
  }
 } catch (PDOException $e) {
  echo "Wystąpił błąd z pobieraniem danych:".$e->getMessage();
 }

Jeżeli wystąpi błąd przy wykonywaniu zapytań metoda execute() zwróci false, dlatego przed linią $movie= $stmt->fetch(); można dodać kolejną instrukcję if:

try {
  $stmt = $db_conn->prepare($sql);
  if($stmt) {
  $result = $stmt->execute(array(
  "movie_id" => 1)
  );
  if($result){
    $movie= $stmt->fetch();
   }else{
    $error = stmt->errorInfo();
    echo "Błąd zapytania:". $error[2];
   }
  }
 } catch (PDOException $e) {
  echo "Wystąpił błąd z pobieraniem danych:".$e->getMessage();
 }

metoda errorInfo() zwraca tablicę:

  • w pierwszym polu [0] jest zawarte zapytanie SQL,
  • w drugim polu [1] kod błędu
  • w trzecim polu [2] komunikat o błędzie

Podsumowanie

Oto wprowadzenie do baz danych w obiektowym PHP. Kolejne rozdziały będą opublikowane prawdopodobnie w osobnych artykułach. Niniejszy, stale aktualizowany kurs PDO to wstęp dla osób, które chciałby by poznać chociaż w podstawowym stopniu programowanie baz danych w obiektowym PHP.

Źródła

https://www.php.net/manual/en/book.pdo.php

Oceń artykuł na temat: Kurs PDO – bazy danych w obiektowym PHP (tutorial)
Średnia : 4.7 , Maksymalnie : 5 , Głosów : 41