Cztery kroki skryptu łączącego PHP z MySQL: mysqli_connect, mysqli_query, pętla z mysqli_fetch_array i mysqli_close. Funkcje ze ściągi CKE, wypisywanie wyników w liście i w tabeli, obrona przed SQL injection zapytaniem przygotowanym, hasła w password_hash oraz biblioteka PDO. Lekcja 12.8 kursu INF.03.
Biblioteka mysqli (MySQL Improved, ulepszony MySQL) to zestaw funkcji PHP, którymi skrypt łączy się z serwerem bazy danych, wysyła zapytania SQL i odbiera wiersze wyniku. Na egzaminie praktycznym jest to osobno oceniany rezultat: arkusz mówi wprost, że skrypt „łączy się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie egzamin" i że „na końcu działania skrypt zamyka połączenie z serwerem". W części pisemnej wracają pytania o nazwy i kolejność parametrów tych funkcji, o to, która z nich pobiera pojedynczy wiersz, oraz o ataki na aplikację internetową, z SQL injection na czele.
Cztery kroki skryptu z bazą
Skrypt bazodanowy zawsze ma ten sam szkielet: połącz, zapytaj, przeczytaj wiersze, zamknij. Każdy krok to jedna funkcja z tabeli dołączonej do arkusza.
Cztery kroki skryptu mysqli: połączenie, zapytanie, pętla czytająca wiersze jako tablicę z kluczami 0 i nazwa, na końcu zamknięcie połączenia
mysqli_connect (POŁĄCZ) otwiera połączenie i przyjmuje cztery parametry w stałej kolejności: adres serwera, nazwę użytkownika, hasło i nazwę bazy. Dla środowiska XAMPP z arkusza są to kolejno 'localhost', 'root', '' oraz 'egzamin', a puste apostrofy na trzecim miejscu znaczą konto bez hasła. Funkcja oddaje identyfikator połączenia, który każda następna funkcja dostaje jako pierwszy argument. Bazę można też wybrać osobno, przez mysqli_select_db($polaczenie, 'egzamin'). Nieudane połączenie oddaje false, a powód opisuje mysqli_connect_error, więc sprawdza się to od razu i kończy skrypt funkcją die (UMRZYJ). Polskie znaki wymagają jeszcze jednego wiersza: mysqli_set_charset($polaczenie, 'utf8mb4') ustawia kodowanie rozmowy z serwerem, bez niego zamiast liter ze znakami diakrytycznymi pojawiają się na stronie pytajniki.
mysqli_query (ZAPYTAJ) wysyła jedno zapytanie SQL. Dla SELECT oddaje wynik, czyli zestaw wierszy czekających na odczyt, a dla INSERT, UPDATE i DELETE oddaje samo true albo false, bo tam nie ma czego czytać. Wynik nie jest jeszcze danymi: wiersze wyjmuje z niego po jednym mysqli_fetch_array, a gdy wierszy już nie ma, funkcja oddaje null i pętla while sama się kończy.
Wiersz z mysqli_fetch_array ma dwa komplety kluczy naraz: numery kolumn liczone od zera oraz nazwy kolumn, więc ten sam produkt czyta się jako $w[0] i $w[1] albo przez $w['nazwa'] i $w['cena']. Który komplet powstanie, rozstrzyga drugi parametr: MYSQLI_NUM daje same numery, MYSQLI_ASSOC same nazwy, a MYSQLI_BOTH oba i to jest wartość domyślna. Krócej robią to samo mysqli_fetch_row, która daje tablicę indeksowaną, i mysqli_fetch_assoc, która daje asocjacyjną. Na koniec mysqli_close zwalnia połączenie i to ten wiersz arkusz sprawdza jako ostatni punkt rezultatu.
<?php
$polaczenie = mysqli_connect('localhost', 'root', '', 'egzamin');
if (!$polaczenie) {
die('Blad polaczenia: ' . mysqli_connect_error());
}
mysqli_set_charset($polaczenie, 'utf8mb4');
$wynik = mysqli_query($polaczenie, 'SELECT nazwa, cena FROM produkt');
while ($w = mysqli_fetch_array($wynik)) {
echo '<li>' . $w['nazwa'] . ', ' . $w['cena'] . ' zł</li>';
}
mysqli_close($polaczenie);
Skrypt wypisuje jedną pozycję <li> dla każdego wiersza tabeli produkt, a po ostatnim wierszu zamyka połączenie.
Funkcje mysqli ze ściągi CKE
Arkusz dołącza do zadania tabelę „Wybór funkcji języka PHP do obsługi bazy MySQL i MariaDB", więc nazw nie trzeba pamiętać co do znaku, ale trzeba wiedzieć, co która robi i co oddaje.
mysqli_connect
otwiera połączenie z serwerem bazy
identyfikator połączenia albo false
mysqli_connect_error
opisuje powód nieudanego połączenia
tekst komunikatu
mysqli_select_db
wybiera bazę na otwartym połączeniu
true albo false
mysqli_set_charset
ustawia kodowanie znaków w rozmowie z serwerem
true albo false
mysqli_query
wysyła jedno zapytanie SQL
wynik dla SELECT, true albo false dla zmian
mysqli_fetch_array
pobiera kolejny wiersz wyniku
tablicę z numerami i nazwami kolumn
mysqli_fetch_assoc
pobiera kolejny wiersz wyniku
tablicę z nazwami kolumn
mysqli_fetch_row
pobiera kolejny wiersz wyniku
tablicę z numerami kolumn od zera
mysqli_num_rows
liczy wiersze pobranego wyniku
liczbę wierszy
mysqli_num_fields
liczy kolumny pobranego wyniku
liczbę kolumn
mysqli_affected_rows
liczy wiersze zmienione ostatnim zapytaniem
liczbę wierszy
mysqli_insert_id
podaje klucz ostatnio wstawionego wiersza
liczbę z kolumny automatycznej
mysqli_error
opisuje błąd ostatniej operacji
tekst komunikatu
mysqli_close
zamyka połączenie z serwerem
true
Wypisanie wyników: lista i tabela
Wzór wyniku podaje arkusz, a skrypt ma go odtworzyć co do przecinka. Dwa najczęstsze wzory to lista numerowana i wiersze tabeli, a dane biorą się z tabeli produkt w bazie sklep, tej samej co w dziale o zapytaniach SELECT. <?php
$p = mysqli_connect('localhost', 'root', '', 'sklep');
$wynik = mysqli_query($p, 'SELECT nazwa, cena FROM produkt ORDER BY cena');
echo '<ol>';
while ($w = mysqli_fetch_assoc($wynik)) {
echo '<li>' . $w['nazwa'] . ', ' . $w['cena'] . ' zł</li>';
}
echo '</ol>';
mysqli_close($p);
1. Kabel HDMI, 25 zł
2. Mysz, 45 zł
3. Klawiatura, 120 zł
4. Monitor 24, 650 zł
5. Monitor 27, 990 zł
Numery pozycji rysuje przeglądarka, bo znacznik <ol> numeruje sam: skrypt wypisuje wyłącznie <li>, a kolejność bierze się z klauzuli ORDER BY, nie z kodu PHP. Gdy zapytanie nie znajdzie nic, pętla nie wykona się ani razu i lista zostanie pusta, dlatego przed nią sprawdza się mysqli_num_rows($wynik).
Drugi wzór wypełnia tabelę. Tu wygodniejsza jest mysqli_fetch_row: numery kolumn odpowiadają kolejności z SELECT.
<?php
$p = mysqli_connect('localhost', 'root', '', 'sklep');
$sql = 'SELECT nazwa, kategoria, sztuk FROM produkt WHERE sztuk > 0';
$wynik = mysqli_query($p, $sql);
echo '<table>';
while ($w = mysqli_fetch_row($wynik)) {
echo '<tr><td>' . $w[0] . '</td><td>' . $w[1] . '</td><td>' . $w[2] . '</td></tr>';
}
echo '</table>';
mysqli_close($p);
Klawiatura akcesoria 15
Monitor 24 monitory 4
Monitor 27 monitory 2
Trzy wiersze, a nie pięć: Mysz ma zero sztuk, więc odpada na warunku, a Kabel HDMI ma w kolumnie sztuk wartość NULL, której żadne porównanie nie spełnia. Gdy wzór łączy dane z dwóch tabel, na przykład imię klienta obok nazwy produktu, robi to zapytanie ze złączeniem JOIN, a PHP nadal tylko przepisuje wiersze do znaczników. Dane z formularza i SQL injection
Trzeci skrypt arkusza bierze wartość z pola formularza wysłanego metodą POST do tej samej strony i wstawia ją do klauzuli WHERE. Wartość czeka w tablicy $_POST pod kluczem równym atrybutowi name pola, co opisuje lekcja o formularzach w PHP. Wersja napisana wprost jest dziurawa. <?php
$p = mysqli_connect('localhost', 'root', '', 'sklep');
$kategoria = $_POST['kategoria'];
$sql = "SELECT nazwa, cena FROM produkt WHERE kategoria = '$kategoria'";
$wynik = mysqli_query($p, $sql);
while ($w = mysqli_fetch_assoc($wynik)) {
echo '<li>' . $w['nazwa'] . ', ' . $w['cena'] . ' zł</li>';
}
mysqli_close($p);
SQL injection (wstrzyknięcie SQL) to atak polegający na wpisaniu w pole formularza znaków, które zamykają wartość i dopisują do zapytania własny warunek. Skrypt skleja zapytanie z napisów, więc apostrof wpisany przez użytkownika przestaje być danymi i staje się składnią. Wpis ' OR '1'='1 daje warunek kategoria = '' OR '1'='1', prawdziwy dla każdego wiersza, więc lista pokazuje całą tabelę zamiast jednej kategorii, a ten sam chwyt w formularzu logowania przepuszcza intruza bez hasła. Wpis '; DROP TABLE produkt; -- zamyka apostrof, dokleja drugą instrukcję i znakiem komentarza -- unieważnia resztę wiersza: mysqli_query wykona tylko jedno polecenie i takie zapytanie odrzuci, ale mysqli_multi_query usunie tabelę naprawdę.
Obroną jest zapytanie przygotowane (prepared statement): do serwera idzie najpierw sam wzorzec ze znakami zapytania w miejscu wartości, a wartości osobno, więc serwer zna strukturę polecenia, zanim zobaczy dane, i żaden apostrof jej już nie zmieni. mysqli_prepare przygotowuje wzorzec, mysqli_stmt_bind_param podstawia wartości, mysqli_stmt_execute wykonuje, a mysqli_stmt_get_result oddaje wynik do znanej już pętli. Drugi parametr wiązania to typy kolejnych wartości: s dla napisu, i dla liczby całkowitej, d dla liczby z częścią dziesiętną.
<?php
$p = mysqli_connect('localhost', 'root', '', 'sklep');
$kategoria = $_POST['kategoria'];
$minimum = (int) $_POST['sztuk'];
$sql = 'SELECT nazwa, cena FROM produkt WHERE kategoria = ? AND sztuk >= ?';
$zapytanie = mysqli_prepare($p, $sql);
mysqli_stmt_bind_param($zapytanie, 'si', $kategoria, $minimum);
mysqli_stmt_execute($zapytanie);
$wynik = mysqli_stmt_get_result($zapytanie);
while ($w = mysqli_fetch_assoc($wynik)) {
echo '<li>' . $w['nazwa'] . ', ' . $w['cena'] . ' zł</li>';
}
mysqli_close($p);
Wypisana lista wygląda tak samo jak wcześniej, tylko apostrof wpisany w pole trafia do bazy jako zwykły znak i niczego nie zmienia. Minimum, które da się zrobić bez zapytań przygotowanych: liczbę rzutuje się na typ całkowity zapisem (int) $_POST['id'], a napis przepuszcza przez mysqli_real_escape_string($p, $_POST['kategoria']), która poprzedza apostrofy ukośnikiem. Wartość przepuszczona przez tę funkcję nadal wymaga apostrofów wokół siebie w zapytaniu.
Wstrzyknięcie SQL nie jest jedynym atakiem przez pole formularza. XSS (Cross Site Scripting, skrypt między witrynami) polega na wpisaniu w pole znacznika <script> z kodem JavaScript, który wykona się w przeglądarce każdego, kto potem otworzy stronę z tą wartością, i wykradnie na przykład ciasteczko z identyfikatorem sesji. Obroną jest htmlspecialchars($tekst), które zamienia znaki <, >, & i cudzysłów na encje, więc znacznik wyświetla się jako tekst, zamiast się wykonać. Trzeci atak, CSRF (Cross Site Request Forgery, fałszowanie żądania), podsuwa zalogowanemu użytkownikowi ukryty formularz wysyłający żądanie do cudzej aplikacji, a broni się przed nim losowym znacznikiem zapisanym w sesji.
Pułapka: walidacja HTML5 i sprawdzanie w JavaScript to wygoda dla użytkownika, nie zabezpieczenie: żądanie da się wysłać z pominięciem strony, więc dane sprawdza się jeszcze raz w skrypcie po stronie serwera.
Hasła użytkowników i dane dostępowe
Hasła nigdy nie leżą w bazie w postaci wpisanej przez użytkownika. Zapisuje się skrót kryptograficzny (hash), czyli wynik przeliczenia hasła funkcją, której nie da się odwrócić. Robi to password_hash($haslo, PASSWORD_DEFAULT): dokłada losową domieszkę (salt, sól), liczy skrót powolnym algorytmem i oddaje napis długości około 60 znaków, więc kolumna w bazie potrzebuje typu VARCHAR(255). To samo hasło zapisane dwa razy daje dwa różne skróty, więc skrótów nie porównuje się znak po znaku: robi to password_verify($haslo, $skrot), która oddaje true albo false.
<?php
session_start();
$login = $_POST['login'];
$zapytanie = mysqli_prepare($p, 'SELECT haslo FROM uzytkownik WHERE login = ?');
mysqli_stmt_bind_param($zapytanie, 's', $login);
mysqli_stmt_execute($zapytanie);
$w = mysqli_fetch_assoc(mysqli_stmt_get_result($zapytanie));
if ($w && password_verify($_POST['haslo'], $w['haslo'])) {
$_SESSION['login'] = $login;
}
Po udanym sprawdzeniu skrypt zapisuje login w tablicy $_SESSION, a kolejne podstrony pytają tylko, czy ten wpis istnieje. Mechanizm sesji opisuje lekcja o plikach, ciasteczkach i sesjach. Funkcje md5 i sha1 do haseł nie nadają się od dawna: liczą się bardzo szybko, nie mają domieszki, a dla popularnych haseł gotowe tablice skrótów oddają hasło od ręki. Dane dostępowe do bazy trzyma się w osobnym pliku dołączanym przez include, poza katalogiem publicznym serwera, żeby nikt nie pobrał ich adresem, i w tym samym miejscu trzyma się kopię zapasową bazy. PDO i inne sposoby połączenia
PDO (PHP Data Objects, obiekty danych PHP) to druga standardowa biblioteka bazodanowa PHP, obiektowa i wspólna dla wielu silników. Połączenie tworzy operator new, a rodzaj bazy, adres serwera i jej nazwę podaje się w jednym napisie.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=egzamin;charset=utf8mb4', 'root', '');
$zapytanie = $pdo->prepare('SELECT nazwa, cena FROM produkt WHERE id = ?');
$zapytanie->execute([5]);
$wiersz = $zapytanie->fetch(PDO::FETCH_ASSOC);
echo $wiersz['nazwa'];
Skrypt wypisuje Kabel HDMI, czyli nazwę produktu o kluczu 5. Wartości podaje się tablicą prosto w execute, bez osobnego wiązania i bez podawania typów, fetch(PDO::FETCH_ASSOC) oddaje jeden wiersz jako tablicę asocjacyjną, a wszystkie naraz fetchAll. Przejście na PostgreSQL albo SQLite to zmiana samego napisu z adresem i na tym polega przewaga PDO nad mysqli, która obsługuje wyłącznie MySQL i MariaDB.
Sama mysqli ma też wersję obiektową: połączenie tworzy new mysqli('localhost', 'root', '', 'egzamin'), zapytanie wysyła $polaczenie->query($sql), wiersz oddaje $wynik->fetch_assoc(), a zamyka $polaczenie->close(). Nazwy są te same, tylko zapisane po kropce, i o tę wersję pytają zadania wspominające pole insert_id. Najstarszy zestaw mysql_connect, mysql_query i mysql_fetch_row bez litery i usunięto z PHP 7, choć w bankach pytań jeszcze się pojawia.
Na egzaminie
Pytania pisemne o połączenie z bazą mają kilka stałych kształtów. Pierwszy to parametry: w zapisie mysqli_connect('localhost', 'root', '', 'zwierzaki') idą po kolei adres serwera, nazwa użytkownika, hasło i nazwa bazy, więc pytanie o wartość na trzecim miejscu ma odpowiedź „hasło użytkownika". Drugi to wybór funkcji: pracę z bazą otwiera mysqli_connect, a kończy mysqli_close, nie exit, die ani mysqli_commit. Trzeci to rodzaj tablicy z wierszem: mysqli_fetch_row oddaje tablicę indeksowaną, mysqli_fetch_assoc asocjacyjną, a mysqli_fetch_array obie naraz. Czwarty to liczby: liczbę wierszy wyniku podaje mysqli_num_rows, liczbę kolumn mysqli_num_fields, a klucz świeżo wstawionego wiersza mysqli_insert_id. Piąty to zapytanie w ramce: INSERT INTO dane VALUES (NULL, '$a', '$b', '$c') wstawia wiersz, a NULL zostawia kolumnę klucza automatycznego serwerowi. Szósty to inny silnik: pg_connect łączy z bazą PostgreSQL, nie z MySQL. Siódmy to kodowanie polskich znaków, za które odpowiada mysqli_set_charset, oraz komunikat błędu z mysqli_error i jego numer z mysqli_errno. Ósmy to bezpieczeństwo: atak polegający na wpisaniu w pole formularza fragmentu zapytania to SQL injection, broni przed nim zapytanie przygotowane, a hasła zapisuje się skrótem z password_hash i sprawdza przez password_verify.
Pułapka: mysqli_query oddaje wynik, a nie wiersze. Pojedynczy wiersz wychodzi dopiero z mysqli_fetch_array, mysqli_fetch_assoc albo mysqli_fetch_row, wywołanej w pętli, bo jedno wywołanie daje jeden wiersz.
W zadaniu praktycznym skrypt połączenia z bazą jest osobno ocenianym rezultatem, a jego początek jest za każdym razem ten sam: localhost, użytkownik root, puste hasło, nazwa bazy podana w arkuszu i mysqli_close na końcu. Zapytania idą do bazy zaimportowanej wcześniej w phpMyAdmin, a wynik ma trafić na stronę dokładnie w postaci ze wzoru: te same przecinki, ta sama jednostka, ta sama kolejność kolumn. Plik .php musi leżeć w katalogu htdocs i zostać otwarty adresem http://localhost/..., bo prosto z dysku nie wykona się wcale i zamiast listy produktów w oknie przeglądarki zostanie kod albo pusta strona. Ściąga
mysqli_connect('localhost', 'root', '', 'egzamin')
serwer, użytkownik, hasło, baza
mysqli_select_db($p, 'egzamin')
wybór bazy na otwartym połączeniu
mysqli_connect_error()
powód nieudanego połączenia
mysqli_set_charset($p, 'utf8mb4')
kodowanie polskich znaków
mysqli_query($p, $sql)
wysyła zapytanie, oddaje wynik SELECT
mysqli_fetch_array($w)
wiersz z kluczami liczbowymi i nazwami kolumn
mysqli_fetch_assoc($w)
wiersz z nazwami kolumn
mysqli_fetch_row($w)
wiersz z numerami kolumn od zera
mysqli_num_rows($w)
liczba wierszy wyniku
mysqli_num_fields($w)
liczba kolumn wyniku
mysqli_affected_rows($p)
liczba wierszy zmienionych przez UPDATE
mysqli_insert_id($p)
klucz ostatnio wstawionego wiersza
mysqli_error($p)
komunikat o błędzie ostatniej operacji
mysqli_close($p)
zamknięcie połączenia, ostatni wiersz skryptu
mysqli_prepare($p, $sql)
wzorzec zapytania ze znakami zapytania
mysqli_stmt_bind_param($z, 'si', $a, $b)
podstawienie wartości, s napis, i liczba
mysqli_real_escape_string($p, $t)
ucieczka apostrofów w napisie z formularza
htmlspecialchars($t)
obrona przed XSS przy wypisywaniu na stronę
password_hash($h, PASSWORD_DEFAULT)
skrót hasła do zapisania w bazie
password_verify($h, $skrot)
sprawdzenie hasła przy logowaniu
new PDO('mysql:host=localhost;dbname=egzamin', 'root', '')
połączenie biblioteką PDO
$zapytanie->execute([$id])
wykonanie zapytania PDO z wartością
$zapytanie->fetch(PDO::FETCH_ASSOC)
jeden wiersz jako tablica asocjacyjna
Pobierz ściągę PDF