17. PHP – współpraca z bazami danych


Szybki quiz – przypomnienie poprzedniej lekcji

Lekcja 15: klasy i obiekty. Odpowiedz bez uruchamiania kodu.

1. Co wyświetli ten kod?

class Licznik
{
    private int $wartosc = 0;

    public function zwieksz(): void
    {
        $wartosc++;
    }

    public function pobierz(): int
    {
        return $this->wartosc;
    }
}

$licznik = new Licznik();
$licznik->zwieksz();
$licznik->zwieksz();
echo $licznik->pobierz();

2. Zadanie ma właściwość private string $tytul i metodę pobierzTytul(). Co się stanie po wykonaniu echo $zadanie->tytul;?

3. Czy ten kod utworzy poprawny obiekt? Dlaczego?

$zadanie = new Zadanie(tytul: 'SQL', id: 7, opis: '', termin: '2026-10-01', wykonane: false, xp: 20);

4. Co wyświetli ostatnia linia, jeśli $kama ma 99 XP?

$ktos = $kama;
$ktos->dodajXp(10);
echo $kama->pobierzXp();

5. Co wyświetli ten kod, jeśli $uzytkownik ma 99 XP?

try {
    $uzytkownik->dodajXp(-5);
} catch (Exception $wyjatek) {
    echo 'Błąd: ' . $wyjatek->getMessage();
}

echo ' XP: ' . $uzytkownik->pobierzXp();

6. Znajdź błąd. Metoda się wykonuje, nie ma komunikatu, a XP użytkownika się nie zmienia.

public function dodajXp(int $xp): void
{
    $this->xp + $xp;
}

7. Podaj dwie różnice między klasą abstrakcyjną a interfejsem.

Odpowiedzi
  1. 0. W metodzie zwieksz() brakuje $this->. PHP zwiększa lokalną zmienną $wartosc (z ostrzeżeniem o nieistniejącej zmiennej), która znika po wyjściu z metody. Właściwość obiektu pozostaje 0.
  2. Błąd krytyczny: Cannot access private property. Właściwość jest private, więc z zewnątrz dostępna jest tylko przez metodę pobierzTytul().
  3. Tak. Przy argumentach nazwanych kolejność nie ma znaczenia — liczą się nazwy parametrów. Muszą być podane wszystkie parametry bez wartości domyślnej.
  4. 109. Przypisanie $ktos = $kama nie tworzy kopii obiektu — obie zmienne wskazują ten sam obiekt.
  5. Błąd: Liczba XP do dodania musi być większa od zera. XP: 99. Wyjątek przerywa metodę przed zmianą XP.
  6. Wynik dodawania nie zostaje nigdzie zapisany. Powinno być $this->xp += $xp; (czyli $this->xp = $this->xp + $xp;).
  7. Na przykład: klasa abstrakcyjna może zawierać właściwości i gotowe implementacje metod, interfejs tylko ich sygnatury (i stałe); klasa może dziedziczyć po jednej klasie abstrakcyjnej, ale implementować wiele interfejsów.

TaskQuest

Dodatek

Najwygodniejszym i optymalnym miejscem do przechowywania dużej ilości informacji są bazy danych.

Wyobraź sobie aplikację jak Discord, Instagram czy system do zarządzania serwerem gier – wszystkie te platformy muszą gdzieś przechowywać miliony danych użytkowników, wiadomości, postów czy statystyk graczy. Do tego właśnie służą bazy danych.

Aby PHP mógł korzystać z bazy danych, muszą być spełnione podstawowe warunki:

  • Serwer bazy danych musi być zainstalowany i uruchomiony
  • PHP musi posiadać dane dostępowe do połączenia (host, użytkownik, hasło, nazwa bazy)
  • Odpowiednie rozszerzenie PHP musi być włączone (MySQLi lub PDO)

Sposoby komunikacji z bazą danych

W historii PHP istniały 3 sposoby łączenia się z bazą MySQL:

1. mysql (stare rozszerzenie) – USUNIĘTE

  • Zdeprecjonowane: PHP 5.5.0 (2013)
  • Całkowicie usunięte: PHP 7.0.0 (2015)
  • Funkcje: mysql_connect(), mysql_query() itp.
  • Status: NIE UŻYWAJ – nie działa w nowoczesnych wersjach PHP

2. MySQLi (MySQL Improved) – AKTYWNE

MySQLi to wersja poprawiona i aktualna wycofanej od PHP7 biblioteki mysql.

Cechy MySQLi:

  • Może być stosowane w podejściu proceduralnym i obiektowym
  • Współpracuje tylko z bazami MySQL/MariaDB
  • Wspiera prepared statements (ochrona przed SQL Injection)
  • Obsługuje transakcje
  • Dostępne w PHP 5, 7, 8 i nowszych

Kiedy używać MySQLi:

  • Gdy na pewno będziesz używać tylko MySQL/MariaDB
  • Gdy pracujesz ze starszym kodem proceduralnym
  • Gdy potrzebujesz specyficznych funkcji MySQL

3. PDO (PHP Data Objects) – AKTYWNE

PDO to rozszerzenie w pełni obiektowe, które współpracuje z wieloma silnikami bazy danych, nie tylko MySQL.

Cechy PDO:

  • Uniwersalny interfejs dla różnych baz danych (MySQL, PostgreSQL, SQLite, Oracle, MS SQL Server…)
  • Tylko tryb obiektowy
  • Named parameters (:nazwa) – bardziej czytelne zapytania
  • Prepared statements domyślnie
  • Łatwa zmiana bazy danych bez przepisywania kodu

Kiedy używać PDO:

  • Dla większości nowych projektów (zalecane!)
  • Gdy planujesz możliwość zmiany bazy danych w przyszłości
  • Gdy budujesz uniwersalną aplikację
  • Gdy preferujesz czysty kod obiektowy

PDO vs MySQLi – Porównanie

CechaPDOMySQLi
Obsługiwane bazyMySQL, PostgreSQL, SQLite, Oracle, MS SQL…Tylko MySQL/MariaDB
Styl programowaniaTylko obiektowyProceduralny + obiektowy
Prepared statementsNamed (:id) + positional (?)Tylko positional (?)
WydajnośćBardzo dobraNieznacznie szybsze dla MySQL
Łatwość użyciaBardzo intuicyjneIntuicyjne
PopularnośćBardzo popularneBardzo popularne

Rekomendacja: Dla nowych projektów zalecamy PDO ze względu na uniwersalność i czytelność kodu.

MySQL i MariaDB

RDBMS (Relational Database Management System)

RDBMS to system zarządzania relacyjnymi bazami danych – dane przechowywane są w tabelach, które mogą być ze sobą powiązane relacjami (np. użytkownik → posty, gracz → statystyki).

MySQL i MariaDB są właśnie takimi systemami.

Historia i różnice

MySQL:

  • Stworzony w 1995 roku
  • Od 2010 roku należy do Oracle Corporation
  • Najpopularniejszy system open-source do zarządzania bazami danych
  • Używany przez: Facebook, Twitter, YouTube, Wikipedia

MariaDB:

  • Fork MySQL utworzony w 2009 roku przez oryginalnych twórców MySQL
  • Powstała jako alternatywa po przejęciu MySQL przez Oracle
  • Licencja: GPL (w pełni open-source)
  • Kompatybilna z MySQL (drop-in replacement do wersji 5.5)
  • Szybszy rozwój i częstsze aktualizacje
  • Oferuje lepsze silniki obsługi pamięci masowej (Aria, ColumnStore, MyRocks)

Co wybrać?

Oba rozwiązania są darmowe i świetnie sprawdzają się w nauce i projektach komercyjnych.

MySQL – jeśli:

  • Pracujesz z narzędziami/aplikacjami wymagającymi MySQL
  • Potrzebujesz wsparcia komercyjnego od Oracle
  • Używasz starszej dokumentacji/tutoriali

MariaDB – jeśli:

  • Zależy Ci na pełnej otwartości (GPL)
  • Chcesz nowsze funkcje i szybsze aktualizacje
  • Budujesz nowy projekt

Dla nauki: Oba systemy są prawie identyczne w użyciu – składnia SQL jest taka sama, więc nauka na jednym oznacza umiejętność pracy z drugim.

Więcej o tych dwóch systemach możecie przeczytać na stronie: https://hostclub.pl/mariadb-vs-mysql-porownanie-baz-danych/

Podstawowe pojęcia

Zanim przejdziemy do łączenia z bazą, poznaj kluczowe pojęcia:

baza danych (database) – Zorganizowany zbiór danych. Przykład: baza discord_app zawiera wszystkie dane aplikacji Discord.

tabela (table) – Struktura przechowująca dane w formie wierszy i kolumn. Przykład: tabela users przechowuje dane użytkowników.

kolumna (column/field) – Pojedyncze pole w tabeli. Przykład: username, email, created_at.

wiersz (row/record) – Pojedynczy rekord w tabeli. Przykład: jeden użytkownik to jeden wiersz.

klucz główny (primary key) – Unikalne pole identyfikujące wiersz. Przykład: id użytkownika.

SQL (Structured Query Language) – Język używany do komunikacji z bazą danych.

Podstawowe operacje (CRUD):

  • CREATE (INSERT) – dodawanie danych
  • READ (SELECT) – odczytywanie danych
  • UPDATE – aktualizacja danych
  • DELETE – usuwanie danych

Tworzenie bazy danych – dwa sposoby

1. Graficznie w phpMyAdmin

phpMyAdmin to narzędzie webowe do zarządzania bazami MySQL/MariaDB (dostępne w XAMPP, WAMP, na hostingu).

Kroki:

  1. Otwórz phpMyAdmin (zwykle http://localhost/phpmyadmin)
  2. Kliknij zakładkę „Bazy danych”
  3. Wpisz nazwę bazy (np. social_media_app)
  4. Wybierz kodowanie: utf8mb4_unicode_ci (obsługa emoji i polskich znaków)
  5. Kliknij „Utwórz”

2. Przez kod PHP

Możesz również tworzyć bazy bezpośrednio z poziomu PHP (przydatne przy automatyzacji).

Szczegóły w dalszej części materiału.

Co dalej?

W kolejnych częściach nauczysz się:

  • Łączyć się z bazą danych (PDO i MySQLi)
  • Wykonywać zapytania SQL (SELECT, INSERT, UPDATE, DELETE)
  • Zabezpieczać aplikację przed SQL Injection
  • Obsługiwać błędy połączenia
  • Pracować z prepared statements
  • Budować praktyczne aplikacje (system logowania, chat, panel gracza)

Jak sprawdzić czy masz włączone rozszerzenia w kodzie PHP

<?php
// Sprawdź MySQLi
if (extension_loaded('mysqli')) {
    echo "MySQLi jest dostępne<br>";
} else {
    echo "MySQLi NIE jest dostępne<br>";
}

// Sprawdź PDO
if (extension_loaded('pdo_mysql')) {
    echo "PDO MySQL jest dostępne<br>";
} else {
    echo "PDO MySQL NIE jest dostępne<br>";
}

// Lista wszystkich dostępnych sterowników PDO
echo "<br>Dostępne sterowniki PDO:<br>";
print_r(PDO::getAvailableDrivers());
?>

Jeśli coś nie działa, sprawdź plik php.ini i odkomentuj linie:

extension=mysqli
extension=pdo_mysql

Przykłady zastosowań baz danych

Żeby lepiej zrozumieć do czego służą bazy danych, zobacz przykłady z życia codziennego:

Aplikacja do streamingu muzyki (typ Spotify)

Tabele:

  • users – użytkownicy (id, username, email, premium)
  • songs – utwory (id, title, artist, duration, file_path)
  • playlists – playlisty (id, user_id, name, created_at)
  • playlist_songs – piosenki w playlistach (playlist_id, song_id)
  • listening_history – historia odsłuchań (user_id, song_id, played_at)

System zarządzania serwerem gier (typ Minecraft)

Tabele:

  • players – gracze (id, username, uuid, first_join, last_seen)
  • player_stats – statystyki (player_id, kills, deaths, playtime, level)
  • inventory – ekwipunek (player_id, item_id, quantity, slot)
  • transactions – zakupy (id, player_id, item, price, date)
  • bans – bany (player_id, reason, banned_by, expires_at)

Panel Discord Bot

Tabele:

  • guilds – serwery (id, name, owner_id, prefix, premium)
  • guild_settings – ustawienia (guild_id, welcome_channel, log_channel, auto_role)
  • commands_usage – statystyki (guild_id, command_name, uses, last_used)
  • warnings – ostrzeżenia (user_id, guild_id, reason, moderator_id, date)

Przydatne linki

TaskQuest 1.7 — projektujemy bazę danych TaskQuest

KARTA PRACY

Po Lekcji 15 zadanie i użytkownik są w TaskQuest obiektami. Jednak dane nadal nie mają prawdziwego domu. Zadania leżą w jednym pliku CSV, wspólnym dla wszystkich, którzy się zalogują. XP, streak i data ostatniej aktywności są wpisane w kod index.php. Metody dodajXp() i oznaczJakoWykonane() istnieją, ale ich efekt znika razem z końcem żądania.

Dziś zaprojektujemy i zbudujemy bazę danych TaskQuest. Będzie w niej miejsce na wielu użytkowników, każdy z własnymi zadaniami, XP i streakiem.

Cel lekcji

Nauczyć się:

  • czym jest relacyjna baza danych i czym różni się od pliku CSV,
  • podstawowych pojęć: tabela, kolumna, wiersz (rekord), klucz główny, klucz obcy, relacja 1:N,
  • dobierać typy kolumn (INT, VARCHAR, DATE) i ograniczenia (NOT NULL, DEFAULT, UNIQUE, UNSIGNED, AUTO_INCREMENT),
  • decydować, czego nie zapisywać w bazie, bo da się to obliczyć,
  • utworzyć bazę i tabele w phpMyAdmin skryptem SQL i sprawdzić, że baza sama pilnuje poprawności danych,
  • sprawdzić w PHP, czy serwer ma rozszerzenie mysqli, przez które TaskQuest połączy się z bazą.

Efekt: w projekcie pojawia się plik sql/taskquest.sql. Tworzy on bazę TaskQuest z dwiema tabelami (uzytkownicy, zadania) i danymi testowymi przeniesionymi z CSV. diagnostyka.php pokazuje, czy PHP jest gotowe do pracy z bazą.

Uwaga — co jest, a czego jeszcze nie ma:

  • TaskQuest nadal czyta zadania z data/zadania.csv, a XP i streak nadal są symulowane w index.php. Po tej lekcji baza istnieje i zawiera dane, ale aplikacja jeszcze z niej nie korzysta. Połączenie PHP z bazą i pierwsze zapytania SELECT to Lekcja 18.
  • Z SQL potrzebujemy dziś tylko trzech poleceń: CREATE TABLE (budowa tabeli), INSERT (dane testowe) i SELECT * (podgląd). Wszystkie wykonujemy ręcznie w phpMyAdmin, nie z PHP.
  • Do bazy łączymy się rozszerzeniem mysqli, w wersji obiektowej. Od Lekcji 15 wiesz, co to znaczy: mysqli to klasa, a połączenie to obiekt tworzony przez new.
  • Hasła: tabela użytkowników dostaje dziś kolumnę na hasło, ale zostaje ona pusta. Hasła pojawią się w Lekcji 19.

Rozgrzewka — 4 pytania z Lekcji 15

Odpowiedz z pamięci, zanim zaczniesz:

  1. Klasa Zadanie ma właściwości id, tytul, opis, termin, wykonane, xp. Jaki typ PHP ma każda z nich?
  2. Dlaczego Uzytkownik nie ma właściwości poziom, tylko metodę pobierzPoziom()?
  3. Zadanie nie ma metody „ustaw XP”, a Uzytkownik pozwala tylko dodawać XP. Co chroni takie ograniczenie?
  4. Po wywołaniu $uzytkownik->dodajXp(50) odświeżasz stronę i XP znowu wynosi 99. Dlaczego?

1. Wracamy do naszego TaskQuest

Wszystkie pliki PHP z Lekcji 15 zostają i działają tak samo. Dzisiejsza praca to głównie projekt danych i SQL. W PHP zmienia się tylko narzędzie programisty (diagnostyka.php) i numer wersji w stopce.

Docelowa struktura projektu

taskquest/
├── index.php                        bez zmian
├── dodaj-zadanie.php                bez zmian
├── zaloguj.php                      bez zmian
├── wyloguj.php                      bez zmian
├── diagnostyka.php                  (zmiana: plik SQL na liście wymaganych, sekcja „Baza danych”)
├── classes/
│   ├── Zadanie.php                  bez zmian
│   └── Uzytkownik.php               bez zmian
├── config/
│   └── config.php                   bez zmian (dane dostępowe do bazy dojdą w L18)
├── includes/
│   ├── header.php                   bez zmian
│   ├── footer.php                   (zmiana: "TaskQuest 1.7")
│   └── ... pozostałe pliki funkcji  bez zmian
├── sql/                             NOWY KATALOG
│   └── taskquest.sql                NOWY — tabele bazy + dane testowe
└── data/
    └── zadania.csv                  bez zmian — aplikacja nadal z niego czyta

Co zostaje, co dochodzi

ElementPo Lekcji 15Po Lekcji 17
data/zadania.csv, wczytajZadania(), zapiszZadania()źródło danychzostają — nadal źródło danych aplikacji
klasy Zadanie, Uzytkownikistniejązostają bez zmian — ich właściwości podpowiadają kolumny tabel
symulowane xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14'w index.phpzostają w index.php; te same wartości trafiają do bazy jako dane testowe
baza taskquest z tabelami uzytkownicy, zadanianie madochodzi (skrypt sql/taskquest.sql)
sprawdzenie rozszerzenia mysqlinie madochodzi w diagnostyka.php

Nic nie usuwamy.

Przygotowanie (zrób to teraz)

  1. Zrób kopię zapasową katalogu taskquest/ z Lekcji 15 jako taskquest_lekcja15/ (obok, nie w środku).
  2. Uruchom w XAMPP (albo w innym używanym pakiecie) Apache i MySQL. W XAMPP moduł nazywa się „MySQL”, choć w środku działa MariaDB (sekcja 3.2).
  3. Wejdź na http://localhost/phpmyadmin i sprawdź, czy strona się otwiera.
  4. Otwórz w edytorze swój data/zadania.csv. Przyda się w kroku 3 sekcji 5.

2. Problem w aplikacji

Nauczycielka chce, żeby cała klasa, 30 osób, używała TaskQuest na jednym serwerze szkolnym. Zobaczmy, co się stanie z obecną wersją. Każdy punkt poniżej to znane ograniczenie, które zapisywaliśmy przy wcześniejszych lekcjach.

SytuacjaCo robi TaskQuest 1.5Dlaczego
Kuba i Ola logują się na różne loginywidzą te same zadaniajest jeden plik zadania.csv dla wszystkich
Kuba zdobywa XPu Oli też jest 99 XP, u Kuby po odświeżeniu też 99XP jest wpisane w index.php
Kuba i Ola jednocześnie dodają zadaniejedno z zadań może zniknąćzapiszZadania() wczytuje cały plik i nadpisuje go w trybie w; dwa zapisy naraz nadpisują się nawzajem
Ktoś ręcznie psuje jedną linię CSVnikt nie widzi swoich zadańpierwszy zły wiersz przerywa wczytywanie całego pliku
Chcemy pokazać tylko zadania Olitrzeba wczytać wszystko i filtrować w PHPplik nie potrafi „odpowiedzieć na pytanie”
Plik ma termin 2026-02-31zapisuje go bez protestuCSV to zwykły tekst; pilnuje tylko nasz kod

Można by dla każdego ucznia tworzyć osobny plik, pilnować blokad, pisać własne wyszukiwanie. To jednak oznacza budowanie własnej, gorszej bazy danych. Lepiej użyć prawdziwej.

Baza danych to program (serwer), który przechowuje dane w tabelach i sam dba o to, żeby:

  • wiele osób mogło jednocześnie czytać i zapisywać,
  • zmiana jednego wiersza nie wymagała przepisywania całości,
  • do tabeli nie trafiały dane złego typu, zbyt długie teksty, nieistniejące daty,
  • dane w różnych tabelach do siebie pasowały (np. zadanie nie może należeć do nieistniejącego użytkownika),
  • dało się zadać pytanie, np. „pokaż niewykonane zadania użytkownika nr 2 posortowane po terminie”. Tym zajmiemy się w Lekcji 18.

3. Nowy mechanizm: relacyjna baza danych

3.1 Pojęcia — na przykładzie, który już znasz

Twój data/zadania.csv jest bardzo blisko tabeli w bazie:

id;tytul;opis;termin;wykonane;xp          ← nagłówek = nazwy KOLUMN
1;"   Nauczyć   się regexów   PHP   ";... ← jedna linia = jeden WIERSZ (rekord)
PojęcieW CSVW bazie danych
baza danychkatalog data/taskquest: zbiór tabel jednej aplikacji
tabelaplik zadania.csvzadania: dane jednego rodzaju
kolumnapole w nagłówku, np. termintermin, ale z typem, np. DATE (baza przyjmie tylko datę)
wiersz (rekord)jedna liniajedno zadanie
klucz główny (primary key)kolumna id, a o unikalność dba nastepneIdZadania()id: baza gwarantuje unikalność i sama nadaje kolejne numery
SQLbrakjęzyk, w którym rozmawiamy z bazą (CREATE TABLE, SELECT, …)

Najważniejsza różnica: w CSV każda kolumna to tekst, a sprawdzanie poprawności robi nasz kod (sprawdzWierszZadania(), walidujNoweZadanie()). W bazie każda kolumna ma typ i reguły, a baza sama odrzuca to, co ich nie spełnia.

3.2 MySQL czy MariaDB?

MySQL to najpopularniejszy darmowy system baz danych. Od 2010 roku należy do firmy Oracle. MariaDB powstała w 2009 roku jako jego odgałęzienie (fork), stworzone przez pierwotnych autorów MySQL. Oba systemy są relacyjne: dane leżą w tabelach, które mogą być ze sobą powiązane.

W podstawach, których używamy, są praktycznie identyczne: ta sama składnia SQL i to samo rozszerzenie PHP. XAMPP zawiera MariaDB, choć moduł w panelu nazywa się „MySQL”. Jeśli phpMyAdmin pokazuje „MariaDB”, to nie jest błąd.

3.3 Jak PHP rozmawia z bazą — mysqli

Samo PHP nie zna bazy danych. Potrzebuje rozszerzenia, czyli dodatkowego modułu:

RozszerzenieStatus
mysql (funkcje mysql_connect(), mysql_query())usunięte w PHP 7.0 (2015). Nie działa. Zobaczysz je jeszcze w starych poradnikach w internecie
mysqli („MySQL improved”)aktualne; tego używamy w TaskQuest

Istnieje też trzecie rozszerzenie, PDO, ale w kursie z niego nie korzystamy.

mysqli można używać na dwa sposoby: funkcjami (mysqli_connect(...)) albo obiektowo (new mysqli(...)). W TaskQuest używamy wyłącznie wersji obiektowej. Od Lekcji 15 znasz obiekty, a TaskQuest jest już zbudowany z klas.

Sprawdźmy, czy Twój PHP ma mysqli. W katalogu taskquest/ utwórz brudnopis.php:

<?php

var_dump(extension_loaded('mysqli'));
var_dump(extension_loaded('mysql'));
var_dump(class_exists('mysqli'));

echo 'Wersja PHP: ' . PHP_VERSION;

Przewidź wynik, a potem uruchom http://localhost/taskquest/brudnopis.php.

  • extension_loaded('mysqli') → true: rozszerzenie jest włączone.
  • extension_loaded('mysql') → false: stare rozszerzenie nie istnieje w PHP 7 i 8.
  • class_exists('mysqli') → true: mysqli to klasa, tak jak Twoja Zadanie. W Lekcji 18 napiszesz new mysqli(...) i dostaniesz obiekt połączenia.

Jeśli mysqli daje false, otwórz php.ini (w XAMPP: panel → Apache → Config → PHP (php.ini)). Znajdź linię ;extension=mysqli, usuń średnik na początku i zrestartuj Apache.

3.4 Tabela w SQL — przykład: tabela uzytkownicy

Zacznijmy od klasy, którą znasz:

class Uzytkownik
{
    private string $login;
    private string $imieUzytkownika;
    private int $xp;
    private int $streak;
    private string $dataOstatniejAktywnosci;
    // ...
}

Tak wygląda odpowiadająca jej tabela:

CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NULL,
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Czytamy linia po linii. Każda kolumna to: nazwa, typ, reguły.

FragmentZnaczenieSkąd taka decyzja w TaskQuest
INTliczba całkowitajak int w PHP
UNSIGNEDbez znaku: tylko 0 i więcejXP i streak nie mogą być ujemne. dodajXp() pilnuje tego w PHP, baza pilnuje dodatkowo
VARCHAR(20)tekst o maksymalnej długości 20 znakówwalidujLogin() pozwala na 3–20 znaków; imię: walidujDaneLogowania() pozwala na najwyżej 30
DATEdata RRRR-MM-DDten sam format, którego używamy od lekcji o datach
NOT NULLwartość musi być podanaużytkownik bez loginu nie ma sensu
NULLwartość może nie istniećnowy użytkownik nie miał jeszcze żadnej aktywności; hasła jeszcze nie ma
DEFAULT 0wartość, gdy jej nie podamynowy użytkownik zaczyna od 0 XP
UNIQUEw całej tabeli wartość nie może się powtórzyćdwóch użytkowników nie może mieć tego samego loginu
PRIMARY KEYklucz główny: jednoznacznie wskazuje wiersz; zawsze unikalny i nie NULL„użytkownik nr 1”
AUTO_INCREMENTbaza sama nadaje kolejne numery 1, 2, 3…to, co robi nasza nastepneIdZadania(), ale pewniej
ENGINE=InnoDBsposób przechowywania tabelitylko InnoDB pilnuje powiązań między tabelami (3.6)
utf8mb4kodowanie znakówpolskie litery i emoji (Eksperyment 3)

NULL to nie jest pusty tekst ani zero. NULL oznacza „brak wartości, nie wiadomo”. Pusty tekst '' to tekst, który istnieje, tylko nie ma znaków. W PHP NULL z bazy stanie się null.

Dwie rzeczy, które wyglądają na zmiany — i dlaczego je robimy

  1. id zamiast samego loginu. Login jest unikalny, więc mógłby być kluczem głównym. Tak się jednak nie robi. Login jest widoczny dla użytkownika i kiedyś może się zmienić. Wtedy trzeba by poprawić go w każdym zadaniu, które na niego wskazuje. Liczba id nigdy się nie zmienia, a login pilnujemy regułą UNIQUE. Klasa Uzytkownik nie ma dziś id, bo nie było go skąd wziąć. Wróci to w Lekcji 18.
  2. imie, a nie imie_uzytkownika. Jesteśmy w tabeli uzytkownicy, więc dopisek „użytkownika” nic nie dodaje. To ta sama nazwa co klucz w sesji ($_SESSION['imie']). Właściwość w klasie nadal nazywa się imieUzytkownika.

Konwencja nazw w bazie TaskQuest

Nazwy tabel i kolumn są po polsku, tak jak w PHP, ale w stylu SQL: małe litery, bez polskich znaków, słowa rozdzielone podkreślnikiem (snake_case):

PHPBaza
$dataOstatniejAktywnoscidata_ostatniej_aktywnosci
$tytul, $termin, $xptytul, termin, xp
klasa Zadanie (jedno zadanie)tabela zadania (zbiór zadań, liczba mnoga)

3.5 Czego NIE zapisujemy w bazie

W tabeli uzytkownicy nie ma kolumn poziom, tytul_gracza, procent_postepu ani brakujace_xp. Wszystkie wynikają z XP. Od lekcji o funkcjach liczą je funkcje obliczPoziom(), pobierzTytul(), obliczProcentPostepu() i ileBrakujeXp(), a od Lekcji 15 także metody Uzytkownik.

Gdybyśmy zapisali XP i poziom, prędzej czy później zdarzyłoby się tak:

xp = 350, poziom = 2

Któraś wartość kłamie. Tylko która? Baza tego nie wie.

Zasada: w bazie zapisujemy dane, których nie da się obliczyć z innych danych. Resztę liczymy w PHP. Każda informacja ma jedno źródło prawdy, tak jak w Lekcji 15, gdzie usunęliśmy luźne $xp obok obiektu $uzytkownik.

Tę zasadę zastosujesz za chwilę sama/sam przy zadaniach (4b).

3.6 Relacja 1:N i klucz obcy

Każde zadanie musi mieć właściciela. Jeden użytkownik może mieć wiele zadań, a jedno zadanie należy do dokładnie jednego użytkownika. To relacja jeden do wielu (1:N):

uzytkownicy                         zadania
+----+-----------+                  +----+---------------+----------------------+
| id | login     |                  | id | uzytkownik_id | tytul                |
+----+-----------+                  +----+---------------+----------------------+
|  1 | Kama_2026 | ◄──────────┬──── |  1 |             1 | Nauczyć się regexów  |
|    |           |            ├──── |  2 |             1 | ...                  |
|    |           |            └──── |  3 |             1 | ...                  |
|  2 | Ola_TQ    | ◄─────────────── |  4 |             2 | Zaprojektować bazę   |
+----+-----------+                  +----+---------------+----------------------+
          1                                         N

Połączenie realizuje kolumna uzytkownik_id w tabeli zadania. Przechowuje ona id właściciela. Kolumna, która wskazuje klucz główny innej tabeli, to klucz obcy (foreign key):

FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE

Czytaj: „uzytkownik_id musi być id istniejącego wiersza w uzytkownicy. Gdy użytkownik zostanie usunięty, usuń też (cascade, kaskadowo) jego zadania”.

Dlaczego uzytkownik_id jest w zadania, a nie odwrotnie? Pole użytkownika mieści jedną wartość. Nie zmieścisz w nim listy „zadania 1, 2, 3”. W relacji 1:N klucz obcy zawsze trafia na stronę „N”.

3.7 Dane testowe — INSERT

Tabela po utworzeniu jest pusta. Wiersze dodaje polecenie INSERT:

INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);
  • W nawiasie po nazwie tabeli wymieniamy kolumny, które wypełniamy. Pomijamy id (nada je AUTO_INCREMENT) i haslo_hash (zostanie NULL).
  • Teksty i daty w apostrofach, liczby bez nich, NULL bez apostrofów ('NULL' to byłby tekst „NULL”).
  • Kolejne wiersze rozdzielamy przecinkiem, a całe polecenie kończy średnik.

Kama ma dokładnie te wartości, które TaskQuest symuluje od Lekcji 15 (xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14'). Gdy w Lekcji 18 podłączymy bazę, strona powinna pokazać to samo co dziś. To będzie nasz test.

Do podejrzenia zawartości tabeli wystarczy:

SELECT * FROM uzytkownicy;

Więcej o SELECT w Lekcji 18.

3.8 Typowe błędy

BłądSkutek
'NULL' w apostrofachzapisuje tekst „NULL”, a nie brak wartości
liczba w apostrofach '99'zwykle zadziała (baza przekonwertuje), ale to zła praktyka. Typ kolumny jest po to, żeby było jasne, co to jest
data '14.09.2026'błąd: baza przyjmuje RRRR-MM-DD
utf8 zamiast utf8mb4emoji w tytule zadania powoduje błąd (Eksperyment 3)
przecinek po ostatniej kolumnie w CREATE TABLEbłąd składni #1064
tabela zadania tworzona przed uzytkownicybłąd: klucz obcy wskazuje tabelę, której jeszcze nie ma
cudzysłów typograficzny ’ zamiast ' (kopiowanie z Worda)błąd składni

4. SAMODZIELNIE — projekt tabeli zadania

Na kartce albo w pliku tekstowym zaprojektuj tabelę zadania. Masz do dyspozycji:

  • klasę Zadanie: int $id, string $tytul, string $opis, string $termin, bool $wykonane, int $xp,
  • reguły z walidujNoweZadanie(): tytuł wymagany, najwyżej 100 znaków; opis najwyżej 300 znaków (może być pusty); termin wymagany, poprawna data; XP to liczba całkowita 1–100,
  • relację z 3.6: każde zadanie ma właściciela.

Dla każdej kolumny podaj: nazwę, typ, NULL czy NOT NULL, ewentualnie DEFAULT, i czy jest kluczem. Kolumnę wykonane na razie pomiń. Zajmiemy się nią w 4b.

Efekt: tabelka w rodzaju

kolumna        | typ           | NULL?    | inne
id             | ...           | ...      | ...
...
HINT

Idź po kolei po właściwościach klasy i zadaj sobie przy każdej trzy pytania: jaki typ w PHP → jaki typ w bazie? Czy wartość może nie istnieć? Jaki jest limit długości albo zakres? Nie zapomnij o kolumnie, której nie ma w klasie, a jest potrzebna do relacji.

POMOC
  • string z limitem → VARCHAR(limit),
  • int nieujemny → INT UNSIGNED,
  • termin → DATE,
  • klucz główny wygląda jak w uzytkownicy,
  • właściciel: kolumna typu takiego samego jak uzytkownicy.id (klucz obcy musi mieć zgodny typ).

Opis może być pusty. Czy to znaczy NULL, czy pusty tekst? Pomyśl, co dziś zwraca pobierzOpis() dla zadania bez opisu i jaki typ ma właściwość $opis.

ROZWIĄZANIE
kolumna        | typ           | NULL?    | inne
id             | INT UNSIGNED  | NOT NULL | PRIMARY KEY, AUTO_INCREMENT
uzytkownik_id  | INT UNSIGNED  | NOT NULL | FOREIGN KEY → uzytkownicy(id), ON DELETE CASCADE
tytul          | VARCHAR(100)  | NOT NULL |
opis           | VARCHAR(300)  | NOT NULL | DEFAULT ''
termin         | DATE          | NOT NULL |
xp             | INT UNSIGNED  | NOT NULL |
  • opis jako NOT NULL DEFAULT '': właściwość $opis jest typu string (nie ?string), a zadanie bez opisu ma dziś opis ''. Brak opisu to pusty tekst, nie „nie wiadomo”. Gdyby kolumna dopuszczała NULL, w PHP trzeba by obsługiwać dwa różne „puste” przypadki.
  • uzytkownik_id jako NOT NULL: zadanie bez właściciela nie istnieje.
  • xp bez DEFAULT: nie ma sensownej domyślnej nagrody; formularz zawsze ją podaje. Zakresu 1–100 nie wymusza UNSIGNED (to tylko „≥ 0”). Pilnuje go walidujNoweZadanie(). Wymuszenie zakresu w bazie to zadanie końcowe B.
  • VARCHAR(300) wystarcza na opis. Dla bardzo długich tekstów (np. treść artykułu) jest typ TEXT, ale opis TaskQuest jest krótki i ma limit.

4b. SAMODZIELNIE — jak zapisać, że zadanie jest wykonane?

Od lekcji o datach streak jest liczony „na niby”. Zadania nie mają daty wykonania, więc $zadanieWykonane znaczy tylko „na liście jest choć jedno wykonane zadanie”. Do prawdziwego streaka będziemy potrzebowali wiedzieć, kiedy zadanie wykonano.

Masz trzy propozycje:

A) wykonane TINYINT(1) NOT NULL DEFAULT 0          (0/1, jak w CSV)
B) wykonane TINYINT(1) + data_wykonania DATE NULL  (dwie kolumny)
C) data_wykonania DATE NULL                         (NULL = niewykonane)

Odpowiedz na pytania:

  1. Która propozycja nie pozwala policzyć prawdziwego streaka?
  2. W propozycji B wpisz wiersz, w którym dane sobie przeczą. Ile takich sprzecznych kombinacji istnieje?
  3. Jak w propozycji C sprawdzić, czy zadanie jest wykonane?
  4. Którą wybierasz i dlaczego? Skorzystaj z zasady z 3.5.
HINT

Zasada z 3.5 dotyczyła XP i poziomu. Czy wykonane da się obliczyć z data_wykonania?

ROZWIĄZANIE
  1. A: nie wiadomo, kiedy zadanie wykonano.
  2. Na przykład wykonane = 0, data_wykonania = '2026-09-14' („niewykonane, wykonane 14 września”) albo wykonane = 1, data_wykonania = NULL. Są dwie sprzeczne kombinacje i baza nie wie, która kolumna mówi prawdę.
  3. data_wykonania nie jest NULL → wykonane.
  4. C. wykonane da się obliczyć z data_wykonania, dokładnie tak jak poziom z XP. Jedna kolumna, zero sprzeczności, a przy okazji mamy datę, której potrzebuje prawdziwy streak.

Klasa Zadanie nie zmienia się: nadal ma bool $wykonane i czyWykonane(). Przy tworzeniu obiektu z wiersza bazy wartość wykonane obliczymy w PHP. Przećwiczysz to w sekcji 6.

Kolumnę data_wykonania wybieramy jako DATE, bo streak liczy dni, a nie godziny. Ten sam typ mają termin i data_ostatniej_aktywnosci.

5. Włączamy bazę do TaskQuest

Od tego miejsca pracujesz na projekcie. brudnopis.php z sekcji 3.3 zostaw. Przyda się jeszcze w sekcji 6, potem go usuniesz.

Krok 1 — utwórz bazę taskquest

  1. W phpMyAdmin kliknij zakładkę Bazy danych.
  2. Nazwa: taskquest.
  3. Metoda porównywania napisów: utf8mb4_unicode_ci. utf8mb4 to pełne UTF-8, łącznie z emoji. unicode_ci określa, jak porównywać i sortować teksty: ci = case insensitive, czyli wielkość liter nie ma znaczenia przy porównywaniu. Test 2 pokaże, co to oznacza dla loginów.
  4. Utwórz. Baza pojawi się w lewym panelu.

Krok 2 — SAMODZIELNIE: plik sql/taskquest.sql, część 1: tabele

Struktury bazy nie „wyklikujemy”, tylko zapisujemy w pliku. Dzięki temu jest częścią projektu i można ją odtworzyć na każdym komputerze jednym uruchomieniem. W zespołach programistycznych robi się dokładnie tak.

W katalogu taskquest/ utwórz katalog sql/, a w nim plik taskquest.sql. Plik ma zawierać, w tej kolejności:

  1. komentarz na górze: co to za plik i jak go uruchomić (komentarz w SQL zaczyna się od -- , z odstępem po dwóch minusach),
  2. usunięcie starych tabel, jeśli istnieją: DROP TABLE IF EXISTS nazwa;. Dzięki temu skrypt można uruchamiać wiele razy. Zastanów się nad kolejnością (która tabela zależy od której?),
  3. CREATE TABLE uzytkownicy z sekcji 3.4,
  4. CREATE TABLE zadania według Twojego projektu z sekcji 4 i decyzji z 4b, z kluczem obcym z 3.6 i tym samym ENGINE/CHARSET/COLLATE co uzytkownicy. Kolejność kolumn: id, uzytkownik_id, a dalej jak w CSV (tytul, opis, termin, data_wykonania w miejscu dawnego wykonane, xp).
HINT

Tabeli, na którą wskazuje klucz obcy, nie da się usunąć, dopóki istnieje tabela, która na nią wskazuje. Tworzenie idzie w odwrotnej kolejności niż usuwanie. Klucz obcy dopisujesz jako ostatnią linię wewnątrz nawiasu CREATE TABLE, po przecinku.

ROZWIĄZANIE
-- TaskQuest 1.7 — struktura bazy danych i dane testowe
-- Uruchom w phpMyAdmin: baza taskquest → zakładka SQL → wklej całość → Wykonaj.
-- Skrypt można uruchamiać wiele razy: najpierw usuwa stare tabele (RAZEM Z DANYMI!).

DROP TABLE IF EXISTS zadania;
DROP TABLE IF EXISTS uzytkownicy;

CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NULL,
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT UNSIGNED NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    opis VARCHAR(300) NOT NULL DEFAULT '',
    termin DATE NOT NULL,
    data_wykonania DATE NULL,
    xp INT UNSIGNED NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

haslo_hash ma VARCHAR(255), bo funkcja password_hash() z Lekcji 19 zwraca tekst, którego długość może rosnąć w nowych wersjach PHP. Dokumentacja PHP zaleca kolumnę na 255 znaków.

Krok 3 — SAMODZIELNIE: plik sql/taskquest.sql, część 2: dane testowe

Pod tabelami dopisz dwa polecenia INSERT:

  1. użytkownicy: Kama i Ola, dokładnie jak w sekcji 3.7,
  2. zadania:
    • wszystkie zadania z Twojego data/zadania.csv jako zadania Kamy (uzytkownik_id = 1),
    • jedno nowe zadanie Oli (uzytkownik_id = 2), dowolne, niewykonane. Dzięki niemu w bazie będą zadania dwóch różnych właścicieli.

Zasady przepisywania z CSV:

W CSVW INSERT
idpomiń: numery nada AUTO_INCREMENT
tytul, opisprzepisz bez zmian, także „brudne” spacje w tytułach (czyści je wyczyscTytul() przy wyświetlaniu)
terminbez zmian
wykonane = 1data_wykonania = '2026-09-14' (CSV nie zna daty wykonania, bierzemy symulowaną datę ostatniej aktywności Kamy)
wykonane = 0data_wykonania = NULL
xpbez zmian (liczba bez apostrofów)

Apostrof wewnątrz tekstu (np. Rock'n'roll) w SQL piszemy podwójnie: 'Rock''n''roll'.

HINT

Dlaczego Kama to na pewno uzytkownik_id = 1, a Ola = 2? Skrypt przed chwilą utworzył tabelę od nowa, więc AUTO_INCREMENT zaczyna od 1, a Kama jest wstawiana jako pierwsza.

ROZWIĄZANIE
INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);

INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, data_wykonania, xp) VALUES
(1, '   Nauczyć   się regexów   PHP   ', 'tu opis z CSV', '2026-09-20', '2026-09-14', 20),
-- ... kolejne zadania z data/zadania.csv (uzytkownik_id = 1) ...
(2, 'Zaprojektować bazę TaskQuest', 'Tabele uzytkownicy i zadania, klucze i relacja 1:N.', '2026-10-05', NULL, 30);

Pierwszy wiersz to zadanie nr 1 z CSV: wykonane (1), więc ma datę wykonania. Ostatni to zadanie Oli.

Krok 4 — uruchom skrypt i sprawdź wynik

  1. phpMyAdmin → kliknij bazę taskquest w lewym panelu (to ważne: skrypt nie wybiera bazy sam) → zakładka SQL.

  2. Wklej całą zawartość sql/taskquest.sql → Wykonaj.

  3. Błąd? Czytaj komunikat od końca: numer błędu i fragment „near '…’” wskazują miejsce. Popraw plik taskquest.sql (nie tylko okienko w phpMyAdmin) i uruchom ponownie. Skrypt sam usunie to, co zdążył utworzyć.

  4. Zakładka SQL:SELECT * FROM uzytkownicy;
    SELECT * FROM zadania;
    Sprawdź: 2 użytkowników, zadania Kamy mają uzytkownik_id 1, zadanie Oli ma 2, wykonane zadania mają datę, niewykonane NULL. Liczba zadań Kamy = liczba zadań w CSV.

  5. Kliknij tabelę zadania → zakładka Struktura. Obejrzyj typy, NULL i klucz. Przy uzytkownik_id phpMyAdmin pokazuje indeks; baza tworzy go sama dla klucza obcego.

Krok 5 — SAMODZIELNIE: diagnostyka.php sprawdza gotowość do pracy z bazą

diagnostyka.php od Lekcji 10 sprawdza, czy projekt ma wszystkie potrzebne pliki. Rozbuduj ją o trzy rzeczy:

  1. do $wymaganePliki dopisz sql/taskquest.sql (będzie 18 ścieżek),
  2. dodaj nową sekcję „Baza danych”:
    • [OK] Rozszerzenie mysqli jest włączone. albo [BRAK] Rozszerzenie mysqli nie jest włączone — włącz extension=mysqli w php.ini.,
    • liczbę i nazwy plików .sql w katalogu sql/,
  3. w includes/footer.php zmień wersję na TaskQuest 1.7.

Zasada z Lekcji 10 obowiązuje dalej: diagnostyka nie łączy się z bazą i nie dołącza plików aplikacji. Ma działać nawet przy zepsutym projekcie. Dziś sprawdzamy tylko, czy PHP w ogóle potrafi rozmawiać z bazą.

HINT

Nie potrzebujesz nowej funkcji. Jedna wbudowana funkcja PHP z sekcji 3.3 daje odpowiedź true/false. Do listy plików .sql masz własną funkcję z Lekcji 10, która ma parametr na rozszerzenie pliku. Sekcja „Pliki danych” (pliki .csv w data/) robi prawie to samo.

ROZWIĄZANIE

W $wymaganePliki dopisz na końcu listy:

    'sql/taskquest.sql',

Pod sekcją „Pliki danych” (tą, która korzysta z $plikiCsv) dopisz nową sekcję, w tym samym stylu HTML co sekcje powyżej:

<h2>Baza danych</h2>

<?php if (extension_loaded('mysqli')) { ?>
    <p>[OK] Rozszerzenie mysqli jest włączone.</p>
<?php } else { ?>
    <p>[BRAK] Rozszerzenie mysqli nie jest włączone — włącz extension=mysqli w php.ini.</p>
<?php } ?>

<?php $plikiSql = listaPlikowWKatalogu('sql', 'sql'); ?>
<p>Skrypty SQL w katalogu sql/: <?= count($plikiSql); ?></p>
<ul>
    <?php foreach ($plikiSql as $plik) { ?>
        <li><?= $plik; ?></li>
    <?php } ?>
</ul>

Jeśli w Twojej diagnostyka.php zmienne są obliczane na górze pliku, a na dole jest tylko HTML, przenieś linię $plikiSql = ... na górę, obok $plikiCsv. Zachowaj układ, który już masz.

includes/footer.php:

        <p>TaskQuest 1.7</p>
    </div>
</body>
</html>

Sprawdź: diagnostyka.php → 18 × [OK], [OK] Rozszerzenie mysqli jest włączone., 1 skrypt SQL: taskquest.sql.

6. Kolejne zadanie — jak wiersz z bazy stanie się obiektem Zadanie

W Lekcji 18 PHP pobierze z bazy wiersz zadania. Dostanie go jako tablicę asocjacyjną, której klucze to nazwy kolumn. Z niej trzeba będzie zbudować obiekt Zadanie, tak jak dziś wczytajZadania() buduje go z wiersza CSV. Przećwiczmy to już teraz, na wierszu „udawanym”.

W brudnopis.php (zastąp poprzednią zawartość):

<?php

require_once __DIR__ . '/classes/Zadanie.php';

// Tak będzie wyglądał wiersz z tabeli zadania (na razie wpisany ręcznie):
$wiersz = [
    'id' => 4,
    'uzytkownik_id' => 2,
    'tytul' => 'Zaprojektować bazę TaskQuest',
    'opis' => 'Tabele uzytkownicy i zadania, klucze i relacja 1:N.',
    'termin' => '2026-10-05',
    'data_wykonania' => null,
    'xp' => 30,
];

// TODO: utwórz $zadanie = new Zadanie(...) z argumentami nazwanymi

var_dump($zadanie->czyWykonane());
var_dump($zadanie->pobierzTytul());

Zadanie:

  1. Utwórz obiekt $zadanie z danych $wiersz, używając argumentów nazwanych (jak od Lekcji 15).
  2. Obiekt potrzebuje wykonane (bool), a wiersz ma data_wykonania. Oblicz to w miejscu wywołania konstruktora, bez zmieniania klasy Zadanie.
  3. Przewidź i sprawdź wynik dla data_wykonania = null, a potem dla '2026-09-30'.
  4. Jednej kolumny z wiersza nie przekazujesz do obiektu. Której? Czy to problem dzisiaj? Kiedy przestanie nim być?
HINT

Masz porównać wartość z null tak, żeby wynik był bool. Ten sam operator stosowaliśmy w Lekcji 12 przy $bladZadan.

POMOC
wykonane: $wiersz['data_wykonania'] !== null,
ROZWIĄZANIE
$zadanie = new Zadanie(
    id: $wiersz['id'],
    tytul: $wiersz['tytul'],
    opis: $wiersz['opis'],
    termin: $wiersz['termin'],
    wykonane: $wiersz['data_wykonania'] !== null,
    xp: $wiersz['xp'],
);
  • null → bool(false), '2026-09-30' → bool(true).
  • Nie przekazujemy uzytkownik_id. Dziś to nie problem: obiekt zadania nie musi wiedzieć, czyje jest. Pytanie „czyje to zadania?” zada baza, gdy w Lekcji 18 poprosimy tylko o zadania zalogowanego użytkownika.
  • Porównaj z wczytajZadania(): tam wykonane: $wiersz[4] === '1' (z tekstu CSV), tu wykonane: $wiersz['data_wykonania'] !== null (z kolumny bazy). Zmienia się źródło, a obiekt zostaje ten sam. Dlatego reszta aplikacji (funkcje listy, index.php) nie będzie musiała się zmieniać.

Dokładne typy wartości, które zwraca mysqli (np. czy id przyjdzie jako liczba, czy jako tekst), sprawdzimy w Lekcji 18 na prawdziwym połączeniu. Tu symulujemy wiersz. Data (DATE) przychodzi jako tekst RRRR-MM-DD, a NULL jako null.

7. Testy

Gdzie: testy 1–6 w phpMyAdmin (baza taskquest → zakładka SQL), testy 7–8 w przeglądarce. Przed każdym testem zapisz swoją przewidywaną odpowiedź: czy baza przyjmie polecenie, a jeśli nie, to z jakiego powodu.

Testy 2–6 próbują wstawić złe dane. Baza powinna je odrzucić i nic nie zapisać.

Test 1 — struktura i relacja

SELECT * FROM zadania WHERE uzytkownik_id = 2;

Oczekiwane: dokładnie jeden wiersz, zadanie Oli. (WHERE to warunek. Dokładniej poznasz go w Lekcji 18.) Zamień 2 na 1: tyle wierszy, ile zadań w Twoim CSV. Zamień na 3: pusty wynik, bez błędu. Użytkownika nr 3 nie ma, więc nie ma też jego zadań.

Test 2 — unikalny login

INSERT INTO uzytkownicy (login, imie) VALUES ('Kama_2026', 'Druga Kama');
INSERT INTO uzytkownicy (login, imie) VALUES ('KAMA_2026', 'Trzecia Kama');

Wykonuj po jednym poleceniu.

PolecenieOczekiwane
'Kama_2026'błąd #1062 Duplicate entry (reguła UNIQUE)
'KAMA_2026'też #1062. Porównanie utf8mb4_unicode_ci nie rozróżnia wielkości liter, więc dla bazy to ten sam login. Dobrze: dwóch użytkowników „Kama_2026” i „KAMA_2026” byłoby mylące

Test 3 — zadanie nieistniejącego użytkownika

INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (99, 'Zadanie bez właściciela', '2026-10-01', 10);

Oczekiwane: #1452 Cannot add or update a child row: a foreign key constraint fails. Klucz obcy nie wpuści zadania, którego właściciel nie istnieje. Zwróć uwagę, że pominęliśmy opis i data_wykonania, a mimo to tylko uzytkownik_id spowodował błąd. Dlaczego? Sprawdź DEFAULT i NULL tych kolumn. Po zmianie 99 na 1 polecenie przejdzie. Potem uruchom ponownie cały taskquest.sql, żeby wrócić do czystych danych.

Test 4 — baza pilnuje reguł kolumn

Każde polecenie osobno:

INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, NULL, '2026-10-01', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Ujemne XP', '2026-10-01', -5);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Zły dzień', '2026-02-31', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, REPEAT('a', 101), '2026-10-01', 10);

(REPEAT('a', 101) to funkcja SQL, która tworzy tekst ze 101 liter „a”.)

DaneKtóra reguła?Oczekiwany błąd
tytuł NULLNOT NULL#1048 Column 'tytul' cannot be null
XP -5UNSIGNED#1264 Out of range value
termin 2026-02-31typ DATE#1292 Incorrect date value
tytuł 101 znakówVARCHAR(100)#1406 Data too long

Porównaj z CSV. sprawdzWierszZadania() sprawdza tylko kształt daty (RRRR-MM-DD), więc 2026-02-31 z pliku przechodzi. Baza sprawdza kalendarz.

Jeśli któreś polecenie się wykonało (zamiast błędu pojawiło się ostrzeżenie, a w tabeli np. 0 zamiast -5 albo obcięty tytuł), Twój serwer działa w trybie „nieścisłym” (non-strict). Sprawdź: SELECT @@sql_mode;. W wyniku powinno być STRICT_TRANS_TABLES. Jeśli go nie ma, zgłoś to nauczycielowi i po teście uruchom ponownie taskquest.sql. Wniosek na przyszłość: walidacji w PHP nie usuwamy nigdy. Baza jest drugą linią obrony, a nie pierwszą.

Test 5 — NULL to nie tekst

INSERT INTO zadania (uzytkownik_id, tytul, termin, data_wykonania, xp) VALUES (1, 'Test NULL', '2026-10-01', 'NULL', 10);

Przewidź: czy to zadanie będzie wykonane, niewykonane, czy dostaniesz błąd? Oczekiwane: błąd #1292 Incorrect date value: 'NULL'. Tekst 'NULL' to nie brak wartości, tylko napis, który nie jest datą. Bez apostrofów polecenie przejdzie. Uruchom ponownie taskquest.sql.

Test 6 — skrypt da się uruchomić ponownie

Uruchom cały sql/taskquest.sql drugi raz. Oczekiwane: brak błędów, znowu 2 użytkowników i ta sama liczba zadań. Nie ma „śmieci” z testów 3–5, a id zadań znowu zaczynają się od 1.

Test 7 — diagnostyka

http://localhost/taskquest/diagnostyka.php: 18 × [OK], sekcja „Baza danych” z [OK] przy mysqli i jednym skryptem taskquest.sql. Zmień chwilowo nazwę pliku sql/taskquest.sql na taskquest.txt. Przewidź, co pokaże diagnostyka, sprawdź i przywróć nazwę. Oczekiwane: [BRAK] przy sql/taskquest.sql i 0 skryptów SQL.

Test 8 — regresja: aplikacja działa jak po Lekcji 15

Zaloguj się i porównaj taskquest/index.php z taskquest_lekcja15/index.php tego samego dnia. Różnić się może tylko wersja w stopce (1.5 → 1.7). Dodaj zadanie przez formularz. Pojawi się na liście i w data/zadania.csv, ale nie w bazie (SELECT * FROM zadania;). To potwierdza, że aplikacja jeszcze nie korzysta z bazy.

8. Obowiązkowe eksperymenty

Gdzie: phpMyAdmin, baza taskquest. Po każdym eksperymencie uruchom ponownie cały sql/taskquest.sql, żeby wrócić do czystego stanu.

Eksperyment 1: kolejność ma znaczenie

W taskquest.sql zamień miejscami dwie linie DROP TABLE IF EXISTS (najpierw uzytkownicy, potem zadania) i uruchom skrypt. Tabele już istnieją z wcześniejszego uruchomienia.

Wynik: #1451 Cannot delete or update a parent row: a foreign key constraint fails. Baza nie pozwala usunąć tabeli uzytkownicy, dopóki tabela zadania na nią wskazuje. Inaczej zadania wskazywałyby na użytkowników, których nie ma. Przywróć kolejność w pliku. Zasada: usuwamy od tabel zależnych („N”), tworzymy od niezależnych („1”).

Eksperyment 2: co robi ON DELETE CASCADE

SELECT * FROM zadania;
DELETE FROM uzytkownicy WHERE login = 'Ola_TQ';
SELECT * FROM zadania;

Przed wykonaniem przewidź, ile zadań zostanie. Wynik: zadanie Oli zniknęło razem z nią, choć polecenie dotyczyło tylko tabeli uzytkownicy. (DELETE to usuwanie wierszy. Z poziomu PHP zajmiemy się nim w Lekcji 19.)

Teraz wyobraź sobie, że w CREATE TABLE zadania nie ma ON DELETE CASCADE. Co zrobiłaby baza z tym samym DELETE? Sprawdź: usuń z pliku ON DELETE CASCADE, uruchom skrypt, powtórz DELETE. Wynik: #1451, czyli baza odmawia usunięcia użytkownika, który ma zadania. Obie reakcje chronią przed „osieroconymi” zadaniami. Różnią się tym, co wydarzy się z danymi. Dla TaskQuest wybieramy kaskadę: usunięcie konta usuwa też zadania. Przywróć ON DELETE CASCADE w pliku.

Eksperyment 3: utf8mb4 i emoji

INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Trening 💪 i nauka SQL', '2026-10-01', 10);
SELECT tytul FROM zadania;

Działa: emoji zapisało się poprawnie. Teraz utwórz tabelę testową ze starszym kodowaniem utf8mb3. Dawniej nazywało się po prostu utf8 i mieściło znaki zajmujące najwyżej 3 bajty:

CREATE TABLE test_kodowania (tytul VARCHAR(100)) DEFAULT CHARSET=utf8mb3;
INSERT INTO test_kodowania VALUES ('Trening 💪');

Wynik: błąd #1366 Incorrect string value. Emoji zajmuje 4 bajty, a utf8mb3 mieści najwyżej 3. Polskie litery (2 bajty) przeszłyby bez problemu, dlatego ten błąd często wychodzi dopiero na produkcji, gdy pierwszy użytkownik wpisze emoji. Posprzątaj: DROP TABLE test_kodowania;

Eksperyment 4 (dodatkowy): skąd się biorą numery id

INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Pierwsze', '2026-10-01', 10);
DELETE FROM zadania WHERE tytul = 'Pierwsze';
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Drugie', '2026-10-01', 10);
SELECT id, tytul FROM zadania;

Przewidź: jakie id dostanie „Drugie”? Czy to samo co „Pierwsze”? Wynik: następny numer. AUTO_INCREMENT nie wraca do zwolnionych numerów, więc w id powstaje „dziura”. To dokładnie ta sama zasada, z powodu której nastepneIdZadania() od Lekcji 13 liczy „największe id + 1”, a nie count() + 1: numer raz użyty nie powinien trafić do innego zadania.

9. ROZWIĄZANIE

W PHP zmieniają się tylko diagnostyka.php i includes/footer.php. Pozostałe pliki są identyczne jak po Lekcji 15 (wersja referencyjna w 15_TaskQuest.md, sekcja 9).

ROZWIĄZANIE

sql/taskquest.sql (nowy plik)

-- TaskQuest 1.7 — struktura bazy danych i dane testowe
-- Uruchom w phpMyAdmin: baza taskquest → zakładka SQL → wklej całość → Wykonaj.
-- Skrypt można uruchamiać wiele razy: najpierw usuwa stare tabele (RAZEM Z DANYMI!).

DROP TABLE IF EXISTS zadania;
DROP TABLE IF EXISTS uzytkownicy;

CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NULL,
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT UNSIGNED NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    opis VARCHAR(300) NOT NULL DEFAULT '',
    termin DATE NOT NULL,
    data_wykonania DATE NULL,
    xp INT UNSIGNED NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Kama: te same wartości, które index.php symuluje od Lekcji 15.
-- Ola: drugi użytkownik, żeby w bazie byli dwaj właściciele zadań.
INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);

-- Zadania Kamy = wszystkie zadania z data/zadania.csv:
--   id pomijamy (AUTO_INCREMENT), tytuł i opis bez zmian (także "brudne" spacje),
--   wykonane 1 → data_wykonania '2026-09-14', wykonane 0 → NULL.
INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, data_wykonania, xp) VALUES
(1, '   Nauczyć   się regexów   PHP   ', 'tu opis z CSV', '2026-09-20', '2026-09-14', 20),
-- ... kolejne zadania z data/zadania.csv (uzytkownik_id = 1) ...
(2, 'Zaprojektować bazę TaskQuest', 'Tabele uzytkownicy i zadania, klucze i relacja 1:N.', '2026-10-05', NULL, 30);

diagnostyka.php

Dwie zmiany (kod jak w sekcji 5, krok 5):

  • w tablicy $wymaganePliki dochodzi 'sql/taskquest.sql' (razem 18 ścieżek),
  • pod sekcją „Pliki danych” dochodzi sekcja „Baza danych”: extension_loaded('mysqli') → [OK]/[BRAK] oraz lista plików z $plikiSql = listaPlikowWKatalogu('sql', 'sql');.

Reszta pliku bez zmian. Nadal bez sesji, bez config.php, bez połączenia z bazą (ma działać nawet przy zepsutym projekcie).

includes/footer.php

        <p>TaskQuest 1.7</p>
    </div>
</body>
</html>

10. Co zmieniliśmy w TaskQuest?

Zostaje

cały kod PHP z Lekcji 15: klasy Zadanie i Uzytkownik, wszystkie funkcje, index.php, dodaj-zadanie.php,
zaloguj.php, wyloguj.php, sesja, cookie filtr, walidacja formularzy
data/zadania.csv — nadal jedyne źródło zadań aplikacji
symulowane dane gracza w index.php (xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14')

Dochodzi

baza danych taskquest (utf8mb4_unicode_ci), tworzona w phpMyAdmin

sql/taskquest.sql (nowy plik, nowy katalog sql/)
    tabela uzytkownicy: id (PK, AUTO_INCREMENT), login (UNIQUE), imie, haslo_hash (NULL do L19),
                        xp, streak, data_ostatniej_aktywnosci (NULL = brak aktywności)
    tabela zadania:     id (PK, AUTO_INCREMENT), uzytkownik_id (FK → uzytkownicy.id, ON DELETE CASCADE),
                        tytul, opis, termin, data_wykonania (NULL = niewykonane), xp
    dane testowe: Kama (id 1, dane jak symulowane w index.php) z zadaniami z CSV, Ola (id 2) z jednym zadaniem

diagnostyka.php: sekcja „Baza danych” (extension_loaded('mysqli'), lista skryptów .sql), 18 wymaganych plików

Nowe narzędzia: pojęcia bazy relacyjnej (tabela, kolumna, wiersz, klucz główny, klucz obcy, relacja 1:N), SQL: CREATE TABLE, DROP TABLE IF EXISTS, INSERT INTO ... VALUES, SELECT *; typy INT UNSIGNED, VARCHAR(n), DATE; NULL/NOT NULL, DEFAULT, UNIQUE, AUTO_INCREMENT, PRIMARY KEY, FOREIGN KEY ... REFERENCES ... ON DELETE CASCADE, ENGINE=InnoDB, utf8mb4; w PHP: extension_loaded(), class_exists(), PHP_VERSION.

Zmieniamy

  • diagnostyka.php: 18 wymaganych plików + sekcja „Baza danych”,
  • includes/footer.php: TaskQuest 1.7.

Usuwamy

Nic.

Świadome decyzje projektowe

DecyzjaDlaczego
nazwy po polsku, snake_casete same słowa co w PHP i CSV (tytul, termin, xp); dataOstatniejAktywnosci → data_ostatniej_aktywnosci
brak kolumn poziom, tytul_graczaobliczane z XP, bo zapisane obok XP mogłyby sobie przeczyć
data_wykonania zamiast wykonanewykonane obliczamy (!== null); jedna kolumna nie może przeczyć sama sobie; daje datę potrzebną do prawdziwego streaka
id jako klucz główny użytkownika, login UNIQUEklucz się nie zmienia, login jest widoczny i mógłby się zmienić
haslo_hash już teraz (NULL)tabelę użytkowników projektujemy raz; hasła od Lekcji 19
opis NOT NULL DEFAULT ''brak opisu to pusty tekst, jak w klasie (string $opis)

Co nowego potrafi TaskQuest po tej lekcji?

Po Lekcji 17 TaskQuest ma zaprojektowaną i zbudowaną bazę danych: tabelę użytkowników i tabelę zadań połączone relacją 1:N. Każde zadanie ma właściciela, a baza sama pilnuje unikalnych loginów, poprawnych dat, długości tekstów, nieujemnych XP i tego, żeby zadanie nie należało do nieistniejącego użytkownika. Całą strukturę i dane testowe odtwarza jeden skrypt sql/taskquest.sql, a diagnostyka.php potwierdza, że PHP ma rozszerzenie mysqli. Sama aplikacja działa jeszcze tak jak po Lekcji 15 i czyta CSV. W Lekcji 18 przełączymy ją na bazę.

11. Zadania końcowe — rozbuduj TaskQuest samodzielnie

Zadania nie są częścią wersji referencyjnej. Każde zapisuj w osobnym pliku SQL, żeby nie zepsuć taskquest.sql.

A. Odznaki: relacja „wiele do wielu”. TaskQuest ma przyznawać odznaki („Pierwsza piątka”, „Tydzień bez przerwy”…). Jeden użytkownik może mieć wiele odznak, a jedną odznakę może mieć wielu użytkowników.

  1. Dlaczego ani kolumna odznaka_id w uzytkownicy, ani uzytkownik_id w odznaki tego nie rozwiąże?
  2. W pliku sql/odznaki.sql zaprojektuj tabelę odznaki (id, nazwa unikalna, opis) i tabelę pośrednią uzytkownicy_odznaki (uzytkownik_id, odznaka_id, data_zdobycia) z dwoma kluczami obcymi. Klucz główny tabeli pośredniej to para kolumn: PRIMARY KEY (uzytkownik_id, odznaka_id). Co taki klucz uniemożliwia?
  3. Dodaj 2 odznaki i przyznaj Kamie jedną z nich.
  4. Uruchom teraz ponownie taskquest.sql. Co się stało i dlaczego? (Podpowiedź: Eksperyment 1.) Jak musiałby wyglądać początek taskquest.sql, gdyby odznaki weszły do projektu?

B. Baza pilnuje zakresu XP. UNSIGNED odrzuca liczby ujemne, ale przepuszcza 500. Sprawdź wersję serwera (SELECT VERSION();), a potem w kopii definicji tabeli zadania dodaj regułę CHECK (xp BETWEEN 1 AND 100). Przetestuj wstawienie zadania z XP 0, 1, 100 i 101. Obowiązkowo sprawdź w dokumentacji MariaDB (albo MySQL, zależnie od wyniku VERSION()), od której wersji CHECK jest naprawdę egzekwowane, a nie tylko akceptowane w składni.

C. Diagram bazy. phpMyAdmin ma narzędzie Designer (w polskim interfejsie: Projektant; baza taskquest → Więcej). Otwórz go, ułóż tabele czytelnie i zrób zrzut ekranu. Na diagramie wskaż: klucz główny każdej tabeli, klucz obcy i stronę „1” oraz „N” relacji.

Sprzątanie

Gdy testy przeszły i wykonane są eksperymenty:

  1. usuń brudnopis.php,
  2. uruchom ponownie sql/taskquest.sql: baza ma zawierać tylko dane testowe,
  3. sprawdź, czy ON DELETE CASCADE i kolejność DROP w pliku są przywrócone (Eksperymenty 1, 2),
  4. sprawdź index.php (działa jak po L15) i diagnostyka.php (18 × [OK], mysqli [OK]).

Do samodzielnego sprawdzenia

[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię wymienić trzy problemy CSV, które rozwiązuje baza danych.
[ ] Potrafię wyjaśnić na przykładzie TaskQuest: tabela, kolumna, wiersz, klucz główny, klucz obcy.
[ ] Wiem, dlaczego klucz obcy uzytkownik_id jest w tabeli zadania, a nie w uzytkownicy.
[ ] Potrafię dobrać typ kolumny (INT UNSIGNED, VARCHAR(n), DATE) i zdecydować o NULL / NOT NULL.
[ ] Wiem, czym NULL różni się od '' i od 'NULL'.
[ ] Potrafię wyjaśnić, dlaczego w bazie nie ma poziomu ani kolumny wykonane.
[ ] Mam bazę taskquest (utf8mb4_unicode_ci) i plik sql/taskquest.sql, który działa przy każdym uruchomieniu.
[ ] Testy 2–5 kończą się oczekiwanymi błędami (albo wiem, dlaczego u mnie nie).
[ ] Potrafię zbudować obiekt Zadanie z wiersza z kolumną data_wykonania (sekcja 6).
[ ] diagnostyka.php pokazuje 18 × [OK] i mysqli [OK].
[ ] Aplikacja działa jak po Lekcji 15 i wiem, że jeszcze nie korzysta z bazy.
[ ] Usunęłam/usunąłem brudnopis.

Pytania kontrolne

  1. Kuba i Ola dodają zadanie w tej samej sekundzie. Co może się stać w TaskQuest 1.5 (CSV) i dlaczego w bazie ten problem nie występuje w ten sam sposób?

  2. Dlaczego zadanie ma kolumnę uzytkownik_id, a użytkownik nie ma kolumny z listą swoich zadań?

  3. Przewidź wynik każdego polecenia (przyjęty / błąd, jaki?), gdy w bazie są tylko Kama (id 1) i Ola (id 2):INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (2, 'A', '2026-10-01', 5);
    INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (3, 'B', '2026-10-01', 5);
    INSERT INTO uzytkownicy (login, imie) VALUES ('ola_tq', 'Ola 2');
    INSERT INTO uzytkownicy (login, imie) VALUES ('Nowy_1', 'Nowy');
    Jakie XP i jaki streak będzie miał użytkownik Nowy_1? Skąd to wiadomo?

  4. Kolega proponuje dodać do uzytkownicy kolumnę poziom INT, „żeby nie liczyć za każdym razem”. Podaj konkretny przykład danych, które wtedy mogą sobie przeczyć, i wskaż, który fragment kodu TaskQuest już ten poziom liczy.

  5. Czym różnią się dla bazy te trzy wartości data_wykonania: NULL, '', 'NULL'? Która oznacza zadanie niewykonane?

  6. W taskquest.sql jest błąd. Znajdź go (są dwa).CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE,
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

  7. Dlaczego sql/taskquest.sql zaczyna się od DROP TABLE IF EXISTS zadania, a nie ... uzytkownicy? Co by się stało na szkolnym serwerze z prawdziwymi danymi uczniów, gdyby ktoś uruchomił ten skrypt?

  8. W Lekcji 18 z bazy przyjdzie wiersz ['id' => 7, 'tytul' => 'X', ..., 'data_wykonania' => '2026-09-21', ...]. Napisz fragment tworzenia obiektu Zadanie, który ustawia wykonane. Czy klasa Zadanie musi się zmienić? Dlaczego nie?

  9. diagnostyka.php pokazuje [OK] przy mysqli. Czy to znaczy, że TaskQuest połączy się z bazą? Czego ta diagnostyka jeszcze nie sprawdza?

  10. Dlaczego mysql_connect() z poradnika z 2012 roku nie zadziała w Twoim PHP, a class_exists('mysqli') zwraca true? Co w Lekcji 18 napiszesz, żeby utworzyć połączenie


PODSUMOWANIE LEKCJI

W skrócie

Baza danych a plik

  • W pliku CSV każda wartość to tekst, a poprawności pilnuje wyłącznie kod PHP. W bazie każda kolumna ma typ i reguły, a baza sama odrzuca to, co ich nie spełnia.
  • Baza to druga linia obrony, a nie pierwsza. Walidacji w PHP nigdy nie usuwamy.

Kolumny i wartości

  • Definicja kolumny to zawsze: nazwa – typ – reguły (NOT NULL, DEFAULT, UNIQUE, UNSIGNED, AUTO_INCREMENT).
  • NULL, '', 0 i 'NULL' to cztery różne rzeczy. NULL = brak wartości, '' = istniejący pusty tekst, 'NULL' = napis.
  • UNSIGNED oznacza tylko „≥ 0”. Nie pilnuje zakresu (np. 1–100) — od tego jest walidacja w PHP albo CHECK.
  • Data w SQL: zawsze RRRR-MM-DD. Teksty i daty w apostrofach, liczby i NULL bez apostrofów.
  • VARCHAR(n) to maksymalna długość, nie stała.

Klucze i relacje

  • Klucz główny (PRIMARY KEY) jednoznacznie wskazuje wiersz: zawsze unikalny i nigdy NULL. Kluczem robimy id, a nie login — login jest widoczny i może się zmienić.
  • Klucz obcy (FOREIGN KEY) wskazuje klucz główny innej tabeli. W relacji 1:N klucz obcy zawsze stoi po stronie „N” (uzytkownik_id jest w zadania).
  • Typ kolumny klucza obcego musi być zgodny z typem klucza, na który wskazuje.
  • Klucze obce działają tylko przy ENGINE=InnoDB.
  • ON DELETE CASCADE = usunięcie „rodzica” usuwa powiązane wiersze. Bez tej opcji baza odmówi usunięcia rodzica (#1451).

Kolejność poleceń

  • Usuwamy od tabel zależnych („N”), tworzymy od niezależnych („1”). W taskquest.sql: najpierw DROP TABLE IF EXISTS zadania, potem uzytkownicy.
  • DROP TABLE IF EXISTS na początku skryptu pozwala uruchamiać go wiele razy — kosztem usunięcia wszystkich danych.

Projekt danych

  • W bazie zapisujemy tylko to, czego nie da się obliczyć z innych danych (dlatego nie ma kolumn poziom, tytul_gracza, ani wykonane obok data_wykonania).
  • Dwie kolumny opisujące to samo prędzej czy później zaczną sobie przeczyć, a baza nie wie, która kłamie.

Kodowanie i porównywanie

  • utf8mb4 mieści emoji (4 bajty), utf8mb3 (dawne utf8) nie — stąd błąd #1366.
  • Metoda porównywania ..._ci (case insensitive) nie rozróżnia wielkości liter: przy UNIQUE Kama_2026 i KAMA_2026 to ten sam login.

PHP i baza

  • Rozszerzenie mysql (mysql_connect()) zostało usunięte w PHP 7.0 — kod ze starych poradników nie zadziała.
  • mysqli – tylko MySQL/MariaDB, dostępne proceduralnie i obiektowo. PDO – wiele silników baz, tylko obiektowo, parametry nazwane :nazwa. W TaskQuest używamy mysqli obiektowo (new mysqli(...)).
  • extension_loaded('mysqli') mówi tylko, że PHP potrafi rozmawiać z bazą. Nie sprawdza serwera, danych dostępowych ani istnienia bazy.

Co trzeba umieć na INF.03?

Praca w phpMyAdmin (część praktyczna egzaminu):

  • utworzyć bazę o podanej w treści nazwie i z podanym zestawem znaków (w arkuszu: baza wodospady, utf8_unicode_ci — na egzaminie stosuj zapis z polecenia, nawet jeśli różni się od utf8mb4_unicode_ci używanego w TaskQuest),
  • zaimportować tabele z gotowego pliku .sql i rozpoznać, że import się powiódł,
  • zapisywać zapytania w pliku tekstowym i dokumentować wynik zrzutem ekranu.

Projekt i struktura bazy:

  • określić pojęcia: encja, atrybut, związek encji, klucz relacji (u nas: tabela, kolumna, wiersz, klucz główny, klucz obcy),
  • odczytać z diagramu bazy tabele, klucze i liczebność relacji: 1:N oraz m:n realizowaną tabelą pośredniczącą (w arkuszu: obiekty – osiagniecia – turysci; u nas to zadanie końcowe A z odznakami),
  • dobrać typ danych do atrybutu i wskazać klucz główny encji,
  • zdefiniować tabelę poleceniem CREATE TABLE z typami, NOT NULL, DEFAULT, UNIQUE, AUTO_INCREMENT, PRIMARY KEY, FOREIGN KEY,
  • wprowadzić dane poleceniem INSERT, w tym do tabeli, w której klucz podstawowy nadawany jest automatycznie (pomijamy kolumnę id).

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. Szkielet tabeli: CREATE TABLE nazwa ( kolumna TYP REGUŁY, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; — bez przecinka po ostatniej pozycji (inaczej #1064).
  2. Klucz główny: id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY.
  3. Klucz obcy: FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE — jako ostatnia pozycja w nawiasie, po przecinku.
  4. W relacji 1:N klucz obcy jest po stronie „N”.
  5. NULL ≠ '' ≠ 0 ≠ 'NULL'.
  6. Data to '2026-10-05'. Liczba bez apostrofów. NULL bez apostrofów.
  7. UNSIGNED = „≥ 0”, nic więcej.
  8. Tworzymy: uzytkownicy → zadania. Usuwamy: zadania → uzytkownicy.
  9. W bazie zapisujemy tylko dane, których nie da się obliczyć.
  10. AUTO_INCREMENT nigdy nie wraca do zwolnionych numerów.
  11. mysql_* nie istnieje od PHP 7. Zostają mysqli i PDO.

Ściągawka – najważniejsze polecenia i składnia

Typy kolumn

PHPSQLUwaga
int (nieujemny)INT UNSIGNEDzakresu nie pilnuje
string z limitemVARCHAR(n)n = maksimum znaków
długi tekstTEXTbez limitu długości w definicji
data RRRR-MM-DDDATEbaza sprawdza kalendarz
boolbrak osobnego typuzwykle TINYINT(1) albo — jak u nas — kolumna DATE NULL

Reguły kolumn

NOT NULL          -- wartość musi być podana
NULL              -- wartość może nie istnieć
DEFAULT 0         -- wartość, gdy jej nie podamy
UNIQUE            -- nie może się powtórzyć w tabeli
UNSIGNED          -- liczba ≥ 0
AUTO_INCREMENT    -- baza sama nadaje kolejne numery
PRIMARY KEY       -- klucz główny

Tabela, klucz obcy, usuwanie

DROP TABLE IF EXISTS zadania;          -- najpierw tabela zależna
DROP TABLE IF EXISTS uzytkownicy;

CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT UNSIGNED NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    opis VARCHAR(300) NOT NULL DEFAULT '',
    termin DATE NOT NULL,
    data_wykonania DATE NULL,
    xp INT UNSIGNED NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Dane

-- komentarz w SQL (dwa minusy i spacja)
INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);

SELECT * FROM zadania;
SELECT * FROM zadania WHERE uzytkownik_id = 2;

Apostrof w tekście podwajamy: 'Rock''n''roll'.

Relacja m:n (tabela pośrednicząca)

CREATE TABLE uzytkownicy_odznaki (
    uzytkownik_id INT UNSIGNED NOT NULL,
    odznaka_id INT UNSIGNED NOT NULL,
    data_zdobycia DATE NOT NULL,
    PRIMARY KEY (uzytkownik_id, odznaka_id),
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE,
    FOREIGN KEY (odznaka_id) REFERENCES odznaki(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

phpMyAdmin – kolejność kliknięć

  1. Zakładka Bazy danych → nazwa + metoda porównywania → Utwórz.
  2. Kliknij bazę w lewym panelu (skrypt sam jej nie wybiera!) → zakładka SQL → wklej → Wykonaj.
  3. Struktura tabeli: kliknij tabelę → zakładka Struktura.
  4. Import gotowego pliku: baza → zakładka Import → wybierz plik .sql → Wykonaj.

Sprawdzenie PHP

var_dump(extension_loaded('mysqli'));   // czy rozszerzenie jest włączone
var_dump(extension_loaded('pdo_mysql'));
var_dump(class_exists('mysqli'));       // mysqli to klasa
echo PHP_VERSION;

W php.ini: extension=mysqli, extension=pdo_mysql (usuń średnik, zrestartuj Apache).

Wiersz z bazy → obiekt

wykonane: $wiersz['data_wykonania'] !== null,

Najczęstsze kody błędów

KodZnaczenie
#1064błąd składni (np. przecinek po ostatniej kolumnie, cudzysłów typograficzny)
#1048kolumna NOT NULL dostała NULL
#1062złamana reguła UNIQUE (duplikat)
#1264wartość poza zakresem (np. -5 w UNSIGNED)
#1292zła data (2026-02-31, '14.09.2026', 'NULL')
#1366zły znak dla kodowania (emoji w utf8mb3)
#1406tekst dłuższy niż VARCHAR(n)
#1451nie można usunąć „rodzica”, bo ma powiązane wiersze
#1452klucz obcy wskazuje na nieistniejący wiersz

Quiz – sprawdź się

1. Dobierz typ kolumny do każdej właściwości klasy: int $xp (1–100), string $tytul (maks. 100 znaków), string $termin (RRRR-MM-DD), string $opis (może być pusty).

2. Które z tych poleceń zakończy się błędem i jakim? Zakładamy tabelę zadania z lekcji.

INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'A', '14.09.2026', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, data_wykonania, xp) VALUES (1, 'B', '2026-10-01', 'NULL', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'C', '2026-10-01', '10');

3. W tabeli jest już użytkownik Kama_2026. Co zrobi baza z poleceniem poniżej, jeśli baza ma metodę porównywania utf8mb4_unicode_ci?

INSERT INTO uzytkownicy (login, imie) VALUES ('KAMA_2026', 'Trzecia Kama');

4. Uzupełnij definicję tak, żeby zadanie nie mogło należeć do nieistniejącego użytkownika, a usunięcie użytkownika kasowało jego zadania:

uzytkownik_id INT UNSIGNED NOT NULL,
______ (uzytkownik_id) ______ uzytkownicy(id) ______

5. Kolega zmienił w CREATE TABLE zadania silnik na ENGINE=MyISAM. Skrypt wykonał się bez błędu. Co przestanie działać?

6. Co wypisze ostatnie polecenie i dlaczego?

INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Pierwsze', '2026-10-01', 10);
DELETE FROM zadania WHERE tytul = 'Pierwsze';
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Drugie', '2026-10-01', 10);
SELECT id, tytul FROM zadania;

7. xp INT UNSIGNED NOT NULL. Czy baza przyjmie zadanie z XP równym 500? Co w TaskQuest pilnuje zakresu 1–100?

8. W bazie są tylko użytkownicy o id 1 i 2. Co zwróci poniższe zapytanie: błąd, pusty wynik czy wszystkie zadania?

SELECT * FROM zadania WHERE uzytkownik_id = 3;

9. Uczeń chce przechowywać informację, że jeden gracz może mieć wiele odznak, a jedną odznakę może mieć wielu graczy. Proponuje dodać do tabeli uzytkownicy kolumnę odznaki VARCHAR(255) z listą nazw po przecinku. Podaj dwa konkretne problemy tego rozwiązania i napisz, jak zrobić to poprawnie.

10. Uzupełnij zdania:
a) Stare rozszerzenie mysql zostało usunięte w PHP __ , dlatego mysql_connect() dziś __ .
b) PDO obsługuje wiele silników baz danych, a mysqli tylko __ .
c) mysqli można używać proceduralnie (mysqli_connect(...)) albo obiektowo (`__
). W TaskQuest używamy wersji ____ . d) extension_loaded(’mysqli’)zwróciłotrue`. Czy to znaczy, że aplikacja połączy się z bazą? ____


Odpowiedzi
  1. xp INT UNSIGNED NOT NULL (zakresu 1–100 ten typ nie wymusza), tytul VARCHAR(100) NOT NULL, termin DATE NOT NULL, opis VARCHAR(300) NOT NULL DEFAULT '' (brak opisu to pusty tekst, nie NULL).
  2. Pierwsze — błąd #1292: data musi być w formacie RRRR-MM-DD. Drugie — błąd #1292: 'NULL' to napis, a nie brak wartości (poprawnie: NULL bez apostrofów). Trzecie się wykona — baza przekonwertuje '10' na liczbę, ale to zła praktyka: apostrofy sugerują tekst.
  3. Błąd #1062 Duplicate entry. Metoda porównywania _ci nie rozróżnia wielkości liter, więc dla reguły UNIQUE to ten sam login co Kama_2026.
  4. FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
  5. Klucz obcy. MyISAM przyjmuje zapis FOREIGN KEY, ale go nie egzekwuje — do tabeli zadania trafi zadanie z nieistniejącym uzytkownik_id, a ON DELETE CASCADE nic nie usunie. Dlatego używamy ENGINE=InnoDB.
  6. Zadanie „Drugie” dostanie następny numer, a nie numer zwolniony przez „Pierwsze” — w kolumnie id powstaje dziura. AUTO_INCREMENT nie wraca do wykorzystanych numerów. Ta sama zasada co w nastepneIdZadania() („największe id + 1”, nie count() + 1).
  7. Tak, przyjmie. UNSIGNED odrzuca tylko liczby ujemne. Zakresu 1–100 pilnuje walidujNoweZadanie() w PHP; w bazie można go wymusić regułą CHECK (zadanie końcowe B).
  8. Pusty wynik, bez błędu. Warunek WHERE po prostu nie pasuje do żadnego wiersza — to nie jest błąd składni ani naruszenie reguły.
  9. Problemy m.in.: baza nie sprawdzi, czy taka odznaka w ogóle istnieje (brak klucza obcego); nie da się zapisać daty zdobycia odznaki; wyszukanie „kto ma odznakę X” wymaga przeszukiwania tekstu; zmiana nazwy odznaki wymaga poprawienia jej w każdym wierszu; limit 255 znaków ograniczy liczbę odznak. Poprawnie: osobna tabela odznaki i tabela pośrednicząca uzytkownicy_odznaki z dwoma kluczami obcymi i kluczem głównym złożonym z pary kolumn (relacja m:n).
  10. a) 7.0; nie zadziała (funkcja nie istnieje). b) MySQL/MariaDB. c) new mysqli(...); obiektowej. d) Nie — to znaczy tylko, że PHP ma włączone rozszerzenie. Nie sprawdza, czy serwer bazy działa, czy dane dostępowe są poprawne i czy baza istnieje.