Szybki quiz – przypomnienie poprzedniej lekcji
Lekcja 14: cookies i sesje. Odpowiedz z pamięci, bez zaglądania do kodu.
1. Co wyświetli ten skrypt przy pierwszym otwarciu strony, a co przy drugim?
<?php
setcookie('motyw', 'ciemny', time() + 3600);
echo $_COOKIE['motyw'] ?? '(brak)';
2. Znajdź błąd i powiedz, jaki komunikat zobaczy użytkownik:
<?php
echo "<h1>TaskQuest</h1>";
session_start();
$_SESSION['login'] = 'Kama';
3. W TaskQuest login zalogowanego użytkownika trzymamy w sesji, a wybrany widok listy — w ciasteczku. Podaj powód tej decyzji (jedno zdanie dla każdego).
4. Wskaż dwa problemy w tej linii:
if ($_COOKIE['rola'] === 'admin') { /* panel administratora */ }
5. Uzupełnij kod usuwający ciasteczko filtr:
setcookie('filtr', '', ______);
6. Zapisujemy liczbę 99 raz do sesji ($_SESSION['xp'] = 99;), raz do ciasteczka (setcookie('xp', 99, time() + 3600);). Co pokaże var_dump() dla każdej z tych wartości w następnym żądaniu?
7. Co wyświetli poniższy kod przy drugim odświeżeniu strony? A co, gdyby zabrakło pierwszej linii?
<?php
session_start();
$_SESSION['wizyty'] = ($_SESSION['wizyty'] ?? 0) + 1;
echo $_SESSION['wizyty'];
Odpowiedzi
- Za pierwszym razem
(brak), za drugimciemny.setcookie()wysyła nagłówek do przeglądarki, a$_COOKIEzawiera to, co przyszło w tym żądaniu — nowe ciasteczko pojawi się tam dopiero w kolejnym. session_start()jest po wypisaniu HTML, a wysyła nagłówek (ciasteczkoPHPSESSID):Warning: Cannot modify header information - headers already sent. Sesję trzeba uruchomić przed jakąkolwiek treścią strony.- Login — w sesji, bo dane sesji leżą na serwerze i użytkownik nie może ich podmienić; od nich zależy, kim jest i co mu wolno. Widok listy — w cookie, bo to tylko wygoda, ma przetrwać zamknięcie przeglądarki, a ewentualna podmiana oznacza jedynie inny wygląd listy.
- Po pierwsze: brak
??— przy braku ciasteczkaUndefined array key 'rola'. Po drugie: ufanie danym z cookie — użytkownik wpisze sobieadminw F12. O uprawnieniach decyduje sesja, nie ciasteczko. time() - 3600(dowolny czas w przeszłości) — przeglądarka sama usuwa wygasłe ciasteczko. Przy ciasteczku ustawionym ze ścieżką trzeba podać tę samą ścieżkę.- Sesja:
int(99)— sesja zachowuje typy PHP. Cookie:string(2) "99"— wszystko, co przychodzi od przeglądarki, jest tekstem. - Wyświetli
2. Bezsession_start()tablica$_SESSIONnie zostanie wypełniona danymi z sesji — zapis nie przetrwa żądania i zawsze zobaczymy1(a w zależności od konfiguracji także ostrzeżenie o nieuruchomionej sesji).
Programowanie obiektowe – OOP (object-oriented programming) – pozwala na przedstawienie rzeczywistości i relacji w niej zachodzących za pomocą obiektów.
Po co nam programowanie obiektowe?
Programowanie obiektowe (ang. Object-Oriented Programming, OOP) wprowadza strukturę i organizację do kodu, co ułatwia jego tworzenie, rozwijanie i utrzymanie. Poniżej znajdziesz wyjaśnienie, po co stosować programowanie obiektowe, z przykładami jego głównych zalet:
1. Organizacja i modularność
- OOP pozwala dzielić kod na mniejsze, logiczne fragmenty (klasy), które odpowiadają rzeczywistym obiektom lub pojęciom.
- Dzięki temu kod jest bardziej przejrzysty i łatwiejszy w zarządzaniu.
Przykład: W grze komputerowej można mieć klasy Gracz, Przeciwnik, Broń. Każda z nich zawiera tylko dane i funkcje związane z tym obiektem.
2. Reużywalność kodu
- Możesz tworzyć klasy i wykorzystywać je wielokrotnie w różnych projektach lub częściach programu.
- Dziedziczenie umożliwia tworzenie nowych klas na podstawie istniejących, dzięki czemu kod jest bardziej elastyczny.
Przykład: Klasa Pojazd może mieć podklasy Samochód, Motocykl, które dziedziczą podstawowe cechy pojazdu (np. prędkość, masa), ale dodają swoje specyficzne funkcje.
3. Łatwiejsze rozwijanie aplikacji
- W OOP zmiany są łatwiejsze do wprowadzenia, ponieważ każda klasa działa jako oddzielny moduł.
- Jeśli potrzebujesz zmienić zachowanie jednego elementu, modyfikujesz tylko odpowiednią klasę, bez wpływu na inne części programu.
Przykład: Dodanie nowej broni w grze wymaga jedynie stworzenia nowej klasy na podstawie istniejącego schematu, np. Broń.
4. Ukrywanie szczegółów (enkapsulacja)
- OOP pozwala ukrywać szczegóły implementacji, dzięki czemu kod jest bardziej bezpieczny i trudniejszy do przypadkowego uszkodzenia.
- Dane w klasach są chronione, a dostęp do nich odbywa się przez ściśle określone metody.
Przykład: W klasie KontoBankowe saldo konta jest ukryte, a użytkownik może je zmieniać tylko za pomocą metod wpłać() i wypłać().
5. Naturalne odwzorowanie rzeczywistości
- OOP naśladuje sposób, w jaki myślimy o świecie, opierając się na obiektach, ich cechach i zachowaniach.
- Dzięki temu kod staje się bardziej intuicyjny i łatwiejszy do zrozumienia.
Przykład: Klasa Kot ma cechy takie jak kolor futra (atrybuty) i umiejętności jak miauczenie (metody).
6. Łatwość debugowania i testowania
- Dzięki temu, że klasy są niezależne, błędy w jednym obiekcie nie wpływają na inne.
- Testowanie poszczególnych modułów (klas) jest prostsze niż testowanie całości kodu.
7. Polimorfizm – elastyczność w działaniu
- Polimorfizm umożliwia różnym obiektom używanie tej samej metody, ale w różny sposób.
- Pozwala to na pisanie bardziej uniwersalnego i elastycznego kodu.
Przykład: Metoda atakuj() może działać inaczej w klasie Rycerz i Mag, mimo że jest wywoływana w ten sam sposób.
8. Ułatwienie pracy zespołowej
- W dużych projektach, podział kodu na klasy ułatwia współpracę, ponieważ każdy programista może pracować nad innymi obiektami, nie wchodząc sobie w drogę.
Podsumowanie
Programowanie obiektowe jest użyteczne, gdy:
- Tworzysz duże, złożone systemy.
- Chcesz pisać kod, który łatwo rozwijać, utrzymywać i testować.
- Potrzebujesz odzwierciedlić rzeczywiste pojęcia lub obiekty w swoim kodzie.
Jeśli jednak tworzysz mały, prosty skrypt, nie zawsze konieczne jest stosowanie OOP – wtedy wystarczą inne podejścia, jak programowanie proceduralne.
Klasa i obiekt
Kiedy mówimy o klasie musimy wyobrazić ją sobie jak ogólny zarys/opis jakiegoś obiektu, zbiór cech wspólnych dla np. człowieka. Gdybyśmy mieli klasę człowiek moglibyśmy określić ogólne cechy jakie definiują ludzi, czyli np. to że śpi, że ma głos, oczy, włosy, ręce, nogi itd.
Obiekt jest instancją danej klasy, czyli konkretnym człowiekiem np. Janem Kowalskim, który ma brązowe włosy, jest typem „skowronka” i ma niebieskie oczy. Te cechy nie są bezpośrednio związane z klasą, a z obiektem (instancją/wystąpieniem klasy).
Można powiedzieć, że Jan Kowalski jest zmienną typu klasy, czyli obiektem typu człowiek.
Przykład utworzenia klasy – Streamer na Twitchu
<?php
// Tworzymy klasę Streamer
class Streamer {
public $nick;
public $subskrybcje;
public $followers;
public $kategoria;
}
?>
Mając tak zdefiniowana klasę możemy otworzyć konkretny obiekt:
<?php
$streamer1 = new Streamer(); // Tworzymy nowy obiekt klasy Streamer
// Nadajemy konkretne cechy
$streamer1->nick = 'GamingLegend';
$streamer1->subskrybcje = 1520;
$streamer1->followers = 45000;
$streamer1->kategoria = 'League of Legends';
// Wyświetlamy rezultaty
echo 'Streamer: ' . $streamer1->nick . '<br>';
echo 'Subskrybcje: ' . $streamer1->subskrybcje . '<br>';
echo 'Followersi: ' . $streamer1->followers . '<br>';
echo 'Gra w: ' . $streamer1->kategoria . '<br>';
/*
Wyświetli się:
Streamer: GamingLegend
Subskrybcje: 1520
Followersi: 45000
Gra w: League of Legends
*/
?>
Wyświetlenie stworzonego obiektu
Możemy wyświetlić sobie stworzony obiekt:
<?php
echo '<pre>';
print_r($streamer1);
echo '</pre>';
/*
Wyświetli się:
Streamer Object
(
[nick] => GamingLegend
[subskrybcje] => 1520
[followers] => 45000
[kategoria] => League of Legends
)
*/
?>
Wartości domyślne w klasie
Klasie możemy nadać domyślne wartości, które zostaną nadane w momencie tworzenia obiektu:
<?php
class Streamer {
public $nick;
public $subskrybcje = 0; // Wartość domyślna
public $followers = 0; // Wartość domyślna
public $kategoria = 'Just Chatting'; // Wartość domyślna
}
$streamer1 = new Streamer(); // Tworzymy nowy obiekt
// Nadajemy tylko niektóre cechy
$streamer1->nick = 'NewbieStreamer';
$streamer1->followers = 150;
// Wyświetlamy rezultaty
echo 'Streamer: ' . $streamer1->nick . '<br>';
echo 'Subskrybcje: ' . $streamer1->subskrybcje . '<br>';
echo 'Followersi: ' . $streamer1->followers . '<br>';
echo 'Kategoria: ' . $streamer1->kategoria . '<br>';
/*
Wyświetli się:
Streamer: NewbieStreamer
Subskrybcje: 0
Followersi: 150
Kategoria: Just Chatting
*/
?>
Już potrafisz:
- stworzyć definicję obiektu w postaci klasy
- dodać publiczne pola do klasy i nadawać im wartości domyślne
- przypisywać im wartości
- odczytywać ich wartość
- korzystać z „->”, by dostać się do pola klasy
Metody
Klasa może definiować metody, czyli nic innego jak funkcje z podejścia proceduralnego. Kiedy tworzymy obiekt nadajemy mu dostęp do metod danej klasy.
Trochę praktyki – Discord Bot
<?php
class DiscordBot {
public $nazwa;
public $serwery = 0; // Liczba serwerów
public $uzytkownicy = 0; // Liczba użytkowników
public $wersja = '1.0'; // Wersja bota
// Teraz tworzymy metodę
public function pobierzStatus() {
return "Bot {$this->nazwa} jest aktywny na {$this->serwery} serwerach.";
}
}
$bot1 = new DiscordBot();
$bot1->nazwa = 'MusicBot';
$bot1->serwery = 350;
$bot2 = new DiscordBot();
$bot2->nazwa = 'ModerationBot';
$bot2->serwery = 1200;
$bot3 = new DiscordBot();
$bot3->nazwa = 'GamesBot';
echo '<pre>';
print_r($bot1);
echo '</pre>';
echo $bot1->pobierzStatus() . '<br>';
echo '<pre>';
print_r($bot2);
echo '</pre>';
echo $bot2->pobierzStatus() . '<br>';
echo '<pre>';
print_r($bot3);
echo '</pre>';
echo $bot3->pobierzStatus() . '<br>';
?>
Zostanie wyświetlone:
DiscordBot Object
(
[nazwa] => MusicBot
[serwery] => 350
[uzytkownicy] => 0
[wersja] => 1.0
)
Bot MusicBot jest aktywny na 350 serwerach.
DiscordBot Object
(
[nazwa] => ModerationBot
[serwery] => 1200
[uzytkownicy] => 0
[wersja] => 1.0
)
Bot ModerationBot jest aktywny na 1200 serwerach.
DiscordBot Object
(
[nazwa] => GamesBot
[serwery] => 0
[uzytkownicy] => 0
[wersja] => 1.0
)
Bot GamesBot jest aktywny na 0 serwerach.
Zmienna $this
Aby dostać się do konkretnego pola w obiekcie, korzystamy ze zmiennej $this.
W powyższym przykładzie nadawaliśmy wartości odnosząc się bezpośrednio do pola klasy. Nie jest to dobra metoda, ponieważ użytkownik może wpisać co chce w dane pole i niestety skrypt może się „wysypać”.
Aby tego uniknąć dodajemy dane za pomocą metody, którą możemy łatwo zabezpieczyć przed niechcianym kodem.
Bezpieczne metody – setter i getter
Powyższy kod powinien wyglądać następująco:
<?php
class DiscordBot {
public $nazwa;
public $serwery = 0;
public $uzytkownicy = 0;
public $wersja = '1.0';
// Metoda pobierająca status (getter)
public function pobierzStatus() {
return "Bot {$this->nazwa} jest aktywny na {$this->serwery} serwerach.";
}
// Metoda przypisująca liczbę serwerów (setter)
public function przypiszSerwery($ilosc) {
// Można dodać tutaj sprawdzanie czy wprowadzona wartość jest int
// i mieści się w określonym przedziale
$this->serwery = $ilosc;
}
}
$bot1 = new DiscordBot();
$bot1->nazwa = 'MusicBot';
$bot1->przypiszSerwery(350); // Bezpieczne przypisanie
$bot2 = new DiscordBot();
$bot2->nazwa = 'ModerationBot';
$bot2->przypiszSerwery(1200);
$bot3 = new DiscordBot();
$bot3->nazwa = 'GamesBot';
echo '<pre>';
print_r($bot1);
echo '</pre>';
echo $bot1->pobierzStatus() . '<br>';
echo '<pre>';
print_r($bot2);
echo '</pre>';
echo $bot2->pobierzStatus() . '<br>';
echo '<pre>';
print_r($bot3);
echo '</pre>';
echo $bot3->pobierzStatus() . '<br>';
?>
Walidacja danych w metodach
A teraz przykład z walidacją liczby serwerów:
<?php
class DiscordBot {
public $nazwa;
public $serwery = 'brak danych'; // Wartość domyślna
public $uzytkownicy = 0;
public $wersja = '1.0';
// Metoda pobierająca status
public function pobierzStatus() {
return "Bot {$this->nazwa} jest aktywny na {$this->serwery} serwerach.";
}
// Metoda przypisująca liczbę serwerów Z WALIDACJĄ
public function przypiszSerwery($ilosc) {
// Sprawdzanie czy wprowadzona wartość jest int i mieści się w przedziale
$ilosc = intval($ilosc);
if ($ilosc > 0 && $ilosc < 1000000) {
$this->serwery = $ilosc;
}
}
}
$bot1 = new DiscordBot();
$bot1->nazwa = 'MusicBot';
$bot1->przypiszSerwery('tekst'); // Nieprawidłowa wartość - zostanie odrzucona
$bot2 = new DiscordBot();
$bot2->nazwa = 'ModerationBot';
$bot2->przypiszSerwery(1200); // Prawidłowa wartość
$bot3 = new DiscordBot();
$bot3->nazwa = 'GamesBot';
echo '<pre>';
print_r($bot1);
echo '</pre>';
echo $bot1->pobierzStatus() . '<br>';
echo '<pre>';
print_r($bot2);
echo '</pre>';
echo $bot2->pobierzStatus() . '<br>';
echo '<pre>';
print_r($bot3);
echo '</pre>';
echo $bot3->pobierzStatus() . '<br>';
?>
Wyświetli się:
DiscordBot Object
(
[nazwa] => MusicBot
[serwery] => brak danych
[uzytkownicy] => 0
[wersja] => 1.0
)
Bot MusicBot jest aktywny na brak danych serwerach.
DiscordBot Object
(
[nazwa] => ModerationBot
[serwery] => 1200
[uzytkownicy] => 0
[wersja] => 1.0
)
Bot ModerationBot jest aktywny na 1200 serwerach.
DiscordBot Object
(
[nazwa] => GamesBot
[serwery] => brak danych
[uzytkownicy] => 0
[wersja] => 1.0
)
Bot GamesBot jest aktywny na brak danych serwerach.
Już potrafisz:
- stworzyć metodę klasy
- zwrócić wartość i przekazać argumenty
- korzystać ze zmiennej $this
- zabezpieczyć dane
Modyfikatory dostępu
Do tej pory wykonywaliśmy przykłady w oparciu o publiczną dostępność danego elementu, czyli public. Taka metoda lub właściwość jest publiczna, więc widoczna wszędzie wewnątrz obiektu i poza nim. Dostęp do pola jest nieograniczony i można je odczytywać i zmieniać dowolnie.
Właściwość lub metoda może być prywatna, czyli private, wówczas może zostać użyta tylko wewnątrz obiektu.
Właściwość lub metoda protected może być użyta tylko wewnątrz obiektu, ale może być również dziedziczona.
Zaleca się w programowaniu i w życiu, nie udostępniać zbyt wiele informacji na zewnątrz, więc tego się trzymajmy – korzystamy z modyfikatorów private.
Przykład public – Serwer gry online
<?php
class SerwerGry {
public $czyOnline = true;
public function wlacz() {
$this->czyOnline = true;
}
public function wylacz() {
$this->czyOnline = false;
}
public function sprawdzStatus() {
return $this->czyOnline;
}
}
$serwer = new SerwerGry();
$serwer->wylacz();
$serwer->czyOnline = true; // Zmieniamy wartość parametru bezpośrednio!
echo $serwer->sprawdzStatus() ? 'Serwer online' : 'Serwer offline';
// Wyświetli: Serwer online
?>
W powyższym przykładzie należy zwrócić uwagę na to, że pomimo iż ostatnią operacją na obiekcie było wyłączenie serwera, wprowadzenie bezpośredniej zmiany pola $serwer->czyOnline na true, sprawiło że otrzymujemy informację, że serwer jest online.
Możemy tego uniknąć blokując dostęp do pól i metod klasy, czyli zmieniając ich modyfikatory z publicznych na prywatne.
Ustawiamy wartość prywatną:
<?php
class SerwerGry {
private $czyOnline = true; // Modyfikator prywatny, brak dostępu z zewnątrz
public function wlacz() {
$this->czyOnline = true;
}
public function wylacz() {
$this->czyOnline = false;
}
public function sprawdzStatus() {
return $this->czyOnline;
}
}
$serwer = new SerwerGry();
$serwer->wylacz();
$serwer->czyOnline = true; // Chcemy zmienić wartość parametru
// Tutaj interpreter zwróci błąd, gdyż nie ma dostępu do zmiennej
echo $serwer->sprawdzStatus() ? 'Serwer online' : 'Serwer offline';
?>
Przykład powyższy generuje błąd, ponieważ nie ma dostępu do prywatnej wartości pola klasy.
Ustawimy teraz metody na prywatne i publiczne:
<?php
class StreamConfig {
private $bitrate = 6000;
private function obliczJakosc() {
return $this->bitrate / 1000 . 'Mbps';
}
public function czyMoznaStreamowac($upload) {
return $this->bitrate < $upload ? 'TAK' : 'NIE';
}
}
$config = new StreamConfig();
echo '<pre>';
print_r($config);
echo '</pre>';
echo $config->czyMoznaStreamowac(8000); // Wyświetli: TAK
echo $config->czyMoznaStreamowac(3000); // Wyświetli: NIE
// echo $config->obliczJakosc(); // Tutaj interpreter zwróci błąd,
// gdyż nie ma dostępu do metody
?>
O modyfikatorze protected, trochę później – przy temacie dziedziczenie.
Już potrafisz:
- ograniczać dostęp do pól klasy i metod
- pozwalać klasie bazowej oraz klasom dziedziczącym na modyfikację pola lub metody
Dziedziczenie
Tak jak w życiu – jako potomkowie dziedziczymy po naszych przodkach pewne cechy (tak naprawdę to 50% od matki i 50% od ojca), tak samo klasy dziedziczą pola i metody, ale mogą zostać uzupełnione o inne elementy, których przodek nie posiada.
Jeżeli klasa przodka posiada pola i metody, które nie są prywatne, to klasa potomna również będzie je posiadać.
Klasa może dziedziczyć tylko po jednej klasie (rozszerzać tylko jedną klasę bazową )
Aby klasa dziedziczyła po innej należy użyć słówka extends i podać nazwę klasy przodka:
Przykład – Postać w grze RPG
<?php
class Postac {
private $nick = 'Gracz123';
protected $poziom = 1;
protected $hp = 100;
public $imie;
public function czyMartwy() {
return $this->hp <= 0;
}
public function otrzymajObrazenia($obrazenia) {
return $this->zadajObrazenia($obrazenia);
}
protected function zadajObrazenia($ilosc) {
$this->hp -= $ilosc;
if ($this->hp < 0) {
$this->hp = 0;
}
return $this->hp;
}
}
// Następuje dziedziczenie
class Wojownik extends Postac {
private $pancerz = 50;
public function mocnyAtak() {
return $this->atakujZSila(30);
}
private function atakujZSila($ilosc) {
if ($this->poziom > 0) { // Mamy dostęp do pola poziom, gdyż jest protected
return $this->otrzymajObrazenia($ilosc); // Metoda otrzymajObrazenia jest public
}
return $this->pancerz += $ilosc;
}
}
$bohater = new Wojownik();
$bohater->imie = 'Conan';
$bohater->otrzymajObrazenia(25);
$bohater->mocnyAtak();
if ($bohater->czyMartwy()) {
echo $bohater->imie . ' nie żyje!';
} else {
echo $bohater->imie . ' walczy dalej!';
}
echo '<pre>';
print_r($bohater);
echo '</pre>';
// $hp = $bohater->hp; // Ta linijka wygeneruje błąd,
// gdyż z zewnątrz nie mamy dostępu do pola protected
// $bohater->zadajObrazenia(10); // Ta linijka podobnie, jak wyżej
?>
Final, a nadpisywanie
Wiemy już doskonale, że zadeklarowanie w kodzie zmiennej o nazwie, która już istnieje, spowoduje nadpisanie jej wartości. Tak samo dzieje się z metodami. Jeżeli klasa dziedzicząca będzie posiadała metodę o nazwie istniejącej w klasie bazowej, to jej funkcjonalność zostanie nadpisana:
<?php
class ProfilBazowy {
public function wyswietlNazwe() {
echo "Nazywam się: Użytkownik";
}
}
class ProfilPremium extends ProfilBazowy {
public function wyswietlNazwe() {
echo "Nazywam się: Premium User";
}
}
$profil = new ProfilPremium();
$profil->wyswietlNazwe();
// Wyświetli: Nazywam się: Premium User
?>
W powyższym kodzie widzimy, że metoda wyswietlNazwe() została nadpisana i zamiast wyświetlić „Nazywam się: Użytkownik” otrzymaliśmy „Nazywam się: Premium User”.
Słowo kluczowe final
Jeżeli chcemy zabronić klasie potomnej nadpisywania metod w klasie bazowej, używamy słowa final przed deklaracją metody:
<?php
class ProfilBazowy {
final public function wyswietlNazwe() {
echo "Nazywam się: Użytkownik";
}
}
class ProfilPremium extends ProfilBazowy {
/*
public function wyswietlNazwe() {
// Ta metoda wygeneruje błąd:
// Fatal error: Cannot override final method...
}
*/
}
$profil = new ProfilPremium();
$profil->wyswietlNazwe();
?>
Modyfikatora final możemy użyć przed nazwą klasy, wówczas ta klasa nie będzie mogła być dziedziczona.
Już potrafisz:
- stworzyć klasę rozszerzającą inną klasę
- korzystać z pól i metod klasy bazowej
- nadpisywać metody klasy bazowej
- blokować nadpisywanie przy pomocy słowa final
Metody magiczne
Nazwa tych metod wzięła się stąd, że wywoływane są automatycznie w momencie tworzenia obiektu i nie wymagają dodatkowych akcji. Metody te występują tylko w kontekście klas, a ich nazwy są poprzedzone 2 znakami „podłogi” (podkreślnika) __ .
Konstruktor – __construct
Dzięki konstruktorowi możemy przesłać określone wartości do obiektu w momencie jego tworzenia.
Teoretycznie PHP umożliwia stworzenie tylko 1go konstruktora, ale po to mamy kreatywność żeby móc sobie z tym poradzić Zapraszam do zbadania tej ewentualności na stronie: https://kursphp.com/programowanie-obiektowe-php/konstruktor/
<?php
// PHP ma TYLKO JEDEN konstruktor, ale możemy symulować wiele:
class Uzytkownik {
private string $imie;
private string $email;
// Jedyny prawdziwy konstruktor
private function __construct(string $imie, string $email) {
$this->imie = $imie;
$this->email = $email;
}
// Statyczne metody fabrykujące (factory methods) - symulacja wielu konstruktorów
public static function zLoginu(string $login): self {
// Pobierz dane z bazy po loginie
return new self($login, "$login@example.com");
}
public static function zEmail(string $email): self {
// Pobierz dane z bazy po email
$login = explode('@', $email)[0];
return new self($login, $email);
}
}
// Użycie - wygląda jak różne konstruktory:
$user1 = Uzytkownik::zLoginu('jan123');
$user2 = Uzytkownik::zEmail('anna@example.com');
Destruktor – __destruct
Destruktor wykonuje się w momencie niszczenia obiektu.
Przykład użycia konstruktora i destruktora:
<?php
class GraczOnline {
private $nick;
private $poziom;
public function __construct(string $nick, int $poziom) {
$this->nick = $nick;
$this->poziom = $poziom;
echo "Gracz {$this->nick} dołączył do gry!<br>";
}
public function getNick(): string {
return $this->nick;
}
public function getPoziom(): int {
return $this->poziom;
}
public function __destruct() {
echo "Gracz {$this->nick} opuścił grę!<br>";
}
}
$gracz = new GraczOnline('ProGamer99', 45);
echo 'Nick: ' . $gracz->getNick() . ', Poziom: ' . $gracz->getPoziom() . '<br>';
unset($gracz); // Usuwanie obiektu
// Wynik: Gracz ProGamer99 opuścił grę!
?>
Inne popularne metody magiczne
// __call() - wywoływana gdy metoda nie istnieje
public function __call($name, $arguments) {
echo "Próba wywołania nieistniejącej metody: $name";
}
// __isset() - sprawdzanie czy właściwość istnieje
public function __isset($name) {
return isset($this->data[$name]);
}
// __unset() - usuwanie właściwości
public function __unset($name) {
unset($this->data[$name]);
}
// __clone() - klonowanie obiektu
public function __clone() {
echo "Obiekt został sklonowany";
}
Więcej informacji na temat metod magicznych znajdziesz w dokumentacji: http://php.net/manual/en/language.oop5.magic.php
Stałe
Stała to właściwość klasy tylko do odczytu. Jak wiemy z języka PHP, stałe deklarujemy drukowanymi literami, są dostępne public i nie można ich modyfikować w kodzie.
Aby zadeklarować stałą, używaliśmy dotychczas konstrukcji define(), możemy to też zrobić używając modyfikatora const. Pamiętać należy, że jedna i druga konstrukcja nie są sobie równe. Za pomocą const definiujemy stałe w klasach i tylko w nich.
Aby się do niej odwołać wewnątrz klasy, musimy użyć modyfikatora self::
Aby wywołać właściwość poza klasą używamy modyfikatora nazwaKlasy::
<?php
class KonfiguracjaGry {
const MAX_GRACZY = 100;
const MIN_POZIOM = 1;
const MAX_POZIOM = 100;
private $liczbaGraczy;
public function __construct($liczbaGraczy) {
$this->liczbaGraczy = $liczbaGraczy;
}
public function mozeDolczyc() {
return $this->liczbaGraczy < self::MAX_GRACZY;
}
}
// KonfiguracjaGry::MAX_GRACZY = 200; // Nie można modyfikować
$gra = new KonfiguracjaGry(85);
echo 'Czy można dołączyć: ' . ($gra->mozeDolczyc() ? 'TAK' : 'NIE') . '<br>';
echo 'Maksymalna liczba graczy: ' . KonfiguracjaGry::MAX_GRACZY . '<br>';
echo 'Maksymalny poziom: ' . KonfiguracjaGry::MAX_POZIOM . '<br>';
?>
Metody i właściwości statyczne
Metody i właściwości statyczne to takie, które nie wymagają utworzenia obiektu, aby móc się do nich odwołać. Nadawane są dla całej klasy. Tworzymy je przy pomocy modyfikatora static.
Z racji tego, że właściwość ta nadawana jest dla klasy (nie dla obiektu), aby się do niej odwołać wewnątrz klasy, musimy użyć modyfikatora self::, nie korzystamy w tym przypadku z $this. Aby wywołać właściwość poza klasą używamy modyfikatora nazwaKlasy::
<?php
class LicznikGraczy {
public static $iloscGraczy = 0;
public function __construct() {
self::$iloscGraczy++;
}
}
echo 'Graczy online: ' . LicznikGraczy::$iloscGraczy . '<br>'; // Wartość początkowa: 0
$gracz1 = new LicznikGraczy();
$gracz2 = new LicznikGraczy();
$gracz3 = new LicznikGraczy();
echo 'Graczy online: ' . LicznikGraczy::$iloscGraczy . '<br>'; // Wartość po dodaniu: 3
?>
Klasy i metody abstrakcyjne
Klasa abstrakcyjna definiuje pewien model i zachowanie obiektu.
Taka klasa nie posiada definicji tych metod, jedynie ich deklaracje, że gdzieś dalej w kodzie muszą się pojawić. Czyli do czasu rozszerzenia ich o funkcjonalności w klasach dziedziczących, te metody nie istnieją (są abstrakcją).
Metody abstrakcyjne definiujemy w klasie bazowej klauzulą abstract. Wszystkie tego typu zadeklarowane metody będą musiały pojawić się w klasach dziedziczących. Jeśli nie pojawią się, to PHP wygeneruje nam błąd.
Nie można utworzyć obiektu klasy abstrakcyjnej. Klasa ta jest jedynie „bazą” dla klas potomnych.
Pamiętamy, że klasę dziedziczymy i możemy zrobić to tylko raz.
<?php
abstract class PostacGry {
abstract public function getNazwa();
abstract public function atak();
}
class Mag extends PostacGry {
public function getNazwa() {
return 'Mag';
}
public function atak() {
return 'Rzuca zaklęcie!';
}
}
class Wojownik extends PostacGry {
public function getNazwa() {
return 'Wojownik';
}
public function atak() {
return 'Uderza mieczem!';
}
}
$mag = new Mag();
echo $mag->getNazwa() . ': ' . $mag->atak() . '<br>';
$wojownik = new Wojownik();
echo $wojownik->getNazwa() . ': ' . $wojownik->atak() . '<br>';
?>
Interfejsy
Interfejs jest zbiorem metod jakie wymagamy by posiadała klasa, która go implementuje.
Możemy implementować wiele interfejsów dla jednej klasy.
Wszystkie metody określane w interfejsie są abstrakcyjne.
<?php
interface PostacInterface {
public function getNazwa(): string;
public function getPoziom(): int;
}
interface WalkaInterface {
public function atak(): string;
public function obrona(): string;
}
class Bohater implements PostacInterface, WalkaInterface {
private $nazwa;
private $poziom;
public function __construct(string $nazwa, int $poziom) {
$this->nazwa = $nazwa;
$this->poziom = $poziom;
}
public function getNazwa(): string {
return $this->nazwa;
}
public function getPoziom(): int {
return $this->poziom;
}
public function atak(): string {
return "{$this->nazwa} atakuje!";
}
public function obrona(): string {
return "{$this->nazwa} broni się!";
}
}
$bohater = new Bohater('Aragorn', 50);
echo $bohater->getNazwa() . '<br>';
echo 'Poziom: ' . $bohater->getPoziom() . '<br>';
echo $bohater->atak() . '<br>';
echo $bohater->obrona() . '<br>';
?>
Różnice między interfejsem, a klasą abstrakcyjną
- klasa może implementować wiele interfejsów, ale rozszerzać tylko jednego rodzica
- metody w interfejsie są publiczne, w klasie publiczne lub chronione
- interfejs może zawierać tylko deklaracje metod, a klasa też metody zdefiniowane
- klasy abstrakcyjne mogą zawierać atrybuty(pola), interfejsy nie
- w interfejsach wszystkie metody są abstrakcyjne, natomiast w klasie abstrakcyjnej można stworzyć metody posiadające ciało, jak i abstrakcyjne
- klasa abstrakcyjna zazwyczaj jest ściśle związana z klasami dziedziczącymi w sensie logicznym, czyli np. tworzymy klasę abstrakcyjną Planeta po której dziedziczą konkretne klasy planet (np. Ziemia, Mars). Interfejs natomiast nie musi być już tak mocno związany z daną klasą, on określa jej cechy, np możesz stworzyć interfejs Zniszczalny, który mówi że dany obiekt może zostać zniszczony. Taki interfejs możesz nadać zarówno klasą Planeta, Gwiazda, Budynek itp.
- interfejs może dziedziczyć jedynie po innych interfejsach, a klasa abstrakcyjna może dziedziczyć po klasach abstrakcyjnych, interfejsach a nawet zwykłych klasach
- interfejs nie może mieć konstruktora, w klasie abstrakcyjnej możemy dostarczyć implementację konstruktora domyślnego
<?php
// PHP 8.0+ - interfejsy mogą mieć metody z implementacją (rzadko używane)
interface Logger {
public function log(string $message): void;
// PHP 8.0+ - metoda z implementacją w interfejsie
public function logError(string $message): void {
$this->log("ERROR: " . $message);
}
}
Przestrzenie nazw
Przestrzenie nazw pozwalają na zadeklarowanie wielu klas lub funkcji o tej samej nazwie.
W programowaniu jak w życiu, tak samo jak nie mogą w jednym folderze istnieć 2 pliki o tej samej nazwie i rozszerzeniu, tak samo w kodzie PHP dwie tak samo nazwane funkcje lub klasy generują błąd.
Przestrzenie nazw PHP umożliwiają grupowanie powiązanych klas, interfejsów, funkcji i stałych.
Zalecane jest tworzenie jednej przestrzeni nazw w jednym pliku.
<?php
// Przestrzenie nazw pozwalają na organizację kodu i unikanie konfliktów nazw
// Przykład problemu BEZ przestrzeni nazw:
// Jeśli masz dwie biblioteki, które mają klasę o nazwie User, będzie konflikt:
// biblioteka1.php
class User { }
// biblioteka2.php
class User { } // BŁĄD! Klasa User już istnieje
// --------------------------------------------------
// Rozwiązanie Z przestrzeniami nazw:
// biblioteka1.php
namespace Biblioteka1;
class User { }
// biblioteka2.php
namespace Biblioteka2;
class User { }
// Teraz możesz używać obu:
use Biblioteka1\User as User1;
use Biblioteka2\User as User2;
$user1 = new User1();
$user2 = new User2();
Przestrzenie nazw działają jak foldery w systemie plików
Zalecane jest: jedna przestrzeń nazw = jeden plik
Konwencja: odpowiadają strukturze folderów (PSR-4)
PHP 8 – nowości
1. Typed Properties (właściwości typowane)
PHP 8 pozwala na określenie typu dla właściwości klasy:
<?php
class GraczOnline {
public string $nick;
public int $poziom;
public float $hp;
public bool $premium;
public function __construct(string $nick, int $poziom) {
$this->nick = $nick;
$this->poziom = $poziom;
$this->hp = 100.0;
$this->premium = false;
}
}
$gracz = new GraczOnline('ProGamer', 45);
// $gracz->poziom = 'tekst'; // Błąd: Typ musi być int
?>
2. Constructor Property Promotion
PHP 8 pozwala na zwięzłe deklarowanie właściwości bezpośrednio w konstruktorze:
<?php
// PHP 7 - długie
class GraczStary {
private string $nick;
private int $poziom;
public function __construct(string $nick, int $poziom) {
$this->nick = $nick;
$this->poziom = $poziom;
}
}
// PHP 8 - zwięzłe
class GraczNowy {
public function __construct(
private string $nick,
private int $poziom,
private float $hp = 100.0
) {}
}
$gracz = new GraczNowy('ProGamer', 45);
?>
3. Named Arguments
PHP 8 pozwala na przekazywanie argumentów po nazwie:
<?php
class PostacGry {
public function __construct(
private string $imie,
private int $poziom = 1,
private int $hp = 100,
private int $mana = 50
) {}
}
// PHP 7 - musimy podać wszystkie
$postac1 = new PostacGry('Gandalf', 1, 100, 200);
// PHP 8 - możemy pominąć środkowe
$postac2 = new PostacGry(
imie: 'Gandalf',
mana: 200,
poziom: 50
);
?>
4. Match Expression
PHP 8 wprowadza match jako lepszą alternatywę dla switch:
<?php
class SystemRang {
public function getRanga(int $punkty): string {
// PHP 7 - switch
switch (true) {
case $punkty < 1000:
return 'Brązowy';
case $punkty < 2000:
return 'Srebrny';
case $punkty < 3000:
return 'Złoty';
default:
return 'Diamentowy';
}
// PHP 8 - match (zwięzłe i bezpieczniejsze)
return match (true) {
$punkty < 1000 => 'Brązowy',
$punkty < 2000 => 'Srebrny',
$punkty < 3000 => 'Złoty',
default => 'Diamentowy'
};
}
}
?>
5. Nullsafe Operator
PHP 8 wprowadza operator ?-> który zapobiega błędom przy null:
<?php
class Ekwipunek {
public ?Bron $bron = null;
}
class Bron {
public string $nazwa;
}
$gracz = new stdClass();
$gracz->ekwipunek = new Ekwipunek();
// PHP 7 - długie sprawdzanie
$nazwaBroni = null;
if ($gracz->ekwipunek !== null && $gracz->ekwipunek->bron !== null) {
$nazwaBroni = $gracz->ekwipunek->bron->nazwa;
}
// PHP 8 - nullsafe operator
$nazwaBroni = $gracz->ekwipunek?->bron?->nazwa;
?>
6. Union Types
PHP 8 pozwala na określenie wielu typów dla jednego parametru:
<?php
class Gracz {
// Może być int lub string
public function setId(int|string $id): void {
// ...
}
// Może zwrócić PostacGry lub null
public function znajdzPostac(string $nazwa): PostacGry|null {
// ...
}
}
?>
Przykłady
Przykład 1: System Streamingu na Twitchu – przykład z obszernym komentarzem w kodzie
<?php
/*
* ==========================================
* KLASA STREAMER - System Streamingu Twitch
* ==========================================
*
* Ta klasa reprezentuje streamera na platformie Twitch.
* Każdy streamer ma:
* - pseudonim (nick)
* - liczbę subskrybcji
* - liczbę followerów
* - status online/offline
*/
class Streamer {
/*
* KONSTRUKTOR - metoda automatycznie wywoływana przy tworzeniu obiektu
*
* PHP 8 - Constructor Property Promotion:
* Parametry konstruktora automatycznie stają się właściwościami klasy.
* To skrót, który oszczędza pisanie kodu.
*
* Tradycyjny sposób (PHP 7):
* -------------------------
* private string $nick;
* public function __construct(string $nick) {
* $this->nick = $nick;
* }
*
* Nowy sposób (PHP 8):
* --------------------
* public function __construct(private string $nick) {}
*
* Oba robią to samo, ale PHP 8 jest krócej!
*/
public function __construct(
private string $nick, // Pseudonim streamera (WYMAGANE - brak wartości domyślnej)
private int $subskrybcje = 0, // Liczba subskrybcji (domyślnie 0)
private int $followers = 0, // Liczba followerów (domyślnie 0)
private bool $czyOnline = false // Status online (domyślnie false = offline)
) {
/*
* Ciało konstruktora jest PUSTE {}, ponieważ PHP 8 automatycznie:
* 1. Tworzy prywatne właściwości: $nick, $subskrybcje, $followers, $czyOnline
* 2. Przypisuje im wartości przekazane przy tworzeniu obiektu
*
* Gdybyśmy chcieli coś dodatkowego zrobić przy tworzeniu obiektu,
* moglibyśmy napisać tu dodatkowy kod, np:
* echo "Utworzono konto dla {$this->nick}!";
*/
}
/*
* METODA: rozpocznijStream()
* --------------------------
* Uruchamia stream - zmienia status streamera na "online"
*
* public - metoda jest dostępna z zewnątrz klasy
* void - metoda nie zwraca żadnej wartości (tylko wykonuje akcję)
*/
public function rozpocznijStream(): void {
// Zmieniamy właściwość $czyOnline na true
// $this-> oznacza "tego obiektu" (tego konkretnego streamera)
$this->czyOnline = true;
// Wyświetlamy komunikat
// {$this->nick} wstawia wartość właściwości $nick do tekstu
echo "{$this->nick} rozpoczął stream!<br>";
}
/*
* METODA: zakonczStream()
* -----------------------
* Kończy stream - zmienia status streamera na "offline"
*/
public function zakonczStream(): void {
// Ustawiamy status na offline
$this->czyOnline = false;
// Wyświetlamy komunikat
echo "{$this->nick} zakończył stream!<br>";
}
/*
* METODA: dodajSuba()
* -------------------
* Dodaje jedną subskrybcję do konta streamera
*
* Dlaczego metoda zamiast bezpośredniego dostępu?
* ------------------------------------------------
* Właściwość $subskrybcje jest PRIVATE, więc nie można jej zmienić z zewnątrz:
*
* ŹLE (nie zadziała):
* $streamer->subskrybcje = 999; // BŁĄD! Właściwość jest private
*
* DOBRZE (zadziała):
* $streamer->dodajSuba(); // OK! Używamy publicznej metody
*
* Zalety tego podejścia:
* - Kontrola: możemy dodać walidację (np. sprawdzić czy sub jest ważny)
* - Bezpieczeństwo: nikt nie ustawi liczby subów na -100
* - Logika biznesowa: możemy dodać dodatkowe akcje (np. powiadomienia)
*/
public function dodajSuba(): void {
// Operator ++ zwiększa wartość o 1
// Równoznaczne z: $this->subskrybcje = $this->subskrybcje + 1;
$this->subskrybcje++;
// Wyświetlamy komunikat z aktualną liczbą subów
echo "Nowy sub! Razem: {$this->subskrybcje}<br>";
/*
* MIEJSCE NA ROZBUDOWĘ:
* Tutaj moglibyśmy dodać dodatkową logikę, np:
*
* if ($this->subskrybcje % 100 == 0) {
* echo "MILESTONE! {$this->subskrybcje} subów!<br>";
* }
*
* Albo:
* if ($this->subskrybcje == 1000) {
* echo "Gratulacje! 1000 subskrypcji!<br>";
* }
*/
}
/*
* METODA: wyswietlStatus()
* ------------------------
* Wyświetla pełny status streamera:
* - pseudonim
* - czy jest online/offline
* - liczba subskrybcji
* - liczba followerów
*/
public function wyswietlStatus(): void {
/*
* OPERATOR TRÓJARGUMENTOWY (ternary operator):
* -------------------------------------------
* Składnia: warunek ? wartość_jeśli_prawda : wartość_jeśli_fałsz
*
* Długa wersja (identyczne działanie):
* if ($this->czyOnline == true) {
* $status = 'LIVE';
* } else {
* $status = 'OFFLINE';
* }
*
* Krótka wersja:
* $status = $this->czyOnline ? 'LIVE' : 'OFFLINE';
*/
$status = $this->czyOnline ? 'LIVE' : 'OFFLINE';
// Wyświetlamy nagłówek z pseudonimem i statusem
echo "--- {$this->nick} [{$status}] ---<br>";
// Wyświetlamy szczegóły
echo "Subskrybcje: {$this->subskrybcje}<br>";
echo "Followersi: {$this->followers}<br>";
}
}
/*
* ==========================================
* UŻYCIE KLASY - Tworzenie obiektu i praca z nim
* ==========================================
*/
/*
* TWORZENIE OBIEKTU (instancji klasy):
* ------------------------------------
* Słowo kluczowe 'new' tworzy nowy obiekt na podstawie klasy Streamer
*
* new Streamer(...) wywołuje konstruktor i przekazuje do niego argumenty:
* - 'GamingLegend' → przypisane do $nick
* - 150 → przypisane do $subskrybcje
* - 5000 → przypisane do $followers
* - $czyOnline nie podane, więc dostaje wartość domyślną: false
*
* Obiekt zapisujemy w zmiennej $streamer
*/
$streamer = new Streamer('GamingLegend', 150, 5000);
/*
* Co się stało w pamięci?
* -----------------------
* Utworzono obiekt $streamer z następującymi wartościami:
*
* $streamer:
* ├─ $nick = 'GamingLegend'
* ├─ $subskrybcje = 150
* ├─ $followers = 5000
* └─ $czyOnline = false
*/
// WYWOŁANIE METODY: wyswietlStatus()
// ----------------------------------
// Operator -> służy do wywoływania metod i dostępu do właściwości obiektu
// Wywołujemy metodę wyswietlStatus() na obiekcie $streamer
$streamer->wyswietlStatus();
/*
* Wyświetli:
* --- GamingLegend [OFFLINE] ---
* Subskrybcje: 150
* Followersi: 5000
*/
echo "<br>"; // Odstęp dla czytelności
// WYWOŁANIE METODY: rozpocznijStream()
// ------------------------------------
// Zmieniamy status streamera na "online"
$streamer->rozpocznijStream();
/*
* Dzieje się:
* 1. $czyOnline zmienia się na true
* 2. Wyświetla się: GamingLegend rozpoczął stream!
*/
// WYWOŁANIE METODY: dodajSuba() - pierwsze wywołanie
// --------------------------------------------------
// Dodajemy pierwszą subskrybcję
$streamer->dodajSuba();
/*
* Dzieje się:
* 1. $subskrybcje zwiększa się z 150 do 151
* 2. Wyświetla się: Nowy sub! Razem: 151
*/
// WYWOŁANIE METODY: dodajSuba() - drugie wywołanie
// ------------------------------------------------
// Dodajemy kolejną subskrybcję
$streamer->dodajSuba();
/*
* Dzieje się:
* 1. $subskrybcje zwiększa się z 151 do 152
* 2. Wyświetla się: Nowy sub! Razem: 152
*/
// WYWOŁANIE METODY: zakonczStream()
// ---------------------------------
// Kończymy stream
$streamer->zakonczStream();
/*
* Dzieje się:
* 1. $czyOnline zmienia się na false
* 2. Wyświetla się: GamingLegend zakończył stream!
*/
echo "<br>"; // Odstęp dla czytelności
// WYWOŁANIE METODY: wyswietlStatus() - ponownie
// ---------------------------------------------
// Wyświetlamy status po zakończeniu streamu
$streamer->wyswietlStatus();
/*
* Wyświetli:
* --- GamingLegend [OFFLINE] ---
* Subskrybcje: 152 <-- Zauważ: zwiększyło się z 150 do 152
* Followersi: 5000
*
* Status zmienił się z LIVE na OFFLINE
*/
/*
* ==========================================
* PEŁNY OUTPUT PROGRAMU:
* ==========================================
*
* --- GamingLegend [OFFLINE] ---
* Subskrybcje: 150
* Followersi: 5000
*
* GamingLegend rozpoczął stream!
* Nowy sub! Razem: 151
* Nowy sub! Razem: 152
* GamingLegend zakończył stream!
*
* --- GamingLegend [OFFLINE] ---
* Subskrybcje: 152
* Followersi: 5000
*/
/*
* ==========================================
* DODATKOWE PRZYKŁADY - Tworzenie wielu streamerów
* ==========================================
*/
echo "<hr><h3>Dodatkowe przykłady:</h3>";
// Przykład 1: Nowy streamer z minimalnymi danymi
// ----------------------------------------------
// Podajemy tylko nick (reszta dostanie wartości domyślne)
$nowyStreamer = new Streamer('RookiePlayer');
/*
* Wartości:
* - $nick = 'RookiePlayer'
* - $subskrybcje = 0 (domyślna)
* - $followers = 0 (domyślna)
* - $czyOnline = false (domyślna)
*/
$nowyStreamer->wyswietlStatus();
/*
* Wyświetli:
* --- RookiePlayer [OFFLINE] ---
* Subskrybcje: 0
* Followersi: 0
*/
echo "<br>";
// Przykład 2: Wielki streamer
// ---------------------------
$megaStreamer = new Streamer('xQcOW', 85000, 1500000);
$megaStreamer->rozpocznijStream();
$megaStreamer->wyswietlStatus();
/*
* Wyświetli:
* xQcOW rozpoczął stream!
* --- xQcOW [LIVE] ---
* Subskrybcje: 85000
* Followersi: 1500000
*/
echo "<br>";
// Przykład 3: Porównanie dwóch streamerów
// ----------------------------------------
echo "<h4>Porównanie streamerów:</h4>";
$streamer1 = new Streamer('Ninja', 75000, 180000);
$streamer2 = new Streamer('Shroud', 50000, 100000);
// Obaj zaczynają stream
$streamer1->rozpocznijStream();
$streamer2->rozpocznijStream();
echo "<br>";
// Wyświetlamy ich statusy
echo "Status Ninja:<br>";
$streamer1->wyswietlStatus();
echo "<br>";
echo "Status Shroud:<br>";
$streamer2->wyswietlStatus();
/*
* WAŻNA OBSERWACJA:
* -----------------
* Każdy obiekt ($streamer1, $streamer2) ma WŁASNE wartości właściwości.
* Zmiana wartości w jednym obiekcie NIE WPŁYWA na drugi obiekt.
*
* To tak jakby mieć dwa oddzielne konta na Twitchu - każde jest niezależne.
*/
/*
* ==========================================
* KLUCZOWE POJĘCIA - PODSUMOWANIE:
* ==========================================
*
* 1. KLASA (class):
* - Szablon/przepis na tworzenie obiektów
* - Definiuje jakie właściwości i metody będą miały obiekty
*
* 2. OBIEKT (object/instancja):
* - Konkretny "egzemplarz" utworzony na podstawie klasy
* - Tworzy się słowem kluczowym 'new'
*
* 3. WŁAŚCIWOŚCI (properties):
* - Zmienne należące do obiektu
* - W naszym przykładzie: $nick, $subskrybcje, $followers, $czyOnline
*
* 4. METODY (methods):
* - Funkcje należące do klasy
* - Operują na właściwościach obiektu
* - W naszym przykładzie: rozpocznijStream(), dodajSuba(), wyswietlStatus()
*
* 5. $this:
* - Odnosi się do "bieżącego obiektu"
* - Używamy go w metodach aby dostać się do właściwości obiektu
*
* 6. MODYFIKATORY DOSTĘPU:
* - private: dostęp tylko wewnątrz klasy
* - public: dostęp z każdego miejsca
*
* 7. KONSTRUKTOR (__construct):
* - Specjalna metoda wywoływana automatycznie przy tworzeniu obiektu
* - Służy do inicjalizacji (ustawienia początkowych wartości)
*
* 8. OPERATOR -> :
* - Służy do dostępu do właściwości i metod obiektu
* - Składnia: $obiekt->metoda() lub $obiekt->właściwość
*/
?>
Przykład z wizualizacją stanu obiektów
<?php
// Wizualizacja jak zmieniają się dane w obiekcie
echo "<h3>Śledzenie zmian w obiekcie krok po kroku:</h3>";
// Krok 1: Tworzenie obiektu
echo "<strong>KROK 1: Tworzenie obiektu</strong><br>";
$streamer = new Streamer('TestStreamer', 10, 100);
echo "Utworzono streamera. Stan obiektu:<br>";
$streamer->wyswietlStatus();
/*
* Stan obiektu:
* $nick = 'TestStreamer'
* $subskrybcje = 10
* $followers = 100
* $czyOnline = false
*/
echo "<br><hr>";
// Krok 2: Rozpoczęcie streamu
echo "<strong>KROK 2: Rozpoczęcie streamu</strong><br>";
$streamer->rozpocznijStream();
echo "Stan obiektu po rozpoczęciu:<br>";
$streamer->wyswietlStatus();
/*
* Stan obiektu:
* $nick = 'TestStreamer'
* $subskrybcje = 10
* $followers = 100
* $czyOnline = true <-- ZMIANA!
*/
echo "<br><hr>";
// Krok 3: Dodanie 3 subskrybcji
echo "<strong>KROK 3: Dodanie 3 subskrybcji</strong><br>";
$streamer->dodajSuba();
$streamer->dodajSuba();
$streamer->dodajSuba();
echo "Stan obiektu po dodaniu subów:<br>";
$streamer->wyswietlStatus();
/*
* Stan obiektu:
* $nick = 'TestStreamer'
* $subskrybcje = 13 <-- ZMIANA! (było 10, teraz 13)
* $followers = 100
* $czyOnline = true
*/
echo "<br><hr>";
// Krok 4: Zakończenie streamu
echo "<strong>KROK 4: Zakończenie streamu</strong><br>";
$streamer->zakonczStream();
echo "Stan obiektu po zakończeniu:<br>";
$streamer->wyswietlStatus();
/*
* Stan obiektu:
* $nick = 'TestStreamer'
* $subskrybcje = 13
* $followers = 100
* $czyOnline = false <-- ZMIANA!
*/
echo "<br><hr>";
// Podsumowanie
echo "<strong>PODSUMOWANIE ZMIAN:</strong><br>";
echo "Początek: 10 subskrybcji, OFFLINE<br>";
echo "Koniec: 13 subskrybcji, OFFLINE<br>";
echo "Różnica: +3 subskrybcje<br>";
?>
Przykład 2: OOP obsługujące fabrykę samochodów
<?php
// Klasa bazowa dla fabryki Chevrolet
class ChevroletFactory {
// Stała reprezentująca maksymalną pojemność produkcji
const MAX_CAPACITY = 500;
// Właściwości klasy
protected $brand = "Chevrolet";
protected $producedCars = 0;
// Konstruktor inicjalizuje fabrykę
public function __construct() {
echo "Fabryka marki {$this->brand} została otwarta.<br>";
}
// Metoda do produkcji samochodów
public function produceCar($quantity) {
if ($this->producedCars + $quantity > self::MAX_CAPACITY) {
echo "Nie można wyprodukować $quantity samochodów. Przekroczono maksymalną pojemność produkcyjną.<br>";
} else {
$this->producedCars += $quantity;
echo "Wyprodukowano $quantity samochodów. Łączna produkcja: {$this->producedCars}.<br>";
}
}
// Metoda do wyświetlenia statusu produkcji
public function factoryStatus() {
echo "Fabryka marki {$this->brand} wyprodukowała {$this->producedCars} samochodów.<br>";
}
// Magiczna metoda __get. Obsługuje odczyt nieistniejących właściwości.
public function __get($name) {
echo "Próba dostępu do właściwości '{$name}', która nie istnieje.<br>";
return null;
}
// Magiczna metoda __set. Obsługuje próbę ustawienia niedozwolonych właściwości.
public function __set($name, $value) {
echo "Próba ustawienia właściwości '{$name}' na wartość '{$value}'. Operacja niedozwolona!<br>";
}
// Magiczna metoda __toString, która pozwoli na łatwą reprezentację obiektu jako tekst
public function __toString() {
return "Fabryka marki {$this->brand}: wyprodukowano {$this->producedCars} samochodów. Maksymalna pojemność: " . self::MAX_CAPACITY . ".<br>";
}
// Destruktor wyświetla komunikat o zamknięciu fabryki i wyprodukowanej liczbie samochodów.
// Destruktor - wywoływany automatycznie gdy:
// 1. Kończy się skrypt
// 2. Obiekt jest usuwany przez unset()
// 3. Obiekt jest nadpisywany przez nowy
public function __destruct() {
echo "Fabryka marki {$this->brand} została zamknięta. Łączna produkcja: {$this->producedCars} samochodów.<br>";
}
}
// Klasa pochodna dla modelu Camaro
class Camaro extends ChevroletFactory {
public $modelName = "Camaro";
private $engineType;
// Konstruktor klasy pochodnej
public function __construct($engineType) {
parent::__construct(); // Wywołanie konstruktora klasy bazowej
$this->engineType = $engineType;
echo "Rozpoczęto produkcję modelu {$this->modelName} z silnikiem {$this->engineType}.<br>";
}
}
// Klasa pochodna dla modelu Corvette
class Corvette extends ChevroletFactory {
public $modelName = "Corvette";
private $color;
// Konstruktor klasy pochodnej
public function __construct($color) {
parent::__construct(); // Wywołanie konstruktora klasy bazowej
$this->color = $color;
echo "Rozpoczęto produkcję modelu {$this->modelName} w kolorze {$this->color}.<br>";
}
}
// Przykład użycia
echo "==== Fabryka Chevrolet ====<br>";
// Tworzenie fabryki dla Camaro
$camaroFactory = new Camaro("V8");
$camaroFactory->produceCar(100); // Produkcja 100 samochodów
$camaroFactory->produceCar(450); // Przekroczenie pojemności
echo $camaroFactory; // Wywołanie __toString
echo $camaroFactory->nonExistentProperty; // Magiczne __get
$camaroFactory->nonExistentProperty = "Test"; // Magiczne __set
$camaroFactory->factoryStatus(); // Status fabryki
echo "<br>==== Fabryka Corvette ====<br>";
// Tworzenie fabryki dla Corvette
$corvetteFactory = new Corvette("Czerwony");
$corvetteFactory->produceCar(200); // Produkcja 200 samochodów
echo $corvetteFactory; // Wywołanie __toString
$corvetteFactory->factoryStatus(); // Status fabryki
// Destruktory wywołają się automatycznie po zakończeniu skryptu
?>
Klasa ChevroletFactory jest klasą bazową, która reprezentuje fabrykę samochodów marki Chevrolet. W tej klasie definiujemy:
-
Stałą
MAX_CAPACITY:
StałaMAX_CAPACITYokreśla maksymalną liczbę samochodów, które mogą być wyprodukowane przez fabrykę. Wartość ta jest stała i niezmienna, wynosi 500 w naszym przypadku. - Właściwości
$brandi$producedCars:$brandto nazwa marki fabryki, w tym przypadku „Chevrolet”.$producedCarsto liczba wyprodukowanych samochodów przez fabrykę, początkowo ustawiona na 0.
- Konstruktor: Konstruktor jest wywoływany, gdy tworzony jest nowy obiekt klasy
ChevroletFactory. W momencie tworzenia fabryki, wyświetlany jest komunikat o jej otwarciu. -
Metoda
produceCar($quantity): Ta metoda służy do produkcji określonej liczby samochodów. Jeśli liczba samochodów przekroczyłabyMAX_CAPACITY, metoda wyświetla komunikat o błędzie. W przeciwnym razie dodaje liczbę wyprodukowanych samochodów do właściwości$producedCars. -
Metoda
__toString(): Jest to metoda magiczna, która pozwala na „przekształcenie” obiektu na ciąg tekstowy. Gdy próbujemy wydrukować obiekt, np.echo $obiekt, PHP wywołuje__toString()i wyświetla zwrócony tekst. W naszym przypadku metoda ta zwraca szczegóły dotyczące fabryki, czyli liczbę wyprodukowanych samochodów i maksymalną pojemność. -
Destruktor: Destruktor jest wywoływany, gdy obiekt jest usuwany z pamięci, co zwykle następuje, gdy skrypt kończy działanie. W naszym przypadku, destruktor wyświetla komunikat o zamknięciu fabryki i liczbie wyprodukowanych samochodów.
-
Magic Methods
__geti__set:__get($name): Została dodana po to, aby obsługiwać przypadki, gdy próbujemy uzyskać dostęp do właściwości, która nie istnieje. W takim przypadku wyświetlamy komunikat informujący o próbie dostępu do nieistniejącej właściwości.__set($name, $value): Została dodana, aby obsługiwać próbę ustawienia wartości właściwości, która nie istnieje, lub która nie jest dozwolona do ustawiania.
Przykładowo, jeśli spróbujemy przypisać wartość do nieistniejącej właściwości, metoda
__setzostanie wywołana i wyświetli komunikat.
W naszym przykładzie mamy dwie klasy pochodne:
-
Camaro: Reprezentuje model samochodu Camaro. Klasa ta dziedziczy po klasieChevroletFactoryi dodaje specyficzną właściwość$engineType(typ silnika).Konstruktor klasy
Camarowywołuje konstruktor klasyChevroletFactoryza pomocąparent::__construct(), a następnie ustawia właściwość$engineTypei wyświetla komunikat o rozpoczęciu produkcji. -
Corvette: Reprezentuje model samochodu Corvette. Podobnie jak w przypadkuCamaro, dziedziczy po klasieChevroletFactory, ale dodaje właściwość$color(kolor samochodu).Konstruktor klasy
Corvetterównież wywołuje konstruktor klasyChevroletFactory, ustawia kolor i wyświetla komunikat o rozpoczęciu produkcji.
W sekcji przykładu użycia tworzymy obiekty klasy Camaro i Corvette i wywołujemy na nich różne metody:
-
Tworzymy fabrykę Camaro:
- Tworzymy obiekt
Camaro, który przyjmuje typ silnika jako argument w konstruktorze. - Wywołujemy metodę
produceCar(), aby wyprodukować samochody, a potem wyświetlamy status fabryki przy pomocyechoi metody__toString().
- Tworzymy obiekt
-
Tworzymy fabrykę Corvette:
- Tworzymy obiekt
Corvette, który przyjmuje kolor jako argument w konstruktorze. - Wywołujemy metodę
produceCar()i ponownie wyświetlamy status fabryki przy pomocyechoi metody__toString().
- Tworzymy obiekt
Podsumowanie:
- Klasa
ChevroletFactoryreprezentuje fabrykę samochodów i zawiera metody do produkcji, statusu fabryki oraz metody magiczne do zarządzania właściwościami obiektów. - Klasy pochodne (
CamaroiCorvette) reprezentują konkretne modele samochodów i dziedziczą właściwości i metody klasy bazowej, dodając własne specyficzne właściwości (np. typ silnika, kolor). - Metoda
__toString()umożliwia łatwe reprezentowanie obiektu w formie tekstowej.
Każdy obiekt fabryki może produkować samochody, wyświetlać stan fabryki oraz korzystać z funkcji związanych z magią PHP (__get, __set).
Przykład 3: Postaci w grze RPG
Poniżej przedstawiam przykład wykorzystania OOP do tworzenia postaci w grze i symulowania ich walki.
<?php
// Klasa główna Postac. To podstawowa klasa, z której dziedziczą inne postacie. Ma podstawowe cechy: imię, poziom i zdrowie.
class Postac {
// Własności
protected $imie;
protected $poziom;
protected $zdrowie;
// Konstruktor
public function __construct($imie, $poziom = 1) {
$this->imie = $imie;
$this->poziom = $poziom;
$this->zdrowie = 100;
}
// Metoda, która zwiększa poziom
public function awans() {
$this->poziom++;
$this->zdrowie += 20;
echo "{$this->imie} awansował na poziom {$this->poziom}! Zdrowie wynosi teraz {$this->zdrowie}.\n";
}
// Metoda magiczna __toString
public function __toString() {
return "Postać: $this->imie, Poziom: $this->poziom, Zdrowie: $this->zdrowie.";
}
}
// Klasa Wojownik, dziedziczy po klasie Postac
class Wojownik extends Postac {
private $sila;
// Konstruktor
public function __construct($imie, $poziom = 1) {
parent::__construct($imie, $poziom);
$this->sila = 10 + $poziom * 2;
}
// Atak
public function atak() {
echo "{$this->imie} wykonuje potężny atak mieczem, zadając {$this->sila} obrażeń!\n";
}
// Nadpisanie metody awans
public function awans() {
parent::awans();
$this->sila += 5;
echo "{$this->imie} zwiększył siłę do {$this->sila}.\n";
}
}
// Klasa Mag, dziedziczy po klasie Postac
class Mag extends Postac {
private $mana;
// Konstruktor
public function __construct($imie, $poziom = 1) {
parent::__construct($imie, $poziom);
$this->mana = 50 + $poziom * 10;
}
// Rzucenie zaklęcia
public function rzucZaklecie() {
if ($this->mana >= 10) {
$this->mana -= 10;
echo "{$this->imie} rzuca zaklęcie, zadając 30 obrażeń! Pozostała mana: {$this->mana}.\n";
} else {
echo "{$this->imie} nie ma wystarczającej ilości many!\n";
}
}
// Nadpisanie metody awans
public function awans() {
parent::awans();
$this->mana += 20;
echo "{$this->imie} zwiększył manę do {$this->mana}.\n";
}
}
// Tworzenie postaci
$wojownik = new Wojownik("Thor");
$mag = new Mag("Gandalf");
// Wypisanie informacji o postaciach
echo $wojownik . "\n"; // Wyświetla: Postać: Thor, Poziom: 1, Zdrowie: 100.
echo $mag . "\n"; // Wyświetla: Postać: Gandalf, Poziom: 1, Zdrowie: 100.
// Akcje postaci
$wojownik->atak(); // Thor wykonuje potężny atak mieczem, zadając 12 obrażeń!
$mag->rzucZaklecie(); // Gandalf rzuca zaklęcie, zadając 30 obrażeń! Pozostała mana: 50.
// Awansowanie postaci
$wojownik->awans(); // Thor awansował na poziom 2! Zdrowie wynosi teraz 120. Zwiększył siłę do 17.
$mag->awans(); // Gandalf awansował na poziom 2! Zdrowie wynosi teraz 120. Zwiększył manę do 80.
echo $wojownik . "\n"; // Wyświetla: Postać: Thor, Poziom: 2, Zdrowie: 120.
echo $mag . "\n"; // Wyświetla: Postać: Gandalf, Poziom: 2, Zdrowie: 120.
Wyjaśnienie kodu:
Dziedziczenie: Klasy Wojownik i Mag dziedziczą po Postac i dodają swoje unikalne właściwości (sila, mana) oraz metody (atak, rzucZaklecie).
Konstruktor: Każda klasa ma swój konstruktor, który inicjalizuje własności. Wojownik i Mag wywołują konstruktor z klasy nadrzędnej za pomocą parent::__construct().
Metoda magiczna __toString: Pozwala reprezentować obiekt jako ciąg znaków, np. przy wywołaniu echo $obiekt.
Polimorfizm: Metoda awans() jest nadpisywana w klasach Wojownik i Mag, aby uwzględnić specyficzne zmiany.
Zadanie: rozszerz kod o kolejną postać np. łucznik
Zadanie 1
Jesteś praktykantem w firmie zajmującej się tworzeniem witryn i aplikacji internetowych. Otrzymałeś zadanie polegające na stworzeniu aplikacji w języku PHP.
W aplikacji ma być utworzona klasa trójkąt, która zawiera dwa publiczne pola, takie jak: wysokość i podstawa oraz konstruktor, który przypisze im losowo wygenerowane wartości.
Ponadto w klasie powinna być zadeklarowana metoda obliczająca pole trójkąta.
W aplikacji należy utworzyć dwa obiekty klasy trójkąt.
Wynikiem działania aplikacji ma być wyświetlona wartość wysokości, podstawy i pola powierzchni obu trójkątów oraz informacja, który z trójkątów ma większą powierzchnię.
Zadanie 2
Jesteś praktykantem w firmie zajmującej się tworzeniem witryn i aplikacji internetowych. Otrzymałeś zadanie polegające na stworzeniu aplikacji w języku PHP.
W aplikacji ma być utworzona klasa odcinek zawierająca cztery publiczne pola, określające współrzędne początku i końca odcinka we współrzędnych x, y. W klasie odcinek należy utworzyć konstruktor, który współrzędnym przypisze podane przez użytkownika (za pomocą formularza) wartości.
Ponadto w klasie powinna być zadeklarowana metoda obliczająca długość odcinka.
W aplikacji należy utworzyć dwa obiekty klasy odcinek.
Wynikiem działania aplikacji ma być wyświetlona wartość długości obu odcinków oraz informacja, który z nich jest dłuższy.
Zadanie 3: System Zarządzania Gildią Graczy
Opis zadania:
Wyobraź sobie, że zarządzasz gildią w grze MMORPG. Twoim celem jest stworzenie systemu, który pozwala dodawać graczy do gildii, zarządzać ich rangami, a także organizować misje. Każdy gracz należy do gildii i może współpracować z innymi w ramach drużyny.
Wymagania funkcjonalne:
Klasa Player:
Atrybuty:
- name – imię gracza.
- rank – ranga w gildii (np. „Rekrut”, „Członek”, „Oficer”).
- level – poziom doświadczenia gracza.
- role – rola w drużynie (np. „Tank”, „DPS”, „Healer”).
Metody:
- promote() – zwiększa rangę gracza (np. z „Rekruta” na „Członka”).
- demote() – obniża rangę gracza (np. z „Oficera” na „Członka”).
Klasa Guild:
Atrybuty:
- name – nazwa gildii.
- members – lista graczy w gildii (obiekty klasy Player).
Metody:
- add_member(player) – dodaje nowego gracza do gildii.
- remove_member(player) – usuwa gracza z gildii.
- list_members() – wyświetla listę wszystkich graczy z ich szczegółami.
- assign_team(players) – przypisuje wybranych graczy do drużyny (np. na misję).
Klasa Mission:
Atrybuty:
- name – nazwa misji.
- difficulty – poziom trudności (np. „Łatwy”, „Średni”, „Trudny”).
- required_roles – lista wymaganych ról (np. [„Tank”, „DPS”, „Healer”]).
Metody:
- is_team_eligible(team) – sprawdza, czy drużyna spełnia wymagania roli.
Rozszerzenia:
- Dodaj system zdobywania punktów doświadczenia i automatycznego awansu na wyższy poziom.
- Wprowadź ograniczenie maksymalnej liczby graczy w gildii.
- Stwórz mechanizm losowania nagród za ukończone misje.
Zadanie 4: Gra Wirtualnych Walk Robotów
Opis zadania:
Stwórz symulację walk robotów, w której każdy robot ma różne parametry, broń i strategie walki.
Wymagania:
Klasa Robot:
Atrybuty:
- name – nazwa robota.
- health – punkty życia.
- attack_power – siła ataku.
- defense – poziom obrony.
- weapon – broń robota (np. „Laser”, „Rakiety”).
Metody:
- attack(opponent) – zmniejsza punkty życia przeciwnika na podstawie siły ataku i broni.
- defend(damage) – redukuje otrzymane obrażenia na podstawie obrony.
- status() – wyświetla aktualne zdrowie robota.
Symulator walki:
Wybierz dwóch robotów i pozwól im walczyć w rundach, aż jeden z nich przegra (zdrowie spadnie do 0). W każdej rundzie roboty na przemian atakują, a wynik walki jest wyświetlany po każdej turze.
Dodatkowe funkcjonalności:
Moduł ulepszeń:
- Dodaj klasę Upgrade, która pozwala ulepszać roboty (np. zwiększać zdrowie, siłę ataku, obronę).
- Przykład: „Dodatkowe Pancerze” zwiększają defense, a „Turbo Silnik” zwiększa szybkość ataku.
Różne typy broni:
- Każda broń może mieć dodatkowe efekty, np. laser ignoruje obronę przeciwnika, a rakiety zadają obrażenia obszarowe.
- Dodaj klasę Weapon z atrybutami damage i special_effect.
Tryb turniejowy:
- Dodaj system turniejów, w którym roboty walczą w eliminacjach, aż pozostanie jeden zwycięzca.
Losowe zdarzenia:
- Wprowadź zdarzenia w trakcie walki, np. „Awaria systemu” lub „Podwójne obrażenia”.
Zadanie 5: System Kart Kolekcjonerskich
Opis zadania:
Stwórz system kart kolekcjonerskich, które mogą być zbierane, wymieniane i używane w pojedynkach.
Wymagania:
Klasa Card:
Atrybuty:
- name – nazwa karty.
- attack – siła ataku karty.
- defense – poziom obrony karty.
- rarity – rzadkość karty (np. „Zwykła”, „Rzadka”, „Legendarna”).
Metody:
- compare(card) – porównuje siłę ataku dwóch kart i zwraca zwycięzcę.
Klasa Player:
Atrybuty:
- name – imię gracza.
- deck – talia kart (lista obiektów klasy Card).
Metody:
- add_card(card) – dodaje kartę do talii gracza.
- remove_card(card) – usuwa kartę z talii.
- battle(opponent) – wybiera jedną kartę z talii i walczy z wybraną kartą przeciwnika.
Rozgrywka:
Gracze losują karty z talii i walczą w turach. Wygrywa gracz, którego karty przetrwają najdłużej.
Dodatkowe funkcjonalności:
Tworzenie własnych kart:
- Dodaj system, w którym gracz może stworzyć własną kartę, przypisując jej atak, obronę i rzadkość w ramach określonych limitów.
Efekty specjalne kart:
- Wprowadź karty z efektami, np. „Ognisty Smok” podwaja atak podczas pierwszej tury, a „Magiczna Bariera” neguje pierwszy atak przeciwnika.
Mechanizm wymiany:
- Gracze mogą wymieniać karty między sobą.
- Dodaj opcję targowania się lub ustalania cen wirtualnych.
Rozgrywka wieloosobowa:
- Wprowadź tryb, w którym gracze mogą walczyć z innymi graczami w turniejach online (symulowanych w grze).
Zadanie 6: Gra Słowna z Przeciwnikiem AI
Opis zadania:
Stwórz grę, w której gracz i przeciwnik komputerowy rywalizują, tworząc jak najdłuższe słowa z losowego zestawu liter.
Wymagania:
Klasa WordGame:
Atrybuty:
- letters – lista liter, z których można tworzyć słowa.
- player_score – wynik gracza.
- ai_score – wynik przeciwnika.
Metody:
- generate_letters() – losuje zestaw liter.
- validate_word(word) – sprawdza, czy słowo jest poprawne (np. istnieje w słowniku).
- ai_move() – komputer tworzy losowe poprawne słowo z dostępnych liter.
Rozgrywka:
Gracz wprowadza słowo, a następnie przeciwnik generuje swoje słowo. Punktacja zależy od długości słowa. Gra kończy się po określonej liczbie tur.
Dodatkowe funkcjonalności:
Tryb rywalizacji:
- Dodaj możliwość gry na czas – gracz i AI mają ograniczoną liczbę sekund na wymyślenie słowa.
Poziomy trudności AI:
- Łatwy: AI wybiera krótkie słowa.
- Średni: AI stara się tworzyć średniej długości słowa.
- Trudny: AI generuje najdłuższe możliwe słowa.
System bonusów:
- Dodaj bonusy za użycie trudnych liter (np. „Q” lub „X”) lub za stworzenie słowa dłuższego niż 7 liter.
Ulepszenie liter:
- Gracz może wymienić kilka liter na nowe za stratę punktów.
Zadanie 7: Fantasy Liga e-Sportowa
Opis zadania:
Zaprojektuj system zarządzania drużyną wirtualnych graczy e-sportowych, gdzie każda drużyna rywalizuje w turnieju.
Wymagania:
Klasa Player:
Atrybuty:
- nickname – pseudonim gracza.
- role – rola w grze (np. „Support”, „Carry”).
- skill_level – poziom umiejętności gracza.
Metody:
- train() – zwiększa poziom umiejętności gracza.
- play_match() – zmniejsza zmęczenie gracza (opcjonalnie wprowadź mechanikę kondycji).
Klasa Team:
Atrybuty:
- name – nazwa drużyny.
- players – lista obiektów klasy Player.
Metody:
- add_player(player) – dodaje gracza do drużyny.
- remove_player(player) – usuwa gracza z drużyny.
- team_power() – oblicza łączny poziom umiejętności drużyny.
Turniej:
Stwórz symulację, w której drużyny rywalizują w turnieju, a wyniki zależą od poziomu umiejętności ich graczy. Wyświetlaj wyniki każdej rundy i ogłoś zwycięzcę.
Dodatkowe funkcjonalności:
Personalizacja graczy:
- Dodaj system awatarów, imion drużyn i personalizowanych logo.
Zdarzenia losowe:
- Przykład: „Kontuzja gracza” – gracz musi zostać wymieniony przed kolejnym meczem.
- „Eksplozja formy” – tymczasowy wzrost umiejętności jednego gracza.
Ekonomia drużyny:
- Wprowadź budżet, który pozwala na zatrudnianie lepszych graczy lub inwestowanie w trening.
Symulacja pełnych rozgrywek:
- Stwórz harmonogram meczów między drużynami, a na koniec wyświetlaj tabelę wyników.
Rozgrywka interaktywna:
- Gracz może aktywnie zarządzać strategią podczas meczu (np. wybór taktyki, zmiany w drużynie).
Zadania 8: System Handlu w Grze MMORPG
Opis zadania:
Stwórz symulację handlu w grze, gdzie gracze mogą sprzedawać i kupować przedmioty od siebie.
Wymagania:
Klasa Item:
Atrybuty:
- name – nazwa przedmiotu.
- type – rodzaj przedmiotu (np. „Broń”, „Zbroja”, „Eliksir”).
- price – cena przedmiotu.
Metody:
- discount(percent) – obniża cenę przedmiotu o podany procent.
Klasa Player:
Atrybuty:
- name – imię gracza.
- inventory – lista posiadanych przedmiotów.
- gold – ilość złota gracza.
Metody:
- buy(item) – kupuje przedmiot, o ile gracz ma wystarczająco złota.
- sell(item) – sprzedaje przedmiot i zwiększa ilość złota.
Klasa Marketplace:
Atrybuty:
- items – lista dostępnych przedmiotów.
Metody:
- list_items() – wyświetla dostępne przedmioty na rynku.
- trade(buyer, seller, item) – przeprowadza transakcję między graczami.
Rozgrywka:
Gracze mogą kupować i sprzedawać przedmioty na rynku. Ceny przedmiotów mogą się zmieniać w zależności od podaży i popytu (opcjonalne).
Dodatkowe funkcjonalności:
Dynamiczna ekonomia:
- Ceny przedmiotów zmieniają się w zależności od podaży i popytu na rynku.
Dom aukcyjny:
- Gracze mogą wystawiać przedmioty na aukcje, a inni licytować je w określonym czasie.
System reputacji:
- Gracze mogą zdobywać lub tracić reputację na podstawie uczciwości w handlu.
Rzemiosło:
- Wprowadź możliwość tworzenia nowych przedmiotów z surowców, które gracze mogą kupować, sprzedawać lub zdobywać podczas misji.
Mechanizm kradzieży:
- Dodaj opcję prób kradzieży przedmiotów od innych graczy, ale z ryzykiem utraty reputacji.
TaskQuest 1.5 — zadanie i użytkownik stają się obiektami
KARTA PRACY
Po Lekcji 14 TaskQuest wie, kto z niego korzysta, i pamięta go między żądaniami. Ale w środku aplikacji dane są rozrzucone. Zadanie to tablica z sześcioma kluczami, a pięć różnych funkcji w funkcje_zadan.php musi „pamiętać”, jak te klucze się nazywają. Dane użytkownika leżą w trzech miejscach: login i imię w sesji, XP i streak w luźnych zmiennych w index.php, a to, co z nich wynika (poziom, tytuł gracza, brakujące XP) — w funkcjach, którym za każdym razem trzeba podać $xp i $poziom.
Dziś połączymy dane i działania na nich w jedną całość. Zadanie i użytkownik staną się obiektami: będą same pilnować swoich danych i same wiedzieć, co potrafią. To programowanie obiektowe (ang. object-oriented programming, OOP).
Cel lekcji
Nauczyć się:
- czym jest klasa, a czym obiekt, i jak tworzyć obiekt przez
new, - zapisywać dane obiektu we właściwościach i działania na nich w metodach; rozumieć, co oznacza
$this, - przygotowywać obiekt do pracy w konstruktorze (
__construct), - ukrywać dane (
private) i udostępniać tylko bezpieczne operacje (public) — to hermetyzacja (enkapsulacja), - tworzyć obiekty czytelnie dzięki argumentom nazwanym,
- przebudować TaskQuest tak, żeby lista zadań była tablicą obiektów
Zadanie, a dane gracza — obiektemUzytkownik.
Efekt: TaskQuest wygląda i działa tak samo jak po Lekcji 14 — z jedną nową rzeczą widoczną dla użytkownika: zadania po terminie są wyróżnione czerwoną ramką. Całą resztę zmian widać w kodzie: zamiast $zadanie['tytul'] piszemy $zadanie->pobierzTytul(), zamiast obliczPoziom($xp) — $uzytkownik->pobierzPoziom().
Uwaga — co jest, a czego jeszcze nie ma:
- Dane nadal są w
data/zadania.csv, a XP, streak i data ostatniej aktywności nadal są symulowane (wpisane w kod). ObiektUzytkownikjest budowany od nowa przy każdym żądaniu z danych w sesji i tych symulowanych wartości. W sesji nadal trzymamy tylkologiniimie— nie cały obiekt.- Klasy dostaną metody
oznaczJakoWykonane()idodajXp(). Sprawdzimy je w brudnopisie, ale strona główna ich jeszcze nie wywołuje — użytkownik nie ma na razie przycisku „Zrobione!”. To zadanie końcowe A, a trwały zapis XP pojawi się razem z bazą danych.- Nadal nie używamy
htmlspecialchars()(Lekcja 22).- Z programowania obiektowego bierzemy dziś fundament: klasa, obiekt, właściwości, metody,
$this, konstruktor,private/public. Dziedziczenie poznasz w pigułce w zadaniu końcowym B. Interfejsy, klasy abstrakcyjne czy metody statyczne nie są TaskQuestowi na razie potrzebne.
Rozgrzewka — 4 pytania z Lekcji 14
Odpowiedz z pamięci, zanim zaczniesz:
- Dlaczego zwykła zmienna PHP (np.
$komunikat) znika po wysłaniu strony, a wartość zapisana w$_SESSIONjest dostępna w następnym żądaniu? - W
index.phpczytamy$_SESSION['imie']bez??, a$_COOKIE['filtr']— z??. Dlaczego? - Co robi
pobierzKomunikat()i dlaczego wywołujemy ją windex.phpdokładnie raz? - Od Lekcji 7 piszesz
new DateTime(...), a od Lekcji 12 —new Exception(...)i$wyjatek->getMessage(). Co Twoim zdaniem robi słowonew, a co strzałka->?
1. Wracamy do naszego TaskQuest
Wszystkie funkcje z Lekcji 1–14 zostają. Kod funkcji się nie zmienia — z jednym wyjątkiem: funkcje, które zaglądały do tablicy zadania ($zadanie['...']), będą teraz pytać obiekt ($zadanie->...()). Logika (filtrowanie, sumowanie, sortowanie, szukanie największego id) zostaje taka sama.
Docelowa struktura projektu
taskquest/
├── index.php (zmiana: obiekt Uzytkownik, lista zadań to obiekty Zadanie, wyróżnienie zadań po terminie)
├── dodaj-zadanie.php (zmiana: nowe zadanie to obiekt Zadanie)
├── zaloguj.php bez zmian
├── wyloguj.php bez zmian
├── diagnostyka.php (zmiana: nowe pliki na liście wymaganych, przegląd katalogu classes/)
├── classes/ NOWY KATALOG — klasy
│ ├── Zadanie.php NOWY — klasa Zadanie
│ └── Uzytkownik.php NOWY — klasa Uzytkownik
├── config/
│ └── config.php bez zmian
├── includes/
│ ├── header.php (zmiana: styl .po-terminie)
│ ├── footer.php (zmiana: "TaskQuest 1.5")
│ ├── funkcje_grywalizacji.php bez zmian
│ ├── funkcje_tekstowe.php bez zmian
│ ├── funkcje_terminow.php bez zmian
│ ├── funkcje_zadan.php (zmiana: $zadanie['...'] → $zadanie->...())
│ ├── funkcje_sesji.php bez zmian
│ ├── funkcje_diagnostyczne.php bez zmian
│ └── funkcje_plikow.php (zmiana: wczytywanie tworzy obiekty, zapis czyta z obiektów)
└── data/
└── zadania.csv bez zmian (format pliku się nie zmienia!)
Co gdzie trafi
| Plik | Co się zmienia |
|---|---|
classes/Zadanie.php | nowa klasa: dane zadania + metody (pobierzTytul(), czyWykonane(), czyPoTerminie()…) |
classes/Uzytkownik.php | nowa klasa: login, imię, XP, streak + metody (pobierzPoziom(), dodajXp()…) |
includes/funkcje_plikow.php | wczytajZadania() zwraca obiekty; zapiszZadania() czyta dane z obiektów |
includes/funkcje_zadan.php | odwołania do kluczy tablicy zamienione na metody |
index.php | obiekt $uzytkownik; lista zadań czytana metodami; zadania po terminie wyróżnione |
dodaj-zadanie.php | nowe zadanie tworzone przez new Zadanie(...) |
header.php, footer.php, diagnostyka.php | styl, numer wersji, lista wymaganych plików |
Przygotowanie (zrób to teraz)
- Zrób kopię zapasową katalogu
taskquest/z Lekcji 14 — jakotaskquest_lekcja14/(obok, nie w środku). Przyda się do testu regresji. - Zrób kopię
data/zadania.csvpoza katalogiem projektu. - Zaloguj się i sprawdź, czy
index.phpdziała, adiagnostyka.phppokazuje 15 ×[OK].
2. Problem w aplikacji
Do zespołu TaskQuest dołącza Ola. Dostaje zadanie: „dopisz przycisk odznaczania zadania” i pisze w brudnopisie kod, który wygląda rozsądnie.
BRUDNOPIS. Wklej do brudnopis.php i zanim uruchomisz, przewidź, co się wyświetli:
<?php
$zadanie = [
'id' => 1,
'tytul' => 'Nauczyć się OOP',
'opis' => 'Klasa, obiekt, metoda',
'termin' => '2026-10-10',
'wykonane' => false,
'xp' => 30
];
echo "<p>Tytuł: " . $zadanie['tytuł'] . "</p>"; // Ola napisała „tytuł” z polską literą
$zadanie['wykonane'] = 'nie'; // Ola „odznacza” zadanie tekstem
if ($zadanie['wykonane']) {
echo "<p>Zadanie wykonane!</p>";
}
$zadanie['xp'] = -500; // nic tego nie zabrania
$inneZadanie = ['id' => 2, 'tytul' => 'Zadanie bez terminu']; // brakuje czterech kluczy
echo "<p>Koniec skryptu.</p>";
Co zobaczyłaś/zobaczyłeś?
Warning: Undefined array key "tytuł"i pusty tytuł — PHP ostrzega, ale jedzie dalej.- „Zadanie wykonane!” — bo niepusty tekst
'nie'w warunkuifjest traktowany jaktrue. - XP = −500 i zadanie bez terminu — żadnego protestu. Kłopot wyjdzie dopiero później, w zupełnie innym miejscu aplikacji (np. gdy
komunikatTerminu()dostanie brak terminu).
Tablica to „pudełko z przegródkami”: każdy może do niej włożyć cokolwiek, pod dowolną nazwą. Nikt nie pilnuje, jakie klucze musi mieć zadanie, jakie wartości są dozwolone i kto może je zmieniać.
Z użytkownikiem jest podobnie. Żeby wyświetlić jego panel, index.php musi znać przepis:
$poziom = obliczPoziom($xp);
$tytul = pobierzTytul($poziom);
$brakujeXp = ileBrakujeXp($xp, $poziom); ← trzeba pamiętać, co i w jakiej kolejności podać
A dane, z których to się liczy, leżą w trzech miejscach: $_SESSION (login, imię), luźne zmienne ($xp, $streak, $dataOstatniejAktywnosci) i funkcje w innym pliku.
Potrzebujemy:
- zadania, które ma zawsze komplet danych i pozwala zmienić tylko to, co wolno (np. oznaczyć je jako wykonane — ale nie ustawić XP na −500),
- użytkownika, który sam wie, jaki ma poziom i ile XP mu brakuje — bez podawania mu za każdym razem jego własnego XP,
- żeby pomyłka w nazwie (
tytułzamiasttytul) kończyła się od razu wyraźnym błędem, a nie cichym ostrzeżeniem i pustym tekstem.
3. Nowy mechanizm PHP: klasy i obiekty
3.1 Obiekty już znasz
Od Lekcji 7 używasz obiektów — tylko cudzych:
$dzisiaj = new DateTime('today'); // new — utwórz obiekt klasy DateTime
echo $dzisiaj->format('Y-m-d'); // -> — poproś obiekt o wykonanie metody format()
$wyjatek = new Exception('Coś poszło nie tak.');
echo $wyjatek->getMessage();
DateTime i Exception to klasy napisane przez twórców PHP. Obiekt $dzisiaj przechowuje w sobie datę (dane) i potrafi ją sformatować (działanie). Nie wiesz, jak DateTime przechowuje datę w środku — i nie musisz. Korzystasz tylko z metod, które klasa udostępnia.
Dziś napiszesz własne klasy. Zaczniemy od małego przykładu w brudnopisie, a dopiero potem przejdziemy do TaskQuest.
3.2 Klasa i obiekt
Wyobraź sobie, że TaskQuest w przyszłości przyznaje odznaki — np. „Pierwsze zadanie” za 10 XP. Każda odznaka ma nazwę i nagrodę XP.
BRUDNOPIS. Zastąp zawartość brudnopis.php:
<?php
class Odznaka
{
public string $nazwa;
public int $xp;
}
$odznaka = new Odznaka();
$odznaka->nazwa = 'Pierwsze zadanie';
$odznaka->xp = 10;
$druga = new Odznaka();
$druga->nazwa = 'Streak 7 dni';
$druga->xp = 50;
echo "<p>" . $odznaka->nazwa . " — " . $odznaka->xp . " XP</p>";
echo "<p>" . $druga->nazwa . " — " . $druga->xp . " XP</p>";
echo "<pre>";
var_dump($odznaka);
echo "</pre>";
Nowe pojęcia:
- Klasa (
class Odznaka) to plan — opis tego, jakie dane ma mieć każda odznaka. Sama klasa niczego nie przechowuje. - Obiekt (
new Odznaka()) to egzemplarz zbudowany według planu. Z jednej klasy możesz zrobić dowolnie wiele obiektów —$odznakai$drugasą od siebie niezależne. - Właściwość (
public string $nazwa;) to zmienna należąca do obiektu. Ma typ — tak jak parametry funkcji. ->sięga do właściwości (i metod) konkretnego obiektu. Zwróć uwagę: po strzałce nie ma$— piszemy$odznaka->nazwa, nie$odznaka->$nazwa.
var_dump() pokazuje obiekt: nazwę klasy, numer obiektu (#1) i wartości wszystkich właściwości.
Zasady zapisu, których trzymamy się w TaskQuest: nazwa klasy wielką literą (Odznaka, Zadanie, Uzytkownik), jedna klasa w jednym pliku, plik nazywa się tak jak klasa.
3.3 Metody i $this
Chcemy, żeby odznaka sama potrafiła się opisać. Dopisz w klasie, pod właściwościami:
public function opis(): string
{
return $this->nazwa . " (+" . $this->xp . " XP)";
}
a zamiast dwóch linii z echo wpisz:
echo "<p>" . $odznaka->opis() . "</p>";
echo "<p>" . $druga->opis() . "</p>";
Metoda to funkcja zapisana w klasie. Wywołujemy ją na obiekcie: $odznaka->opis().
$this oznacza „ten obiekt, na którym właśnie wywołano metodę”. Przy $odznaka->opis() — $this to $odznaka, przy $druga->opis() — $this to $druga. Dlatego ta sama metoda daje różne wyniki. Metoda nie potrzebuje parametrów, żeby dostać nazwę i XP — ma je w swoim obiekcie. To właśnie brakowało nam przy obliczPoziom($xp).
3.4 Konstruktor
Obiekt z punktu 3.2 przez chwilę istnieje „pusty” — między new Odznaka() a ustawieniem nazwy. Gdyby ktoś zapomniał ustawić xp, dostalibyśmy błąd dopiero przy pierwszym odczycie. Lepiej, żeby obiekt był kompletny od chwili powstania.
Służy do tego konstruktor — metoda o specjalnej nazwie __construct (dwa podkreślniki), którą PHP wywołuje automatycznie przy new:
class Odznaka
{
public string $nazwa;
public int $xp;
public function __construct(string $nazwa, int $xp)
{
$this->nazwa = $nazwa;
$this->xp = $xp;
}
public function opis(): string
{
return $this->nazwa . " (+" . $this->xp . " XP)";
}
}
$odznaka = new Odznaka('Pierwsze zadanie', 10);
$druga = new Odznaka('Streak 7 dni', 50);
Argumenty z new Odznaka(...) trafiają do parametrów konstruktora. W linii $this->nazwa = $nazwa; są dwie różne rzeczy o tej samej nazwie: $nazwa — parametr (wartość przekazana przy new), $this->nazwa — właściwość obiektu. Konstruktor przepisuje jedno do drugiego.
Sprawdź: new Odznaka('Pierwsze zadanie') (bez XP) kończy się od razu błędem ArgumentCountError: Too few arguments.... Nie da się już zrobić niekompletnej odznaki.
3.5 private i public — obiekt pilnuje swoich danych
Dopisz na końcu brudnopisu:
$odznaka->xp = -500;
echo "<p>" . $odznaka->opis() . "</p>";
Działa — i to jest problem. Właściwości są public, więc każdy kod może w nich grzebać, tak jak Ola w tablicy zadania.
Zmień w klasie oba słowa public przy właściwościach na private i odśwież. Dostajesz:
Fatal error: Uncaught Error: Cannot access private property Odznaka::$xp
Modyfikatory dostępu mówią, skąd wolno sięgać do właściwości lub metody:
| Modyfikator | Kto ma dostęp |
|---|---|
public | każdy kod — także spoza klasy |
private | tylko metody tej klasy (przez $this) |
Reguła, której trzymamy się w TaskQuest: właściwości private, metody public. Obiekt ukrywa swoje dane i pokazuje na zewnątrz tylko to, na co się zgadza. To nazywa się hermetyzacja (enkapsulacja).
Skoro z zewnątrz nie można przeczytać $odznaka->xp, dodajemy metody, które zwracają dane — tzw. gettery. A jeśli jakąś daną wolno zmieniać, dajemy metodę, która zmienia ją według reguł. Pełna wersja odznaki:
<?php
class Odznaka
{
private string $nazwa;
private int $xp;
public function __construct(string $nazwa, int $xp)
{
$this->nazwa = $nazwa;
$this->xp = $xp;
}
public function pobierzNazwe(): string
{
return $this->nazwa;
}
public function pobierzXp(): int
{
return $this->xp;
}
public function zwiekszNagrode(int $dodatkoweXp): void
{
if ($dodatkoweXp <= 0) {
throw new Exception('Nagrodę za odznakę można tylko zwiększyć.');
}
$this->xp += $dodatkoweXp;
}
}
$odznaka = new Odznaka('Pierwsze zadanie', 10);
$odznaka->zwiekszNagrode(5);
echo "<p>" . $odznaka->pobierzNazwe() . ": " . $odznaka->pobierzXp() . " XP</p>"; // 15 XP
try {
$odznaka->zwiekszNagrode(-500);
} catch (Exception $wyjatek) {
echo "<p>" . $wyjatek->getMessage() . "</p>";
}
echo "<p>Nadal: " . $odznaka->pobierzXp() . " XP</p>"; // 15 XP
Co tu się zmieniło w porównaniu z tablicą?
- XP da się przeczytać (
pobierzXp()), ale zmienić tylko przezzwiekszNagrode()— a ta metoda pilnuje reguły. - Nazwy nie da się zmienić wcale — nie ma do tego metody. Nie każda właściwość potrzebuje metody zmieniającej, a nawet nie każda potrzebuje gettera.
- Złe wywołanie (
-500) to błąd programisty, a nie błąd użytkownika — dlatego jest wyjątek, a nie tablica komunikatów (reguła z Lekcji 12: wyjątek to awaria, a nie zwykła odpowiedź).
Nazwy metod w TaskQuest: pobierz...() — zwraca daną, czy...() — zwraca true/false, czasownik (zwiekszNagrode(), oznaczJakoWykonane()) — zmienia stan obiektu.
3.6 Argumenty nazwane
Przy dwóch parametrach łatwo zapamiętać kolejność. Zadanie w TaskQuest będzie miało sześć — w tym dwa int i bool gdzieś w środku:
new Zadanie(6, 'Test', 'Opis', '2026-10-01', false, 25); // co to jest 6, a co 25? co znaczy false?
Od PHP 8 możesz podać argumenty po nazwie parametru:
$odznaka = new Odznaka(nazwa: 'Streak 7 dni', xp: 50);
$trzecia = new Odznaka(xp: 100, nazwa: 'Poziom 5'); // kolejność nie ma znaczenia
Nazwa przed dwukropkiem to nazwa parametru z konstruktora (bez $). Kod jest dłuższy, ale od razu wiadomo, co jest czym — a zamiana kolejności nie psuje danych (zobaczysz to w Eksperymencie 4). Argumenty nazwane działają tak samo przy zwykłych funkcjach i metodach. W TaskQuest używamy ich przy tworzeniu obiektów z wieloma parametrami.
3.7 Obiekty w tablicy
Obiekty można trzymać w tablicy jak każdą inną wartość. Dopisz w brudnopisie:
$odznaki = [
new Odznaka(nazwa: 'Pierwsze zadanie', xp: 10),
new Odznaka(nazwa: 'Streak 7 dni', xp: 50),
new Odznaka(nazwa: 'Poziom 5', xp: 100)
];
$sumaXp = 0;
foreach ($odznaki as $odznaka) {
$sumaXp += $odznaka->pobierzXp();
}
echo "<p>Odznaki: " . count($odznaki) . ", razem " . $sumaXp . " XP</p>"; // 3, razem 160 XP
count(), foreach, array_filter(), usort() działają tak samo jak dotąd. Zmienia się tylko sposób odczytu elementu: zamiast $element['xp'] — $element->pobierzXp(). Tak właśnie przebudujemy listę zadań w TaskQuest: tablica zostaje, zmienia się to, co w niej leży.
Gdy zapomnisz o zmianie i potraktujesz obiekt jak tablicę ($odznaka['xp']), PHP od razu zatrzyma skrypt:
Fatal error: Uncaught Error: Cannot use object of type Odznaka as array in ... on line ...
Ten komunikat będzie Twoją mapą w sekcji 5 — wskaże każde miejsce, którego jeszcze nie przerobiłaś/nie przerobiłeś.
3.8 Klasa w osobnym pliku
W brudnopisie klasa i kod, który z niej korzysta, są w jednym pliku. W aplikacji każda klasa dostaje własny plik w katalogu classes/ — tak jak funkcje mają swoje pliki w includes/. Plik z klasą to samo PHP (bez zamykającego ?>), a strona, która tworzy obiekt, musi go wcześniej dołączyć:
require_once __DIR__ . '/classes/Zadanie.php';
Bez tego: Error: Class "Zadanie" not found.
3.9 Typowe błędy
$obiekt->$wlasciwosc— dolar po strzałce. PHP szuka wtedy właściwości o nazwie zapisanej w zmiennej$wlasciwosc(której nie ma) → ostrzeżeniaUndefined variableiUndefined property.- Brak
$this->w metodzie —return $xp;zamiastreturn $this->xp;. Wewnątrz metody$xpto zwykła zmienna lokalna, której nie ma (Eksperyment 2). _constructz jednym podkreślnikiem — to wtedy zwykła metoda, PHP jej nie wywoła przynew. Obiekt powstaje bez danych, a błąd wychodzi dopiero przy pierwszym odczycie właściwości:must not be accessed before initialization.- Konstruktor, który nie ustawia wszystkich właściwości — ten sam błąd co wyżej (Eksperyment 3).
- Obiekt traktowany jak tablica —
Cannot use object of type ... as array. - Klasa nie dołączona —
Class "..." not found. - Mylenie klasy z obiektem —
Zadanie->pobierzTytul()nie ma sensu: plan nie ma tytułu, ma go konkretne zadanie ($zadanie->pobierzTytul()).
4. SAMODZIELNIE — klasa Zadanie
PROJEKT. Od tego miejsca kończymy pracę na odznakach. Tworzysz nowy katalog projektu taskquest/classes/ i w nim nowy plik classes/Zadanie.php. Sam plik niczego w aplikacji nie zmienia, dopóki żadna strona go nie dołączy — dlatego najpierw przetestujesz klasę z brudnopisu, a do aplikacji podłączysz ją w sekcji 5.
Napisz klasę Zadanie, która zastąpi tablicę zadania.
Właściwości — wszystkie private, z typami. Nazwy i typy takie same jak klucze tablicy (patrz Lekcja 9 i 11):
| Właściwość | Typ | Znaczenie |
|---|---|---|
$id | int | numer zadania |
$tytul | string | tytuł (surowy, „brudny” — czyścimy go przy wyświetlaniu, jak dotąd) |
$opis | string | opis |
$termin | string | termin w formacie RRRR-MM-DD |
$wykonane | bool | czy zadanie jest wykonane |
$xp | int | nagroda XP za zadanie |
Konstruktor — sześć parametrów o tych samych nazwach, w tej samej kolejności co kolumny w data/zadania.csv: id, tytul, opis, termin, wykonane, xp.
Metody (wszystkie public):
| Metoda | Zwraca | Co robi |
|---|---|---|
pobierzId() | int | numer zadania |
pobierzTytul() | string | tytuł |
pobierzOpis() | string | opis |
pobierzTermin() | string | termin |
czyWykonane() | bool | czy zadanie jest wykonane |
pobierzXp() | int | nagroda XP |
oznaczJakoWykonane() | void | ustawia zadanie jako wykonane |
Świadomie nie ma metod typu „ustaw XP”, „ustaw tytuł”, „odznacz zadanie”. Zadanie w TaskQuest można dziś tylko wykonać — więc tylko na to obiekt pozwala. Jeśli kiedyś aplikacja będzie potrzebowała edycji, dopiszemy metodę z odpowiednimi regułami.
Uwaga na nazwę
pobierzTytul(). Wfunkcje_grywalizacji.phpistnieje funkcjapobierzTytul(int $poziom), która zwraca tytuł gracza („Nowicjusz”…). Metoda$zadanie->pobierzTytul()zwraca tytuł zadania. PHP ich nie pomyli: metodę zawsze wywołujemy na obiekcie (po strzałce), funkcję — bez obiektu. To kolejny „tytuł” w TaskQuest (obok tytułu gracza$tytuli$nazwaStrony) — pilnuj, o który chodzi.
Sprawdź (brudnopis — zastąp całą zawartość brudnopis.php):
<?php
require_once __DIR__ . '/classes/Zadanie.php';
$zadanie = new Zadanie(
id: 1,
tytul: 'Nauczyć się OOP',
opis: 'Klasa, obiekt, metoda',
termin: '2026-10-10',
wykonane: false,
xp: 30
);
echo "<p>" . $zadanie->pobierzId() . ". " . $zadanie->pobierzTytul() . " — " . $zadanie->pobierzXp() . " XP</p>";
echo "<p>" . $zadanie->pobierzOpis() . " (termin: " . $zadanie->pobierzTermin() . ")</p>";
var_dump($zadanie->czyWykonane());
$zadanie->oznaczJakoWykonane();
var_dump($zadanie->czyWykonane());
Oczekiwane: 1. Nauczyć się OOP — 30 XP, Klasa, obiekt, metoda (termin: 2026-10-10), bool(false), bool(true). Żadnych ostrzeżeń.
HINT
Zbuduj klasę dokładnie według wzoru Odznaka z punktu 3.5: najpierw sześć właściwości, potem konstruktor, który przepisuje sześć parametrów do sześciu właściwości, potem gettery. oznaczJakoWykonane() przypomina zwiekszNagrode(), tylko jest prostsza — nie ma reguły do sprawdzenia.
POMOC
<?php
class Zadanie
{
private int $id;
// ... pozostałe pięć właściwości ...
public function __construct(
int $id,
string $tytul,
// ... pozostałe cztery parametry ...
) {
$this->id = $id;
// ...
}
public function pobierzId(): int
{
return $this->id;
}
// ... pozostałe gettery ...
public function oznaczJakoWykonane(): void
{
// ... zmiana jednej właściwości ...
}
}
Długą listę parametrów zapisujemy w kilku liniach — nawias ) i { konstruktora stoją wtedy razem w jednej linii.
Plik projektu: classes/Zadanie.php
ROZWIĄZANIE
<?php
class Zadanie
{
private int $id;
private string $tytul;
private string $opis;
private string $termin;
private bool $wykonane;
private int $xp;
public function __construct(
int $id,
string $tytul,
string $opis,
string $termin,
bool $wykonane,
int $xp
) {
$this->id = $id;
$this->tytul = $tytul;
$this->opis = $opis;
$this->termin = $termin;
$this->wykonane = $wykonane;
$this->xp = $xp;
}
public function pobierzId(): int
{
return $this->id;
}
public function pobierzTytul(): string
{
return $this->tytul;
}
public function pobierzOpis(): string
{
return $this->opis;
}
public function pobierzTermin(): string
{
return $this->termin;
}
public function czyWykonane(): bool
{
return $this->wykonane;
}
public function pobierzXp(): int
{
return $this->xp;
}
public function oznaczJakoWykonane(): void
{
$this->wykonane = true;
}
}
4b. SAMODZIELNIE — czyPoTerminie()
Teraz metoda, która daje TaskQuestowi nową umiejętność. Dopisz w klasie Zadanie, pod oznaczJakoWykonane():
czyPoTerminie(): bool
Zadanie jest po terminie, gdy nie jest wykonane i jego termin jest wcześniejszy niż dzisiaj. Zadanie z terminem na dzisiaj nie jest jeszcze po terminie — użytkownik ma czas do końca dnia. Wykonane zadanie nigdy nie jest po terminie.
| Zadanie | czyPoTerminie() |
|---|---|
| niewykonane, termin wczoraj | true |
| niewykonane, termin dzisiaj | false |
| niewykonane, termin jutro | false |
| wykonane, termin wczoraj | false |
Wykorzystaj wiedzę z Lekcji 7 (DateTime).
HINT
Najpierw obsłuż prosty przypadek: jeśli zadanie jest wykonane — od razu return false. Potem zrób dwa obiekty DateTime: „dzisiaj” i termin zadania. Uwaga: new DateTime() bez argumentu to „teraz” (z godziną), a new DateTime('today') to „dzisiaj o północy” — tak samo jak data z terminu, która też ma godzinę 00:00.
POMOC
Obiekty DateTime można porównywać zwykłymi operatorami <, >, == — PHP porównuje wtedy daty i godziny:
$pierwsza = new DateTime('2026-09-20');
$druga = new DateTime('2026-09-21');
var_dump($pierwsza < $druga); // bool(true)
ROZWIĄZANIE
public function czyPoTerminie(): bool
{
if ($this->wykonane) {
return false;
}
$dzisiaj = new DateTime('today');
$termin = new DateTime($this->termin);
return $termin < $dzisiaj;
}
Dlaczego 'today', a nie samo new DateTime()? Przy terminie na dzisiaj porównywalibyśmy „dziś 00:00” z „dziś 14:37” — i zadanie z dzisiejszym terminem byłoby już „po terminie”. Pełne testy tej metody (z datami liczonymi od dzisiaj, żeby nie zależały od dnia lekcji) — w sekcji 7.
5. Włączamy klasę Zadanie do TaskQuest — krok po kroku
PROJEKT. Od teraz zmieniamy pliki aplikacji. Plan:
PRZED (Lekcja 14) PO (Lekcja 15)
$zadania = [ $zadania = [
['id' => 1, 'tytul' => ..., ...], obiekt Zadanie (id 1),
['id' => 2, 'tytul' => ..., ...], obiekt Zadanie (id 2),
... ...
]; ];
$zadanie['tytul'] $zadanie->pobierzTytul()
$zadanie['wykonane'] $zadanie->czyWykonane()
Plik data/zadania.csv się nie zmienia. Obiekty powstają przy wczytywaniu pliku i zamieniają się z powrotem w wiersze przy zapisie — a pomiędzy nimi cała aplikacja pracuje na obiektach.
Pracujemy „od źródła danych”: najpierw miejsce, w którym zadania powstają (wczytajZadania()), a potem po kolei wszystkie miejsca, które z nich korzystają. Między krokami aplikacja będzie się wysypywać — i to dobrze: komunikat Cannot use object of type Zadanie as array wskaże plik i linię, którą trzeba jeszcze przerobić.
Krok 1 — dołącz klasę
Pliki projektu: index.php i dodaj-zadanie.php. W bloku // --- pliki aplikacji ---, zaraz pod require_once pliku config/config.php, dodaj:
require_once __DIR__ . '/classes/Zadanie.php';
Klasę dołączamy przed plikami z funkcjami, bo funkcje_plikow.php będzie tworzyć obiekty Zadanie. (Ściśle mówiąc, PHP potrzebuje klasy dopiero w chwili wywołania new Zadanie(...), ale kolejność „najpierw klasy, potem funkcje, które z nich korzystają” łatwiej czytać.)
zaloguj.php i wyloguj.php nie korzystają z zadań — nic w nich nie zmieniamy.
Krok 2 — SAMODZIELNIE: wczytajZadania() tworzy obiekty
Plik projektu: includes/funkcje_plikow.php.
W wczytajZadania() znajdź miejsce, w którym z tablicy $wiersz (jednej linii pliku CSV) powstaje zadanie i jest dopisywane do $zadania. Dziś jest to tablica z kluczami 'id', 'tytul', 'opis', 'termin', 'wykonane', 'xp'. Zamień ją na obiekt Zadanie. Konwersje typów zostają takie same: id i xp przez (int), wykonane przez === '1'.
Nie zmieniaj niczego innego — sprawdzanie pliku, sprawdzWierszZadania(), pomijanie pustych linii, finally i wyjątki zostają. sprawdzWierszZadania() nadal pilnuje, żeby wiersz był poprawny, zanim zrobimy z niego obiekt.
Po zmianie odśwież index.php. Spodziewany wynik: błąd, np.
Fatal error: Uncaught Error: Cannot use object of type Zadanie as array in ...\includes\funkcje_zadan.php on line ...
Tak ma być — wczytajZadania() zwraca już obiekty, a funkcje z funkcje_zadan.php jeszcze traktują je jak tablice. To następny krok.
HINT
To jedna instrukcja: zamiast $zadania[] = [ ... ]; będzie $zadania[] = new Zadanie( ... );. Użyj argumentów nazwanych — nazwy parametrów konstruktora są takie same jak dotychczasowe klucze.
ROZWIĄZANIE
Było (u Ciebie fragment może być zapisany trochę inaczej — ważne jest miejsce, w którym powstaje tablica zadania):
$zadania[] = [
'id' => (int) $wiersz[0],
'tytul' => $wiersz[1],
'opis' => $wiersz[2],
'termin' => $wiersz[3],
'wykonane' => $wiersz[4] === '1',
'xp' => (int) $wiersz[5]
];
Jest:
$zadania[] = new Zadanie(
id: (int) $wiersz[0],
tytul: $wiersz[1],
opis: $wiersz[2],
termin: $wiersz[3],
wykonane: $wiersz[4] === '1',
xp: (int) $wiersz[5]
);
Zapis prawie się nie zmienił — ale różnica jest duża. Tablica przyjęłaby literówkę w kluczu albo brak klucza. Konstruktor wymaga wszystkich sześciu wartości, a nazwy parametrów są sprawdzane: tytuł: ... z polską literą skończy się od razu błędem Unknown named parameter $tytuł.
Krok 3 — SAMODZIELNIE: funkcje zadań pytają obiekt
Plik projektu: includes/funkcje_zadan.php.
W pięciu funkcjach zamień odwołania do kluczy tablicy na wywołania metod. Logika każdej funkcji zostaje taka sama.
| Funkcja | Było | Jest |
|---|---|---|
pobierzWykonaneZadania() | $zadanie['wykonane'] | $zadanie->czyWykonane() |
pobierzAktywneZadania() | $zadanie['wykonane'] | $zadanie->czyWykonane() |
sumujXp() | $zadanie['xp'] | $zadanie->pobierzXp() |
sortujPoTerminie() | $a['termin'], $b['termin'] | $a->pobierzTermin(), $b->pobierzTermin() |
nastepneIdZadania() | $zadanie['id'] | $zadanie->pobierzId() |
walidujNoweZadanie() zostaje bez zmian — pracuje na tekstach z formularza, a nie na zadaniu.
Po każdej zmianie odświeżaj index.php: błąd Cannot use object of type Zadanie as array przesunie się do kolejnej funkcji, a na końcu — do index.php.
HINT
W pobierzWykonaneZadania() i pobierzAktywneZadania() jest funkcja przekazana do array_filter() — to w niej jest odwołanie do klucza. W sortujPoTerminie() — w funkcji porównującej przekazanej do usort(). Jeśli w warunku było porównanie === true albo === false, możesz je zostawić — czyWykonane() zwraca prawdziwy bool.
ROZWIĄZANIE
<?php
function pobierzWykonaneZadania(array $zadania): array
{
return array_filter($zadania, function ($zadanie) {
return $zadanie->czyWykonane();
});
}
function pobierzAktywneZadania(array $zadania): array
{
return array_filter($zadania, function ($zadanie) {
return !$zadanie->czyWykonane();
});
}
function sumujXp(array $zadania): int
{
$suma = 0;
foreach ($zadania as $zadanie) {
$suma += $zadanie->pobierzXp();
}
return $suma;
}
function sortujPoTerminie(array $zadania): array
{
usort($zadania, function ($a, $b) {
return $a->pobierzTermin() <=> $b->pobierzTermin();
});
return $zadania;
}
function nastepneIdZadania(array $zadania): int
{
$najwiekszeId = 0;
foreach ($zadania as $zadanie) {
if ($zadanie->pobierzId() > $najwiekszeId) {
$najwiekszeId = $zadanie->pobierzId();
}
}
return $najwiekszeId + 1;
}
// ... walidujNoweZadanie() — bez zmian ...
Jeśli Twoje funkcje są zapisane inaczej (np. funkcją strzałkową fn albo z inną nazwą zmiennej w pętli) — zostaw swój zapis i zmień tylko odwołania z tabeli. Typy parametrów (array $zadania) i typy zwracane się nie zmieniają: to nadal tablice, tylko z obiektami w środku. array_filter() nadal zachowuje klucze, a usort() nadal numeruje od 0.
Krok 4 — SAMODZIELNIE: lista zadań w index.php + wyróżnienie zadań po terminie
Plik projektu: index.php, sekcja prezentacji (gałąź else sekcji „Twoje zadania”).
- W bloku „najpilniejsze zadanie” i w pętli
foreach ($zadaniaDoWyswietlenia as $zadanie)zamień wszystkie$najpilniejsze['...']i$zadanie['...']na metody. W pętli jest ich dziewięć, w bloku najpilniejszego zadania — dwa. - Nowość dla użytkownika: zadanie po terminie ma dostać klasę CSS
zadanie po-terminie(zamiastzadanie). UżyjczyPoTerminie()— to jedyne miejsce w aplikacji, gdzie ta metoda jest dziś potrzebna. - Plik projektu:
includes/header.php— w<style>, pod regułą.sukces, dodaj regułę:
.zadanie.po-terminie { border: 2px solid #c0392b; }
Efekt: strona wygląda jak po Lekcji 14, ale niewykonane zadania z minionym terminem mają czerwoną ramkę. Brak błędów i ostrzeżeń.
HINT
Uważaj na wstawianie w tekst. Zapis {$zadanie['xp']} wewnątrz cudzysłowu zamień na sklejanie kropką: " — " . $zadanie->pobierzXp() . " XP</p>". (Wywołanie metody w klamrach wewnątrz cudzysłowu też działa, ale sklejanie jest czytelniejsze.) Dla klasy CSS: najpierw zostaw dotychczasowe przypisanie $klasa, a pod nim — jeśli zadanie jest po terminie — nadpisz $klasa.
ROZWIĄZANIE
Najpilniejsze zadanie:
$najpilniejsze = $aktywneWgTerminu[0];
echo "<p>Najpilniejsze zadanie: <strong>" . wyczyscTytul($najpilniejsze->pobierzTytul()) . "</strong> — "
. komunikatTerminu($najpilniejsze->pobierzTermin()) . "</p>";
Pętla:
foreach ($zadaniaDoWyswietlenia as $zadanie) {
$tytulCzysty = wyczyscTytul($zadanie->pobierzTytul());
$znacznik = $zadanie->czyWykonane() ? "[X]" : "[ ]";
$klasa = $zadanie->czyWykonane() ? "zadanie wykonane" : "zadanie";
if ($zadanie->czyPoTerminie()) {
$klasa = "zadanie po-terminie";
}
echo "<div class=\"$klasa\">";
echo "<p><strong>$znacznik $tytulCzysty</strong> — " . $zadanie->pobierzXp() . " XP</p>";
echo "<p>" . skrocOpis($zadanie->pobierzOpis(), 50) . "</p>";
echo "<p>Termin: " . $zadanie->pobierzTermin();
if (!$zadanie->czyWykonane()) {
echo " — " . komunikatTerminu($zadanie->pobierzTermin());
}
echo "</p>";
if (stripos($tytulCzysty, 'PHP') !== false) {
echo "<p>To zadanie dotyczy PHP.</p>";
}
echo "</div>";
}
Dlaczego nadpisujemy $klasa tylko jednym if? czyPoTerminie() samo zwraca false dla wykonanych zadań — tę regułę zapisaliśmy raz, w klasie. Strona nie musi jej znać.
Krok 5 — SAMODZIELNIE: zapis zadań
Zostały dwa miejsca, w których zadania wracają do pliku lub powstają z formularza.
a) Plik projektu: includes/funkcje_plikow.php, funkcja zapiszZadania(). Tam, gdzie dla każdego zadania budowana jest tablica przekazywana do fputcsv(), zamień $zadanie['...'] na metody. wykonane nadal zapisujemy jako 1/0. Kolejność pól musi zostać taka sama jak w nagłówku: id;tytul;opis;termin;wykonane;xp.
b) Plik projektu: dodaj-zadanie.php. Nowe zadanie dopisywane do $zadania ma być obiektem Zadanie (argumenty nazwane, id z nastepneIdZadania(), wykonane = false, xp jako (int) $xpZadania).
Sprawdź: dodaj przez formularz zadanie „Test OOP”, termin za tydzień, 25 XP. Pojawia się komunikat „Zadanie zostało dodane.”, zadanie jest na liście, a ostatnia linia data/zadania.csv ma taki sam format jak linie zadań dodanych w Lekcji 13–14 (wykonane = 0, bez true/false).
ROZWIĄZANIE
a) Fragment zapiszZadania() (wiersz dla jednego zadania):
fputcsv($plik, [
$zadanie->pobierzId(),
$zadanie->pobierzTytul(),
$zadanie->pobierzOpis(),
$zadanie->pobierzTermin(),
$zadanie->czyWykonane() ? 1 : 0,
$zadanie->pobierzXp()
], ';', '"', '');
(U Ciebie zmienna pliku może się nazywać inaczej — zostaw swoją. Końcówka ';', '"', '' bez zmian, jak od Lekcji 11.)
b) W dodaj-zadanie.php, w bloku try:
$zadania = wczytajZadania(PLIK_ZADAN);
$zadania[] = new Zadanie(
id: nastepneIdZadania($zadania),
tytul: $tytulZadania,
opis: $opisZadania,
termin: $terminZadania,
wykonane: false,
xp: (int) $xpZadania
);
zapiszZadania(PLIK_ZADAN, $zadania);
Walidacja formularza, komunikat i przekierowanie — bez zmian.
Punkt kontrolny. Strona główna wygląda tak jak po Lekcji 14 (plus czerwone ramki). Liczniki, XP do zdobycia, najpilniejsze zadanie, trzy widoki listy i dodawanie zadania działają. W całym projekcie nie ma już ani jednego ['tytul'], ['wykonane'], ['xp']… na zadaniu — możesz to sprawdzić wyszukiwaniem w edytorze (Ctrl+Shift+F, szukaj ['wykonane']). Jedyne miejsca z tymi nazwami to: nagłówek CSV w zapiszZadania() i konstruktor klasy.
6. Kolejne zadanie — klasa Uzytkownik
Zadanie jest już obiektem. Teraz to samo z użytkownikiem — tym razem z mniejszą pomocą.
Przypomnij sobie problem z sekcji 2: dane gracza leżą w trzech miejscach, a żeby policzyć jego poziom, trzeba za każdym razem podać funkcji jego własne XP. Obiekt Uzytkownik ma sam trzymać swoje dane i sam odpowiadać na pytania o siebie.
6a. SAMODZIELNIE — classes/Uzytkownik.php
Nowy plik projektu: classes/Uzytkownik.php.
Właściwości (private) — nazwy takie same jak dotychczasowe zmienne w index.php:
| Właściwość | Typ |
|---|---|
$login | string |
$imieUzytkownika | string |
$xp | int |
$streak | int |
$dataOstatniejAktywnosci | string |
Konstruktor — pięć parametrów w tej kolejności.
Metody (public):
| Metoda | Zwraca | Co robi |
|---|---|---|
pobierzLogin() | string | login |
pobierzImie() | string | imię |
pobierzXp() | int | łączne XP |
pobierzStreak() | int | streak |
pobierzPoziom() | int | poziom — tak jak obliczPoziom() dla XP tego użytkownika |
pobierzTytulGracza() | string | tytuł gracza — tak jak pobierzTytul() dla jego poziomu |
pobierzXpWAktualnymPoziomie() | int | tak jak obliczXpWAktualnymPoziomie() |
pobierzProcentPostepu() | float | tak jak obliczProcentPostepu() |
pobierzBrakujaceXp() | int | tak jak ileBrakujeXp() |
dodajXp(int $xp) | void | dodaje XP; gdy $xp ≤ 0 — rzuca Exception z komunikatem Liczba XP do dodania musi być większa od zera. (XP się wtedy nie zmienia) |
aktualizujStreak(bool $zadanieWykonane) | void | zmienia streak użytkownika według zasad z funkcji aktualizujStreak() |
Zasady:
- Nie przepisuj logiki funkcji z
funkcje_grywalizacji.phpifunkcje_terminow.php— wywołaj je w metodach. Reguły poziomów, tytułów i streaka mają zostać w aplikacji w jednym miejscu. (Tak samo w Lekcji 14walidujDaneLogowania()wywoływaławalidujLogin(), zamiast kopiować regex.) - Wewnątrz metody użytkownik nie potrzebuje, żeby ktoś podawał mu XP czy poziom — ma
$this->xp, a poziom może policzyć własną metodą:$this->pobierzPoziom(). $dataOstatniejAktywnoscinie ma gettera — strona jej nie wyświetla. Obiekt potrzebuje jej tylko do liczenia streaka.- Na górze pliku, pod
<?php, dopisz komentarz, z jakich plików korzystają metody — strona, która tworzy obiektUzytkownik, musi je dołączyć.
Sprawdź (brudnopis — zastąp zawartość):
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
$uzytkownik = new Uzytkownik(
login: 'Kama_2026',
imieUzytkownika: 'Kama',
xp: 99,
streak: 5,
dataOstatniejAktywnosci: '2026-09-14'
);
echo "<p>" . $uzytkownik->pobierzImie() . " (" . $uzytkownik->pobierzLogin() . ")</p>";
echo "<p>Poziom " . $uzytkownik->pobierzPoziom() . ", " . $uzytkownik->pobierzTytulGracza() . "</p>";
echo "<p>Brakuje: " . $uzytkownik->pobierzBrakujaceXp() . " XP</p>";
$uzytkownik->dodajXp(30);
echo "<p>Po dodaniu: " . $uzytkownik->pobierzXp() . " XP, poziom " . $uzytkownik->pobierzPoziom() . "</p>";
Oczekiwane: poziom, tytuł i brakujące XP takie same, jakie strona główna TaskQuest pokazywała dla 99 XP; po dodaniu 129 XP i poziom taki, jaki dałaby obliczPoziom(129). Pełne testy — w sekcji 7.
HINT
Gettery i konstruktor piszesz tak jak w Zadanie. Metody liczące mają po jednej linii: return funkcja(...); — tylko zamiast zmiennych z index.php podajesz $this->xp albo $this->pobierzPoziom(). aktualizujStreak() zmienia obiekt: wynik funkcji aktualizujStreak() trzeba zapisać z powrotem do $this->streak. dodajXp() zbudujesz według zwiekszNagrode() z odznaki.
POMOC
public function pobierzPoziom(): int
{
return obliczPoziom($this->xp);
}
public function pobierzTytulGracza(): string
{
return pobierzTytul(/* ... poziom tego użytkownika ... */);
}
public function aktualizujStreak(bool $zadanieWykonane): void
{
$this->streak = aktualizujStreak(/* ... trzy argumenty ... */);
}
W ostatniej metodzie aktualizujStreak występuje dwa razy: raz jako nazwa metody (wywoływanej na obiekcie: $uzytkownik->aktualizujStreak(...)), raz jako nazwa funkcji (wywoływanej bez obiektu). To nie jest metoda, która wywołuje samą siebie — to metoda, która prosi o pomoc funkcję o tej samej nazwie.
ROZWIĄZANIE
Plik projektu: classes/Uzytkownik.php
<?php
// Metody korzystają z funkcji z includes/funkcje_grywalizacji.php i includes/funkcje_terminow.php
// (oraz stałej XP_NA_POZIOM) — strona, która tworzy obiekt Uzytkownik, musi je dołączyć.
class Uzytkownik
{
private string $login;
private string $imieUzytkownika;
private int $xp;
private int $streak;
private string $dataOstatniejAktywnosci;
public function __construct(
string $login,
string $imieUzytkownika,
int $xp,
int $streak,
string $dataOstatniejAktywnosci
) {
$this->login = $login;
$this->imieUzytkownika = $imieUzytkownika;
$this->xp = $xp;
$this->streak = $streak;
$this->dataOstatniejAktywnosci = $dataOstatniejAktywnosci;
}
public function pobierzLogin(): string
{
return $this->login;
}
public function pobierzImie(): string
{
return $this->imieUzytkownika;
}
public function pobierzXp(): int
{
return $this->xp;
}
public function pobierzStreak(): int
{
return $this->streak;
}
public function pobierzPoziom(): int
{
return obliczPoziom($this->xp);
}
public function pobierzTytulGracza(): string
{
return pobierzTytul($this->pobierzPoziom());
}
public function pobierzXpWAktualnymPoziomie(): int
{
return obliczXpWAktualnymPoziomie($this->xp, $this->pobierzPoziom());
}
public function pobierzProcentPostepu(): float
{
return obliczProcentPostepu($this->pobierzXpWAktualnymPoziomie());
}
public function pobierzBrakujaceXp(): int
{
return ileBrakujeXp($this->xp, $this->pobierzPoziom());
}
public function dodajXp(int $xp): void
{
if ($xp <= 0) {
throw new Exception('Liczba XP do dodania musi być większa od zera.');
}
$this->xp += $xp;
}
public function aktualizujStreak(bool $zadanieWykonane): void
{
$this->streak = aktualizujStreak($this->streak, $this->dataOstatniejAktywnosci, $zadanieWykonane);
}
}
Zwróć uwagę na dodajXp(): $xp to parametr (ile dodać), $this->xp — łączne XP użytkownika. Dokładnie ta sama sytuacja co $nazwa i $this->nazwa w konstruktorze odznaki.
Dlaczego jest dodajXp(), a nie ma „ustaw XP”? W grze XP się zdobywa. Metoda, która pozwala tylko dodawać, sprawia, że żaden kod nie odbierze użytkownikowi punktów przez pomyłkę.
6b. SAMODZIELNIE — index.php korzysta z obiektu Uzytkownik
Plik projektu: index.php.
- W bloku
// --- pliki aplikacji ---, podrequire_onceklasyZadanie, dołączclasses/Uzytkownik.php. - Blok
// --- dane użytkownika (z sesji ...) ---(dwie zmienne) i blok// --- dane grywalizacji (symulowane) ---(trzy zmienne) zastąp jednym blokiem, który tworzy obiekt$uzytkownik. Postaw go w miejscu dotychczasowych danych użytkownika — przed danymi zadań. Wartości: login i imię z sesji, XP99, streak5, data'2026-09-14'(te same symulowane wartości co dotąd). Użyj argumentów nazwanych. - W bloku „wywołania funkcji: grywalizacja” pięć wywołań funkcji zastąp metodami obiektu (zmienne
$poziom,$tytul,$xpWAktualnymPoziomie,$procentPostepu,$brakujeXpzostają — zmienia się tylko prawa strona). Zmień też komentarz bloku na// --- wywołania metod: grywalizacja ---. - Linię
$streak = aktualizujStreak(...)zastąp wywołaniem metody. - W prezentacji login, imię, łączne XP i streak czytaj z obiektu.
Efekt: strona wygląda identycznie jak przed tym krokiem. Brak ostrzeżeń Undefined variable.
HINT
Po zmianie w index.php nie ma już zmiennych $imieUzytkownika, $login, $xp, $streak, $dataOstatniejAktywnosci — przeniosły się do obiektu jako właściwości o tych samych nazwach. Jeśli PHP zgłasza Undefined variable $xp albo $streak, to znaczy, że gdzieś w prezentacji zostało stare odwołanie. Pasek postępu ($liczbaAktywnychSegmentow) i próg ($progNastepnegoPoziomu) liczą się dalej z $procentPostepu i $poziom — nic w nich nie zmieniasz.
ROZWIĄZANIE
Pliki aplikacji:
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
// ... pliki z includes/ bez zmian ...
Dane użytkownika (zamiast dwóch dotychczasowych bloków):
// --- użytkownik: dane z sesji + symulowane dane grywalizacji ---
$uzytkownik = new Uzytkownik(
login: $_SESSION['login'],
imieUzytkownika: $_SESSION['imie'],
xp: 99,
streak: 5,
dataOstatniejAktywnosci: '2026-09-14'
);
Grywalizacja:
// --- wywołania metod: grywalizacja ---
$poziom = $uzytkownik->pobierzPoziom();
$tytul = $uzytkownik->pobierzTytulGracza();
$xpWAktualnymPoziomie = $uzytkownik->pobierzXpWAktualnymPoziomie();
$procentPostepu = $uzytkownik->pobierzProcentPostepu();
$brakujeXp = $uzytkownik->pobierzBrakujaceXp();
Streak (pod $zadanieWykonane = ...):
$uzytkownik->aktualizujStreak($zadanieWykonane);
Prezentacja (tylko zmienione linie):
<p>Zalogowano jako <strong><?= $uzytkownik->pobierzLogin(); ?></strong></p>
...
<h2>Witaj, <?= $uzytkownik->pobierzImie(); ?>!</h2>
...
<p>Łączne XP: <strong><?= $uzytkownik->pobierzXp(); ?></strong></p>
...
<p>Streak: <strong><?= $uzytkownik->pobierzStreak(); ?></strong></p>
Dlaczego usuwamy luźne zmienne? Gdyby zostały, ta sama informacja byłaby w dwóch miejscach: np. $streak (5) i streak w obiekcie (po aktualizujStreak() — inny). Wystarczy, że ktoś wyświetli nie tę wersję, i strona pokazuje nieaktualny streak (sprawdzisz to w Eksperymencie 6). Jedno źródło prawdy o użytkowniku = obiekt $uzytkownik.
$poziom, $tytul i reszta zostają jako zmienne — to wyniki policzone raz, do wyświetlenia, a nie dane, które coś zmienia.
Dlaczego nie zapisujemy całego obiektu w sesji? Można, ale wtedy klasa musiałaby być dołączona przed session_start() w każdym pliku, a XP i streak i tak są na razie symulowane. Prościej: w sesji zostają login i imie, a obiekt budujemy przy każdym żądaniu.
7. Testy
Gdzie: testy 1–3 w brudnopis.php (katalog taskquest/, jak w poprzednich lekcjach), testy 4–6 w przeglądarce, na prawdziwej aplikacji (zalogowana/zalogowany). Przed każdym uruchomieniem przewidź wynik.
Test 1 — czyPoTerminie() na granicach
Daty liczymy od dzisiaj, żeby test dawał ten sam wynik niezależnie od dnia, w którym go uruchamiasz:
<?php
require_once __DIR__ . '/classes/Zadanie.php';
$wczoraj = new DateTime('yesterday');
$dzisiaj = new DateTime('today');
$jutro = new DateTime('tomorrow');
$przypadki = [
['niewykonane, termin wczoraj', $wczoraj->format('Y-m-d'), false],
['niewykonane, termin dzisiaj', $dzisiaj->format('Y-m-d'), false],
['niewykonane, termin jutro', $jutro->format('Y-m-d'), false],
['wykonane, termin wczoraj', $wczoraj->format('Y-m-d'), true],
['niewykonane, termin 2020-01-01', '2020-01-01', false],
];
foreach ($przypadki as $przypadek) {
$zadanie = new Zadanie(
id: 1,
tytul: 'Test',
opis: '',
termin: $przypadek[1],
wykonane: $przypadek[2],
xp: 10
);
echo "<p>" . $przypadek[0] . ": " . ($zadanie->czyPoTerminie() ? "po terminie" : "w terminie") . "</p>";
}
$zadanie = new Zadanie(id: 2, tytul: 'Zaległe', opis: '', termin: '2020-01-01', wykonane: false, xp: 10);
$zadanie->oznaczJakoWykonane();
echo "<p>Zaległe po wykonaniu: " . ($zadanie->czyPoTerminie() ? "po terminie" : "w terminie") . "</p>";
| Przypadek | Oczekiwany wynik |
|---|---|
| niewykonane, termin wczoraj | po terminie |
| niewykonane, termin dzisiaj | w terminie |
| niewykonane, termin jutro | w terminie |
| wykonane, termin wczoraj | w terminie |
| niewykonane, termin 2020-01-01 | po terminie |
zaległe po oznaczJakoWykonane() | w terminie |
Ostatni wiersz pokazuje, że metody jednego obiektu współpracują: oznaczJakoWykonane() zmienia stan, a czyPoTerminie() od razu to „widzi”.
Test 2 — Uzytkownik liczy tak samo jak funkcje
Metody użytkownika mają dawać dokładnie te same wyniki co funkcje z funkcje_grywalizacji.php i funkcje_terminow.php — przecież je wywołują. Najpierw przewidź (na podstawie tego, jak działa Twoja obliczPoziom()), jaki poziom i ile brakujących XP wyjdzie dla każdego XP z listy — szczególnie dla wartości granicznych 99/100 i 199/200. Potem uruchom:
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
foreach ([0, 1, 99, 100, 199, 200, 250] as $xp) {
$uzytkownik = new Uzytkownik(
login: 'Test_1',
imieUzytkownika: 'Test',
xp: $xp,
streak: 0,
dataOstatniejAktywnosci: '2026-09-14'
);
$poziom = obliczPoziom($xp);
$xpWPoziomie = obliczXpWAktualnymPoziomie($xp, $poziom);
$zgodne = $uzytkownik->pobierzPoziom() === $poziom
&& $uzytkownik->pobierzTytulGracza() === pobierzTytul($poziom)
&& $uzytkownik->pobierzXpWAktualnymPoziomie() === $xpWPoziomie
&& $uzytkownik->pobierzProcentPostepu() === obliczProcentPostepu($xpWPoziomie)
&& $uzytkownik->pobierzBrakujaceXp() === ileBrakujeXp($xp, $poziom);
echo "<p>XP $xp: poziom " . $uzytkownik->pobierzPoziom()
. ", brakuje " . $uzytkownik->pobierzBrakujaceXp() . " XP — "
. ($zgodne ? "OK" : "BŁĄD") . "</p>";
}
$dzisiaj = new DateTime('today');
$wczoraj = new DateTime('yesterday');
$przedwczoraj = new DateTime('-2 days');
$daty = [$dzisiaj->format('Y-m-d'), $wczoraj->format('Y-m-d'), $przedwczoraj->format('Y-m-d')];
foreach ($daty as $data) {
foreach ([true, false] as $zadanieWykonane) {
$uzytkownik = new Uzytkownik(
login: 'Test_1',
imieUzytkownika: 'Test',
xp: 0,
streak: 5,
dataOstatniejAktywnosci: $data
);
$uzytkownik->aktualizujStreak($zadanieWykonane);
$oczekiwany = aktualizujStreak(5, $data, $zadanieWykonane);
echo "<p>$data, " . ($zadanieWykonane ? "wykonane" : "niewykonane") . ": streak "
. $uzytkownik->pobierzStreak() . " — "
. ($uzytkownik->pobierzStreak() === $oczekiwany ? "OK" : "BŁĄD") . "</p>";
}
}
Oczekiwane: we wszystkich 13 liniach OK. Poziomy i brakujące XP — zgodne z Twoimi przewidywaniami. Jeśli gdzieś jest BŁĄD, metoda nie przekazuje funkcji właściwych danych (np. $this->streak zamiast $this->xp) albo w aktualizujStreak() brakuje zapisu do $this->streak.
Dlaczego testujemy „metoda = funkcja”, a nie wpisujemy wyników na sztywno? Bo funkcje zostały przetestowane w poprzednich lekcjach. Metoda niczego nowego nie liczy — ma tylko poprawnie podać dane. Taki test nazywa się testem regresji: sprawdza, czy po przebudowie wynik się nie zmienił.
Test 3 — dodajXp()
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
$uzytkownik = new Uzytkownik(login: 'Test_1', imieUzytkownika: 'Test', xp: 99, streak: 0, dataOstatniejAktywnosci: '2026-09-14');
$uzytkownik->dodajXp(30);
echo "<p>Po +30: " . $uzytkownik->pobierzXp() . "</p>";
$uzytkownik->dodajXp(1);
echo "<p>Po +1: " . $uzytkownik->pobierzXp() . "</p>";
foreach ([0, -50] as $ile) {
try {
$uzytkownik->dodajXp($ile);
echo "<p>Dodano $ile XP</p>";
} catch (Exception $wyjatek) {
echo "<p>Dla $ile: " . $wyjatek->getMessage() . "</p>";
}
}
echo "<p>Na końcu: " . $uzytkownik->pobierzXp() . " XP</p>";
| Linia | Oczekiwane |
|---|---|
| Po +30 | 129 |
| Po +1 | 130 |
| Dla 0 | Liczba XP do dodania musi być większa od zera. |
| Dla -50 | Liczba XP do dodania musi być większa od zera. |
| Na końcu | 130 XP — nieudane wywołania niczego nie zmieniły |
Żadne „Dodano 0 XP” ani „Dodano -50 XP” nie może się pojawić: wyjątek przerywa metodę, zanim zmieni $this->xp, i przerywa też try przed linią z echo.
Test 4 — aplikacja działa jak po Lekcji 14
W przeglądarce, na oryginalnym pliku zadań:
- Strona główna: liczniki (wszystkie / wykonane / aktywne), XP do zdobycia, komunikat o awansie i najpilniejsze zadanie — takie same jak w
taskquest_lekcja14/uruchomionym tego samego dnia. - Widoki „wszystkie”, „aktywne”, „wykonane” pokazują właściwe zadania, posortowane wg terminu; widok nadal jest pamiętany w ciasteczku.
- Dodaj dwa zadania z formularza. W
data/zadania.csv: nowe linie mająido 1 i o 2 większe od największego wcześniejszego,wykonane=0, format taki sam jak starszych linii. - Formularz z błędami (pusty tytuł, XP
0) — nadal lista błędów, nic nie trafia do pliku. - Test awarii z Lekcji 12: zmień nazwę
data/zadania.csv→ na stronie głównej czerwona ramka „Nie udało się wczytać zadań: …”, reszta strony (poziom, XP, streak) działa. Przywróć nazwę. - Test pustej listy: plik z samym nagłówkiem → „Brak zadań.”, liczniki
0, brak błędów. Przywróć plik.
Test 5 — wyróżnienie zadań po terminie
- Dodaj przez formularz trzy zadania: „Termin wczoraj”, „Termin dziś”, „Termin jutro” (daty ustaw odpowiednio — formularz pozwala na datę z przeszłości).
- Czerwoną ramkę ma tylko „Termin wczoraj” (oraz starsze niewykonane zadania, jeśli są).
- W edytorze kodu zmień w
data/zadania.csvpolewykonanezadania „Termin wczoraj” na1. Po odświeżeniu: zadanie jest wykonane ([X], styl wykonanego zadania) i nie ma czerwonej ramki. - Widok „wykonane” nie pokazuje żadnej czerwonej ramki.
Test 6 — regresja i diagnostyka
Porównaj taskquest/index.php z taskquest_lekcja14/index.php tego samego dnia, na tym samym pliku zadań. Różnice mogą być tylko takie:
- czerwone ramki przy niewykonanych zadaniach po terminie,
- wersja w stopce
1.4→1.5.
Powitanie, login, poziom, tytuł, łączne XP, pasek, próg, brakujące XP, streak — identyczne. diagnostyka.php: 17 × [OK] oraz w przeglądzie katalogów nowy katalog classes z dwoma plikami .php.
Sprzątanie
Gdy testy przeszły i wykonasz eksperymenty z sekcji 8:
- usuń
brudnopis.php(i ewentualne inne brudnopisy), - przywróć
data/zadania.csvz kopii albo zostaw zadania, które chcesz mieć, - sprawdź
index.phpidiagnostyka.php.
8. Obowiązkowe eksperymenty
Gdzie: przy każdym eksperymencie jest napisane, w jakim pliku go wykonać. Po każdym przywróć kod do stanu z sekcji 4–6.
Eksperyment 1: prywatne znaczy prywatne
Brudnopis. Utwórz dowolne zadanie (jak w sekcji 4) i dopisz:
$zadanie->xp = 1000;
Przeczytaj komunikat: Error: Cannot access private property Zadanie::$xp (w różnych wersjach PHP treść może się nieznacznie różnić — ważne, że skrypt się zatrzymuje). Porównaj z sekcją 2: w tablicy Ola ustawiła -500 i nic się nie stało. Tu pomyłka kończy się od razu, w linii, która ją popełniła.
Zamień tę linię na $zadanie->oznaczJakoWykonane(); — działa. Obiekt nie zabrania wszystkiego: pozwala na to, co ma sens w TaskQuest.
Pytanie: czy try { ... } catch (Exception $wyjatek) { ... } złapałoby ten błąd? Sprawdź. (Nie — to Error, nie Exception. To błąd programisty, a nie awaria, którą aplikacja ma obsłużyć — trzeba poprawić kod. O rodzinie Error więcej w Lekcji 23.)
Eksperyment 2: zapomniane $this
Plik projektu: classes/Zadanie.php. W pobierzXp() zmień return $this->xp; na return $xp;. Odśwież index.php.
Dostajesz dwa komunikaty:
Warning: Undefined variable $xp in ...\classes\Zadanie.php on line ...
Fatal error: Uncaught TypeError: Zadanie::pobierzXp(): Return value must be of type int, null returned
Wyjaśnij oba. (Wewnątrz metody $xp to zwykła zmienna lokalna — nie istnieje, więc ma wartość null. Metoda obiecała zwrócić int — typ zwracany wyłapał problem.) Przywróć $this->.
Eksperyment 3: konstruktor, który o czymś zapomniał
Plik projektu: classes/Zadanie.php. W konstruktorze zakomentuj linię $this->opis = $opis;. Odśwież index.php.
Fatal error: Uncaught Error: Typed property Zadanie::$opis must not be accessed before initialization
Zwróć uwagę, gdzie jest błąd: nie przy new Zadanie(...), tylko dopiero przy pierwszym odczycie opisu (pobierzOpis() w pętli na stronie głównej). Obiekt powstał niekompletny. Dlatego konstruktor ma ustawiać każdą właściwość. Przywróć linię.
Eksperyment 4: kolejność argumentów
Brudnopis.
<?php
require_once __DIR__ . '/classes/Zadanie.php';
// pomyłka 1: zamienione id i xp (oba int)
$pierwsze = new Zadanie(20, 'Test', 'Opis zadania', '2026-10-01', false, 1);
echo "<p>id: " . $pierwsze->pobierzId() . ", xp: " . $pierwsze->pobierzXp() . "</p>";
// pomyłka 2: zamienione opis i termin (oba string)
$drugie = new Zadanie(1, 'Test', '2026-10-01', 'Opis zadania', false, 20);
echo "<p>termin: " . $drugie->pobierzTermin() . "</p>";
var_dump($drugie->czyPoTerminie());
Pomyłka 1: żadnego błędu — zadanie ma id 20 i nagrodę 1 XP. Nikt tego nie zauważy, dopóki nie zabraknie komuś punktów. Pomyłka 2: obiekt powstaje bez protestu (termin to „jakiś tekst”), a błąd wybucha dopiero w czyPoTerminie() — DateTime nie umie odczytać daty z napisu Opis zadania (wyjątek DateMalformedStringException w PHP 8.3+, zwykły Exception w starszych wersjach).
Teraz przepisz oba wywołania na argumenty nazwane, celowo w pomieszanej kolejności, np.:
$pierwsze = new Zadanie(xp: 20, id: 1, tytul: 'Test', opis: 'Opis zadania', termin: '2026-10-01', wykonane: false);
Wszystko trafia na swoje miejsce. Dlatego w TaskQuest obiekty z wieloma parametrami tworzymy przez argumenty nazwane.
Eksperyment 5: obiekt to nie kopia
Brudnopis.
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
$zadania = wczytajZadania(PLIK_ZADAN);
$aktywneZadania = pobierzAktywneZadania($zadania);
echo "<p>Wykonane przed: " . count(pobierzWykonaneZadania($zadania)) . "</p>";
foreach ($aktywneZadania as $zadanie) {
$zadanie->oznaczJakoWykonane();
}
echo "<p>Wykonane po: " . count(pobierzWykonaneZadania($zadania)) . "</p>";
Przewidź: czy zmiana zadań z odfiltrowanej tablicy $aktywneZadania zmieni coś w oryginalnej tablicy $zadania?
Zmienia! Po pętli wszystkie zadania w $zadania są wykonane. array_filter() zrobiło nową tablicę, ale nie nowe obiekty — obie tablice wskazują te same obiekty. Zmienna z obiektem działa jak numerek w szatni z Lekcji 14: kopiujesz numerek, a kurtka jest jedna. Z tablicami tablic (Lekcja 14) było inaczej — każda tablica miała własną kopię danych.
Sprawdź też data/zadania.csv — nic się nie zmieniło. Obiekty żyją w pamięci tylko do końca skryptu; do pliku trafia to, co przekażemy do zapiszZadania().
To zachowanie bywa wygodne (zadanie końcowe A: znajdujesz obiekt na liście, oznaczasz go i zapisujesz całą listę) i bywa pułapką (zmieniasz „kopię”, a zmienia się oryginał). Jeśli naprawdę potrzebujesz niezależnej kopii obiektu, służy do tego clone: $kopia = clone $zadanie;.
Eksperyment 6: metoda, która nie zapisała wyniku
Plik projektu: classes/Uzytkownik.php. W aktualizujStreak() usuń $this->streak = (zostaw samo wywołanie funkcji). Uruchom Test 2 z sekcji 7.
Linie ze streakiem pokazują BŁĄD tam, gdzie funkcja zmieniłaby streak — w obiekcie zostaje 5. Funkcja aktualizujStreak() zwraca nową wartość, ale sama niczego nie zmienia; jeśli wyniku nie zapiszemy w obiekcie, przepada.
Teraz pomyśl o index.php z Lekcji 14: tam było $streak = aktualizujStreak($streak, ...). Gdybyśmy dziś zostawili w index.php zmienną $streak obok obiektu, na stronie mógłby pojawić się streak sprzed aktualizacji — dwie wersje tej samej informacji, i tylko jedna prawdziwa. Dlatego w sekcji 6b usunęliśmy luźne zmienne. Przywróć $this->streak = .
Eksperyment 7 (dodatkowy): czego typy nie wyłapią
Brudnopis.
<?php
require_once __DIR__ . '/classes/Zadanie.php';
$zadanie = new Zadanie(id: '7', tytul: 'Test', opis: '', termin: '2026-10-01', wykonane: 'nie', xp: 10);
var_dump($zadanie->pobierzId(), $zadanie->czyWykonane());
$zadanie = new Zadanie(id: 'siedem', tytul: 'Test', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);
Wynik: int(7), bool(true), a potem TypeError: ... Argument #1 ($id) must be of type int, string given.
PHP w domyślnym trybie zamienia wartości, gdy się da: tekst '7' na liczbę 7, niepusty tekst 'nie' na true (jak w if z sekcji 2). Odrzuca tylko to, czego zamienić się nie da ('siedem' na int). Wniosek: typy właściwości i parametrów bardzo pomagają, ale nie zastępują walidacji. Dlatego dane z pliku nadal przechodzą przez sprawdzWierszZadania(), dane z formularza — przez walidujNoweZadanie(), a konwersje ((int), === '1') robimy jawnie, zanim powstanie obiekt.
9. ROZWIĄZANIE
Pełny kod TaskQuest po Lekcji 15. Nowe: classes/Zadanie.php, classes/Uzytkownik.php. Zmienione: index.php, dodaj-zadanie.php, includes/funkcje_zadan.php, includes/funkcje_plikow.php, includes/header.php, includes/footer.php, diagnostyka.php. Bez zmian względem Lekcji 14: zaloguj.php, wyloguj.php, config/config.php, includes/funkcje_grywalizacji.php, includes/funkcje_tekstowe.php, includes/funkcje_terminow.php, includes/funkcje_sesji.php, includes/funkcje_diagnostyczne.php, data/zadania.csv.
ROZWIĄZANIE
classes/Zadanie.php
<?php
class Zadanie
{
private int $id;
private string $tytul;
private string $opis;
private string $termin;
private bool $wykonane;
private int $xp;
public function __construct(
int $id,
string $tytul,
string $opis,
string $termin,
bool $wykonane,
int $xp
) {
$this->id = $id;
$this->tytul = $tytul;
$this->opis = $opis;
$this->termin = $termin;
$this->wykonane = $wykonane;
$this->xp = $xp;
}
public function pobierzId(): int
{
return $this->id;
}
public function pobierzTytul(): string
{
return $this->tytul;
}
public function pobierzOpis(): string
{
return $this->opis;
}
public function pobierzTermin(): string
{
return $this->termin;
}
public function czyWykonane(): bool
{
return $this->wykonane;
}
public function pobierzXp(): int
{
return $this->xp;
}
public function oznaczJakoWykonane(): void
{
$this->wykonane = true;
}
public function czyPoTerminie(): bool
{
if ($this->wykonane) {
return false;
}
$dzisiaj = new DateTime('today');
$termin = new DateTime($this->termin);
return $termin < $dzisiaj;
}
}
classes/Uzytkownik.php
<?php
// Metody korzystają z funkcji z includes/funkcje_grywalizacji.php i includes/funkcje_terminow.php
// (oraz stałej XP_NA_POZIOM) — strona, która tworzy obiekt Uzytkownik, musi je dołączyć.
class Uzytkownik
{
private string $login;
private string $imieUzytkownika;
private int $xp;
private int $streak;
private string $dataOstatniejAktywnosci;
public function __construct(
string $login,
string $imieUzytkownika,
int $xp,
int $streak,
string $dataOstatniejAktywnosci
) {
$this->login = $login;
$this->imieUzytkownika = $imieUzytkownika;
$this->xp = $xp;
$this->streak = $streak;
$this->dataOstatniejAktywnosci = $dataOstatniejAktywnosci;
}
public function pobierzLogin(): string
{
return $this->login;
}
public function pobierzImie(): string
{
return $this->imieUzytkownika;
}
public function pobierzXp(): int
{
return $this->xp;
}
public function pobierzStreak(): int
{
return $this->streak;
}
public function pobierzPoziom(): int
{
return obliczPoziom($this->xp);
}
public function pobierzTytulGracza(): string
{
return pobierzTytul($this->pobierzPoziom());
}
public function pobierzXpWAktualnymPoziomie(): int
{
return obliczXpWAktualnymPoziomie($this->xp, $this->pobierzPoziom());
}
public function pobierzProcentPostepu(): float
{
return obliczProcentPostepu($this->pobierzXpWAktualnymPoziomie());
}
public function pobierzBrakujaceXp(): int
{
return ileBrakujeXp($this->xp, $this->pobierzPoziom());
}
public function dodajXp(int $xp): void
{
if ($xp <= 0) {
throw new Exception('Liczba XP do dodania musi być większa od zera.');
}
$this->xp += $xp;
}
public function aktualizujStreak(bool $zadanieWykonane): void
{
$this->streak = aktualizujStreak($this->streak, $this->dataOstatniejAktywnosci, $zadanieWykonane);
}
}
includes/funkcje_zadan.php
Pięć funkcji po zmianie (jeśli Twoje funkcje z Lekcji 9 i 13 są zapisane inaczej, np. funkcją strzałkową, zmieniają się w nich tylko odwołania z tabeli w kroku 3). walidujNoweZadanie() — bez zmian.
<?php
function pobierzWykonaneZadania(array $zadania): array
{
return array_filter($zadania, function ($zadanie) {
return $zadanie->czyWykonane();
});
}
function pobierzAktywneZadania(array $zadania): array
{
return array_filter($zadania, function ($zadanie) {
return !$zadanie->czyWykonane();
});
}
function sumujXp(array $zadania): int
{
$suma = 0;
foreach ($zadania as $zadanie) {
$suma += $zadanie->pobierzXp();
}
return $suma;
}
function sortujPoTerminie(array $zadania): array
{
usort($zadania, function ($a, $b) {
return $a->pobierzTermin() <=> $b->pobierzTermin();
});
return $zadania;
}
function nastepneIdZadania(array $zadania): int
{
$najwiekszeId = 0;
foreach ($zadania as $zadanie) {
if ($zadanie->pobierzId() > $najwiekszeId) {
$najwiekszeId = $zadanie->pobierzId();
}
}
return $najwiekszeId + 1;
}
// ... walidujNoweZadanie() — bez zmian względem Lekcji 13 ...
includes/funkcje_plikow.php
sprawdzWierszZadania() — bez zmian. W wczytajZadania() i zapiszZadania() zmieniają się tylko fragmenty pokazane niżej; sprawdzanie pliku, wyjątki, finally, nagłówek CSV i końcówka ';', '"', '' — bez zmian.
W wczytajZadania() — zadanie powstaje jako obiekt:
$zadania[] = new Zadanie(
id: (int) $wiersz[0],
tytul: $wiersz[1],
opis: $wiersz[2],
termin: $wiersz[3],
wykonane: $wiersz[4] === '1',
xp: (int) $wiersz[5]
);
W zapiszZadania() — wiersz CSV dla jednego zadania:
fputcsv($plik, [
$zadanie->pobierzId(),
$zadanie->pobierzTytul(),
$zadanie->pobierzOpis(),
$zadanie->pobierzTermin(),
$zadanie->czyWykonane() ? 1 : 0,
$zadanie->pobierzXp()
], ';', '"', '');
dodaj-zadanie.php
<?php
session_start();
// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
// --- tylko dla zalogowanych ---
wymagajLogowania();
// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';
$bledy = [];
$bladZapisu = null;
// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$tytulZadania = wyczyscTytul($_POST['tytul'] ?? '');
$opisZadania = trim(preg_replace('/\s+/', ' ', $_POST['opis'] ?? ''));
$terminZadania = trim($_POST['termin'] ?? '');
$xpZadania = trim($_POST['xp'] ?? '');
$bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);
if (count($bledy) === 0) {
try {
$zadania = wczytajZadania(PLIK_ZADAN);
$zadania[] = new Zadanie(
id: nastepneIdZadania($zadania),
tytul: $tytulZadania,
opis: $opisZadania,
termin: $terminZadania,
wykonane: false,
xp: (int) $xpZadania
);
zapiszZadania(PLIK_ZADAN, $zadania);
ustawKomunikat('Zadanie zostało dodane.');
header('Location: index.php');
exit;
} catch (Exception $wyjatek) {
$bladZapisu = $wyjatek->getMessage();
}
}
}
// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
<h2>Nowe zadanie</h2>
<p><a href="index.php">← Wróć do listy zadań</a></p>
<?php if (count($bledy) > 0) { ?>
<div class="blad">
<p>Popraw formularz:</p>
<ul>
<?php foreach ($bledy as $blad) { ?>
<li><?= $blad; ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($bladZapisu !== null) { ?>
<p class="blad">Nie udało się dodać zadania: <?= $bladZapisu; ?></p>
<?php } ?>
<form method="post" action="dodaj-zadanie.php">
<label for="tytul">Tytuł *</label>
<input type="text" id="tytul" name="tytul" maxlength="100" required value="<?= $tytulZadania; ?>">
<label for="opis">Opis</label>
<textarea id="opis" name="opis" rows="3" maxlength="300"><?= $opisZadania; ?></textarea>
<label for="termin">Termin *</label>
<input type="date" id="termin" name="termin" required value="<?= $terminZadania; ?>">
<label for="xp">Nagroda XP (1–100) *</label>
<input type="number" id="xp" name="xp" min="1" max="100" required value="<?= $xpZadania; ?>">
<button type="submit">Dodaj zadanie</button>
</form>
<?php
require_once __DIR__ . '/includes/footer.php';
index.php
<?php
session_start();
// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
// --- tylko dla zalogowanych ---
wymagajLogowania();
// --- użytkownik: dane z sesji + symulowane dane grywalizacji ---
$uzytkownik = new Uzytkownik(
login: $_SESSION['login'],
imieUzytkownika: $_SESSION['imie'],
xp: 99,
streak: 5,
dataOstatniejAktywnosci: '2026-09-14'
);
// --- dane zadań (z pliku CSV — tablica obiektów Zadanie) ---
$zadania = [];
$bladZadan = null;
try {
$zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
$bladZadan = $wyjatek->getMessage();
}
// --- wywołania metod: grywalizacja ---
$poziom = $uzytkownik->pobierzPoziom();
$tytul = $uzytkownik->pobierzTytulGracza();
$xpWAktualnymPoziomie = $uzytkownik->pobierzXpWAktualnymPoziomie();
$procentPostepu = $uzytkownik->pobierzProcentPostepu();
$brakujeXp = $uzytkownik->pobierzBrakujaceXp();
//pasek postępu
$liczbaAktywnychSegmentow = (int) ($procentPostepu / 10);
$progNastepnegoPoziomu = $poziom * XP_NA_POZIOM;
// --- wywołania funkcji: lista zadań ---
$wykonaneZadania = pobierzWykonaneZadania($zadania);
$aktywneZadania = pobierzAktywneZadania($zadania);
$liczbaWszystkich = count($zadania);
$liczbaWykonanych = count($wykonaneZadania);
$liczbaAktywnych = count($aktywneZadania);
$xpDoZdobycia = sumujXp($aktywneZadania);
$zadaniaWgTerminu = sortujPoTerminie($zadania);
$aktywneWgTerminu = sortujPoTerminie($aktywneZadania);
$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);
// uproszczenie (symulacja): aktywność = wykonane jakiekolwiek zadanie z listy
$zadanieWykonane = $liczbaWykonanych > 0;
$uzytkownik->aktualizujStreak($zadanieWykonane);
// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = $_GET['filtr'] ?? $_COOKIE['filtr'] ?? 'wszystkie';
if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
$filtr = 'wszystkie';
}
if (isset($_GET['filtr'])) {
setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}
$zadaniaDoWyswietlenia = match ($filtr) {
'wszystkie' => $zadaniaWgTerminu,
'aktywne' => $aktywneWgTerminu,
'wykonane' => $wykonaneWgTerminu
};
// --- komunikat jednorazowy (z sesji) ---
$komunikat = pobierzKomunikat();
// --- prezentacja ---
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
<?php if ($komunikat !== '') { ?>
<p class="sukces"><?= $komunikat; ?></p>
<?php } ?>
<p>Zdobywaj punkty za wykonane zadania!</p>
<p>Zalogowano jako <strong><?= $uzytkownik->pobierzLogin(); ?></strong></p>
<form method="post" action="wyloguj.php">
<button type="submit">Wyloguj</button>
</form>
<h2>Witaj, <?= $uzytkownik->pobierzImie(); ?>!</h2>
<p>Poziom: <strong><?= $poziom; ?></strong></p>
<p>Tytuł: <strong><?= $tytul; ?></strong></p>
<p>Łączne XP: <strong><?= $uzytkownik->pobierzXp(); ?></strong></p>
<h3>
<?= "Masz $xpWAktualnymPoziomie XP z " . XP_NA_POZIOM . " XP potrzebnych do poziomu " . ($poziom + 1) . " (" . round($procentPostepu, 2) . "%)"; ?>
</h3>
<div class="pasek-xp">
<?php
for ($segment = 1; $segment <= 10; $segment++) {
if ($segment <= $liczbaAktywnychSegmentow) {
echo "█";
} else {
echo "░";
}
}
?>
</div>
<p>Kolejny próg: <?= $progNastepnegoPoziomu; ?> XP</p>
<p>Brakuje XP: <strong><?= $brakujeXp; ?></strong></p>
<p>Streak: <strong><?= $uzytkownik->pobierzStreak(); ?></strong></p>
<h2>Twoje zadania</h2>
<p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>
<?php if ($bladZadan !== null) { ?>
<p class="blad">Nie udało się wczytać zadań: <?= $bladZadan; ?></p>
<?php } else { ?>
<p>
Wszystkie: <strong><?= $liczbaWszystkich; ?></strong> |
Wykonane: <strong><?= $liczbaWykonanych; ?></strong> |
Aktywne: <strong><?= $liczbaAktywnych; ?></strong>
</p>
<p>XP do zdobycia z aktywnych zadań: <strong><?= $xpDoZdobycia; ?></strong></p>
<?php
if ($liczbaWszystkich === 0) {
echo "<p>Brak zadań.</p>";
} elseif ($liczbaAktywnych === 0) {
echo "<p>Wszystkie zadania wykonane — świetna robota!</p>";
} else {
if ($xpDoZdobycia >= $brakujeXp) {
echo "<p class=\"awans\">Wykonując aktywne zadania, awansujesz na poziom " . ($poziom + 1) . "!</p>";
} else {
echo "<p>Aktywne zadania nie wystarczą jeszcze na awans — brakuje " . ($brakujeXp - $xpDoZdobycia) . " XP.</p>";
}
$najpilniejsze = $aktywneWgTerminu[0];
echo "<p>Najpilniejsze zadanie: <strong>" . wyczyscTytul($najpilniejsze->pobierzTytul()) . "</strong> — "
. komunikatTerminu($najpilniejsze->pobierzTermin()) . "</p>";
}
echo "<h3>Lista zadań (wg terminu)</h3>";
echo "<p>Pokaż: <a href=\"index.php?filtr=wszystkie\">wszystkie</a> | "
. "<a href=\"index.php?filtr=aktywne\">aktywne</a> | "
. "<a href=\"index.php?filtr=wykonane\">wykonane</a></p>";
echo "<p>Widok: <strong>$filtr</strong></p>";
if ($liczbaWszystkich > 0 && count($zadaniaDoWyswietlenia) === 0) {
echo "<p>Brak zadań w tym widoku.</p>";
}
foreach ($zadaniaDoWyswietlenia as $zadanie) {
$tytulCzysty = wyczyscTytul($zadanie->pobierzTytul());
$znacznik = $zadanie->czyWykonane() ? "[X]" : "[ ]";
$klasa = $zadanie->czyWykonane() ? "zadanie wykonane" : "zadanie";
if ($zadanie->czyPoTerminie()) {
$klasa = "zadanie po-terminie";
}
echo "<div class=\"$klasa\">";
echo "<p><strong>$znacznik $tytulCzysty</strong> — " . $zadanie->pobierzXp() . " XP</p>";
echo "<p>" . skrocOpis($zadanie->pobierzOpis(), 50) . "</p>";
echo "<p>Termin: " . $zadanie->pobierzTermin();
if (!$zadanie->czyWykonane()) {
echo " — " . komunikatTerminu($zadanie->pobierzTermin());
}
echo "</p>";
if (stripos($tytulCzysty, 'PHP') !== false) {
echo "<p>To zadanie dotyczy PHP.</p>";
}
echo "</div>";
}
?>
<?php } ?>
<?php
require_once __DIR__ . '/includes/footer.php';
includes/header.php
Jedyna zmiana: w <style>, pod regułą .sukces, dochodzi:
.zadanie.po-terminie { border: 2px solid #c0392b; }
includes/footer.php
<p>TaskQuest 1.5</p>
</div>
</body>
</html>
diagnostyka.php
Dwie zmiany: w tablicy $wymaganePliki dochodzą 'classes/Zadanie.php' i 'classes/Uzytkownik.php' (razem 17 ścieżek), a $katalogiDoPrzegladu to ['config', 'includes', 'classes']. Reszta pliku bez zmian — nadal bez sesji i bez dołączania klas czy funkcji aplikacji (ma działać nawet przy zepsutym projekcie).
10. Co zmieniliśmy w TaskQuest?
Zostaje
$poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp // te same nazwy — wartości z metod obiektu $uzytkownik
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu, $zadanieWykonane
$zadania, $bladZadan, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $wykonaneWgTerminu, $xpDoZdobycia // tablice — teraz z obiektami Zadanie
$filtr, $zadaniaDoWyswietlenia, $komunikat, $nazwaStrony
// dodaj-zadanie.php: $tytulZadania, $opisZadania, $terminZadania, $xpZadania, $bledy, $bladZapisu
// zaloguj.php: $login, $imieUzytkownika (dane z formularza — bez zmian)
define('XP_NA_POZIOM', ...), define('PLIK_ZADAN', ...), define('CZAS_PAMIETANIA_WIDOKU', ...)
wszystkie funkcje z Lekcji 1–14 (kod bez zmian poza odwołaniami do danych zadania w funkcje_zadan.php i funkcje_plikow.php)
format data/zadania.csv, sesja (login, imie, komunikat), cookie filtr, obsługa awarii pliku, walidacja formularzy
Dochodzi
// classes/Zadanie.php (nowy plik, nowy katalog classes/)
class Zadanie
// właściwości private: $id, $tytul, $opis, $termin, $wykonane, $xp
__construct(int $id, string $tytul, string $opis, string $termin, bool $wykonane, int $xp)
pobierzId(), pobierzTytul(), pobierzOpis(), pobierzTermin(), czyWykonane(), pobierzXp()
oznaczJakoWykonane() // jedyny sposób zmiany stanu zadania
czyPoTerminie() // niewykonane i termin przed dzisiaj
// classes/Uzytkownik.php (nowy plik)
class Uzytkownik
// właściwości private: $login, $imieUzytkownika, $xp, $streak, $dataOstatniejAktywnosci
__construct(string $login, string $imieUzytkownika, int $xp, int $streak, string $dataOstatniejAktywnosci)
pobierzLogin(), pobierzImie(), pobierzXp(), pobierzStreak()
pobierzPoziom(), pobierzTytulGracza(), pobierzXpWAktualnymPoziomie(), pobierzProcentPostepu(), pobierzBrakujaceXp()
// wywołują funkcje z funkcje_grywalizacji.php
dodajXp(int $xp) // tylko dodatnie XP, inaczej Exception
aktualizujStreak(bool $zadanieWykonane) // wywołuje funkcję aktualizujStreak() i zapisuje wynik w obiekcie
// index.php
$uzytkownik // obiekt Uzytkownik (dane z sesji + symulowane XP/streak/data)
// includes/header.php: styl .zadanie.po-terminie
Nowe narzędzia PHP: class, właściwości z typami, new, ->, metody, $this, __construct, public, private, argumenty nazwane (nazwa: wartość), porównywanie obiektów DateTime operatorem <.
Zmieniamy
includes/funkcje_plikow.php:wczytajZadania()zwraca tablicę obiektówZadanie,zapiszZadania()odczytuje dane metodami (format pliku bez zmian),includes/funkcje_zadan.php: w pięciu funkcjach$zadanie['...']→ metody (logika bez zmian),index.php: dołączone klasy; obiekt$uzytkownik; grywalizacja liczona metodami; streak aktualizowany metodą; lista zadań i najpilniejsze zadanie czytane metodami; klasa CSSzadanie po-terminie,dodaj-zadanie.php: dołączona klasaZadanie, nowe zadanie tworzone przeznew Zadanie(...),includes/header.php,includes/footer.php(TaskQuest 1.5),diagnostyka.php(17 plików, katalogclasses).
Usuwamy
- Tablica z kluczami
'id','tytul', … jako zadanie — zastąpiona obiektemZadanie. Tablica pozwalała na literówki w kluczach, brak kluczy i dowolne zmiany wartości; obiekt ma zawsze komplet danych i pozwala tylko naoznaczJakoWykonane(). - Zmienne
$imieUzytkownika,$loginwindex.phporaz$xp,$streak,$dataOstatniejAktywnosci— przeniesione do obiektu$uzytkownikjako właściwości o tych samych nazwach. Zostawienie ich obok obiektu oznaczałoby dwie wersje tych samych danych (Eksperyment 6). Wzaloguj.phpzmienne$logini$imieUzytkownikazostają (dane z formularza przed zapisem do sesji). - Linia
$streak = aktualizujStreak(...)— zastąpiona przez$uzytkownik->aktualizujStreak($zadanieWykonane). FunkcjaaktualizujStreak()zostaje i jest używana przez metodę.
Co nowego potrafi TaskQuest po tej lekcji?
Po Lekcji 15 zadanie i użytkownik są w TaskQuest obiektami. Zadanie zawsze ma komplet danych (konstruktor), jego dane są ukryte (
private) i można je tylko odczytać albo oznaczyć zadanie jako wykonane; samo wie, czy jest po terminie — a strona główna wyróżnia takie zadania czerwoną ramką. Użytkownik sam zna swój poziom, tytuł, postęp i brakujące XP, aktualizuje swój streak i pozwala tylko dodawać XP (nigdy odejmować). Plik CSV, sesja, formularze i widoki listy działają tak jak po Lekcji 14 — zmieniła się budowa aplikacji, a nie jej zachowanie.
Do samodzielnego sprawdzenia
[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię wyjaśnić różnicę między klasą a obiektem na przykładzie Zadanie.
[ ] Wiem, co oznacza $this i dlaczego metoda pobierzXp() nie potrzebuje parametru.
[ ] Wiem, po co jest konstruktor i co się dzieje, gdy nie ustawi którejś właściwości.
[ ] Potrafię wyjaśnić, po co właściwości są private, a metody public.
[ ] Wiem, dlaczego Zadanie ma oznaczJakoWykonane(), a nie ma „ustaw XP”.
[ ] Potrafię utworzyć obiekt z argumentami nazwanymi i wiem, przed jaką pomyłką chronią.
[ ] Mam classes/Zadanie.php z czyPoTerminie(); Test 1 się zgadza.
[ ] Mam classes/Uzytkownik.php; Test 2 i Test 3 dają same OK / oczekiwane wartości.
[ ] W całym projekcie nie ma już odwołań $zadanie['...'].
[ ] Strona wygląda jak po Lekcji 14, zadania po terminie mają czerwoną ramkę (Testy 4–6).
[ ] diagnostyka.php pokazuje 17 razy [OK].
[ ] Potrafię wyjaśnić, co pokazał Eksperyment 5 (obiekt to nie kopia) i Eksperyment 7 (typy a walidacja).
[ ] Usunęłam/usunąłem brudnopisy.
Pytania kontrolne
Czym różni się klasa od obiektu? Ile klas
Zadanie, a ile obiektówZadanieistnieje w TaskQuest, gdy w pliku CSV jest 5 zadań?W metodzie
dodajXp(int $xp)występują$xpi$this->xp. Co oznacza każde z nich? Co by się stało, gdyby w metodzie napisać$xp += $xp;?Przewidź wynik:
$a = new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);Czy wynik byłby taki sam, gdyby
$b = $a;
$b->oznaczJakoWykonane();
var_dump($a->czyWykonane());$abyła tablicą, a zamiast metody —$b['wykonane'] = true;?Kolega proponuje: „Zróbmy wszystkie właściwości
Zadaniejakopublic, będzie mniej pisania — nie trzeba getterów”. Podaj dwa konkretne problemy z sekcji 2, które wtedy wrócą.Dlaczego
Uzytkownik::pobierzPoziom()wywołuje funkcjęobliczPoziom(), zamiast mieć w sobie skopiowaną pętlęwhile? Co by się stało, gdyby kiedyś zmieniła się stałaXP_NA_POZIOMalbo sposób liczenia poziomu?W kodzie jest błąd, przez który strona pokazuje zły streak. Znajdź go:
public function aktualizujStreak(bool $zadanieWykonane): void
{
aktualizujStreak($this->streak, $this->dataOstatniejAktywnosci, $zadanieWykonane);
}Zadanie ma termin na dzisiaj. Co zwróci
czyPoTerminie()? Jak zmieniłby się wynik, gdyby w metodzie byłonew DateTime()zamiastnew DateTime('today')? Kiedy (o której godzinie) byłoby to widać?Po zmianie
wczytajZadania()(krok 2) strona pokazałaCannot use object of type Zadanie as array in ...funkcje_zadan.php. Dlaczego błąd pojawił się wfunkcje_zadan.php, a nie wfunkcje_plikow.php, gdzie wprowadziliśmy zmianę?Dlaczego
new Zadanie(id: '7', ..., wykonane: 'nie', ...)nie zgłasza błędu i co z tego wynika dla walidacji danych z pliku i formularza (Eksperyment 7)?Po co w
index.phpzostały zmienne$poziomi$tytul, skoro można w prezentacji pisać$uzytkownik->pobierzPoziom()? A dlaczego usunęliśmy$streak?Dlaczego obiekt
Uzytkownikbudujemy windex.phpprzy każdym żądaniu, zamiast zapisać go w sesji?
Zadanie końcowe — rozbuduj TaskQuest samodzielnie
Wybierz jedno (albo więcej):
A) „Zrobione!” — obiekty w akcji. Przy każdym niewykonanym zadaniu na liście dodaj mały formularz method="post" action="oznacz-wykonane.php" z ukrytym polem id i przyciskiem „Zrobione!”. Nowy plik oznacz-wykonane.php (bez własnego HTML, jak wyloguj.php):
- tylko POST i tylko dla zalogowanych,
idz formularza sprawdź regexem/^\d+$/przed(int)(reguła z Lekcji 13),- wczytaj zadania, znajdź na liście obiekt o tym
id(pobierzId()), wywołaj na nimoznaczJakoWykonane()i zapisz całą listę (zapiszZadania()) — wszystko w jednymtry, jak wdodaj-zadanie.php, - ustaw komunikat jednorazowy
Zadanie wykonane! +N XP., gdzieNpochodzi zpobierzXp()znalezionego zadania, i przekieruj naindex.php, - gdy zadania o takim
idnie ma — komunikatNie znaleziono zadania..
Pytania do przemyślenia: dlaczego wystarczy zmienić obiekt znaleziony w pętli, żeby zmiana trafiła do zapisywanej tablicy $zadania (Eksperyment 5)? Łączne XP użytkownika na stronie nie wzrośnie — dlaczego? Co musiałoby się stać, żeby $uzytkownik->dodajXp() miało trwały efekt? (Odpowiedź przyniesie baza danych.)
Jeśli robiłaś/robiłeś zadanie końcowe A z Lekcji 13 lub C z Lekcji 14 — przerób istniejący oznacz-wykonane.php zamiast pisać od nowa.
B) Własny wyjątek — dziedziczenie w pigułce. Klasa może dziedziczyć po innej klasie — przejąć wszystkie jej właściwości i metody i dodać coś od siebie. Zapisuje się to słowem extends:
class BladPlikuZadan extends Exception
{
}
Taka pusta klasa to pełnoprawny wyjątek: ma getMessage() i wszystko inne, co ma Exception — bo jest „rodzajem Exception”. Zadanie:
- Utwórz
classes/BladPlikuZadan.phpz powyższą klasą. - W
wczytajZadania()izapiszZadania()zamieńthrow new Exception(...)nathrow new BladPlikuZadan(...)(komunikaty bez zmian); dołącz nową klasę wszędzie, gdzie dołączaszfunkcje_plikow.php. - Sprawdź test awarii z Lekcji 12 (zmiana nazwy pliku) — czy
catch (Exception $wyjatek)windex.phpnadal łapie wyjątek? Wyjaśnij dlaczego. - W brudnopisie napisz
tryz dwoma blokamicatch: najpierwcatch (BladPlikuZadan $wyjatek), potemcatch (Exception $wyjatek), i sprawdź, który zadziała dlathrow new BladPlikuZadan('x'), a który dlathrow new Exception('y'). Do czego może się przydać rozróżnianie rodzajów awarii?
Nie zapomnij o diagnostyka.php (18 plików).
C) Zadanie wie, czego dotyczy. W index.php jest warunek stripos($tytulCzysty, 'PHP') !== false. Przenieś tę wiedzę do klasy: metoda Zadanie::czyDotyczy(string $temat): bool zwraca true, gdy oczyszczony tytuł zawiera podany tekst bez względu na wielkość liter (użyj istniejącej wyczyscTytul()). W index.php zamień warunek na $zadanie->czyDotyczy('PHP'). Testy: 'PHP', 'php', ' Nauczyć się PHP ', 'Python', pusty tytuł. Pytanie: skoro metoda korzysta z funkcji z funkcje_tekstowe.php, co musi zrobić każda strona, która wywoła czyDotyczy()?
Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory (w B — extends jest wyjaśnione w treści zadania).
Most do Lekcji 17
(Lekcja 16 w naszym kursie jest pominięta — kolejnym etapem TaskQuest jest Lekcja 17.)
Nasze obiekty są porządne, ale krótko żyją: powstają przy każdym żądaniu z pliku CSV i znikają po wysłaniu strony. Spójrz, co nadal jest „na niby”:
new Uzytkownik(..., xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14') ← wpisane w kod, takie samo dla każdego
data/zadania.csv ← jedna lista dla wszystkich użytkowników
zapiszZadania() ← nadpisuje cały plik przy każdej zmianie
Ola i Kama widzą te same zadania i ten sam poziom. dodajXp() działa — ale wynik przepada. W Lekcji 17 zaprojektujemy bazę danych TaskQuest: tabele dla użytkowników i zadań, w których każdy użytkownik ma swoje XP, swój streak i swoje zadania. A nasze klasy się przydadzą: gdy TaskQuest zacznie pobierać dane z bazy, wiersze z wynikiem zapytania zamienimy w obiekty Zadanie i Uzytkownik — dokładnie tak, jak dziś robimy to z wierszami pliku CSV.
PODSUMOWANIE LEKCJI
W skrócie
Klasa i obiekt
- Klasa to plan, obiekt to egzemplarz zbudowany według planu. Z jednej klasy powstaje dowolnie wiele niezależnych obiektów.
- Obiekt tworzy
new NazwaKlasy(...). Do właściwości i metod sięgamy przez->— bez$po strzałce ($zadanie->tytul, nie$zadanie->$tytul). - Nazwa klasy wielką literą, jedna klasa = jeden plik o tej samej nazwie, plik trzeba dołączyć (
require_once), zanim użyjemynew.
Właściwości, metody, $this
- Właściwość to zmienna należąca do obiektu, metoda to funkcja zapisana w klasie i wywoływana na obiekcie.
- Wewnątrz metody do własnych danych sięgamy tylko przez
$this->— samo$xpto zwykła zmienna lokalna, której zwykle nie ma. - Metoda nie potrzebuje parametrów, żeby poznać dane swojego obiektu — dlatego
$uzytkownik->pobierzPoziom()zastępujeobliczPoziom($xp). - Metoda, która ma zmienić stan obiektu, musi zapisać wynik w
$this->...— samo wywołanie funkcji w środku niczego nie zapamiętuje.
Konstruktor
__construct— dwa podkreślniki. PHP wywołuje go automatycznie przynew, dzięki czemu obiekt jest kompletny od chwili powstania.- Brakujący argument to natychmiastowy
ArgumentCountError. Konstruktor, który nie ustawi którejś właściwości (albo nazwany_construct), daje błąd dopiero przy odczycie:must not be accessed before initialization.
Hermetyzacja
- Reguła TaskQuest: właściwości
private, metodypublic. Obiekt ukrywa dane i udostępnia tylko operacje, na które się zgadza. private— dostęp tylko z metod tej klasy;protected— także z klas dziedziczących;public— zewsząd.- Nie każda właściwość potrzebuje gettera, a tym bardziej metody zmieniającej.
ZadaniemaoznaczJakoWykonane(), bo tylko to wolno z nim zrobić — nie ma „ustaw XP” ani „odznacz”. - Nazewnictwo:
pobierz...()zwraca daną,czy...()zwracatrue/false, czasownik (dodajXp(),oznaczJakoWykonane()) zmienia stan.
Rzeczy, które zaskakują
- Obiekt to nie kopia.
$b = $a;dla obiektów daje dostęp do tego samego obiektu — zmiana przez$bwidoczna jest w$a. Dla tablic byłaby to kopia. - Typy właściwości nie zastępują walidacji.
new Zadanie(id: '7', wykonane: 'nie', ...)przejdzie (PHP przekonwertuje tekst naint/bool), więc dane z pliku i formularza nadal trzeba sprawdzać. - Tablica obiektów to nadal tablica.
count(),foreach,array_filter(),usort()działają jak dotąd — zmienia się tylko odczyt elementu:$element['xp']→$element->pobierzXp(). - Potraktowanie obiektu jak tablicy przerywa skrypt:
Cannot use object of type ... as array.
Poza TaskQuestem (teoria z lekcji)
extends— dziedziczenie, tylko po jednej klasie;finalblokuje nadpisanie metody (lub dziedziczenie klasy).constistaticnależą do klasy, nie do obiektu: w środkuself::, na zewnątrzNazwaKlasy::— bez$this.- Klasa abstrakcyjna — nie da się z niej utworzyć obiektu, może mieć pola i gotowe metody, dziedziczy się ją raz. Interfejs — same deklaracje metod publicznych, ale klasa może implementować wiele interfejsów.
Co trzeba umieć na INF.03?
- czytać i pisać zapis
new Klasa(...)oraz$obiekt->metoda()— bez tego nie da się użyćDateTime, wyjątków ani obiektowego połączenia z bazą, - rozpoznać w kodzie, czy dana rzecz jest funkcją (
funkcja($x)), metodą obiektu ($obiekt->metoda()), czy elementem klasy (Klasa::STALA,Klasa::metoda()), - odczytać dane z obiektu zwróconego przez bibliotekę i wypisać je na stronie (to samo, co robimy z wierszami z bazy),
- poprawnie łączyć podejście proceduralne i obiektowe — na egzaminie wolno pisać skrypty proceduralnie, byle działały i miały znaczące nazwy zmiennych,
- rozpoznać typowe komunikaty błędów (
Class "..." not found,Cannot access private property,Cannot use object of type ... as array) i wiedzieć, co je powoduje — to oszczędza czas przy debugowaniu na egzaminie praktycznym.
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
- Klasa to plan, obiekt to egzemplarz.
newtworzy obiekt,->sięga do jego właściwości i metod (po strzałce nie ma$). $thisto „ten obiekt”. Wewnątrz metody zawsze$this->wlasciwosc.__construct(dwa podkreślniki) wykonuje się automatycznie przynewi ma zrobić obiekt kompletnym.- Właściwości
private, metodypublic. Dane czytamy getterem, zmieniamy tylko metodą, która pilnuje reguł. - Argumenty nazwane (
new Zadanie(id: 1, tytul: 'A', ...)) przy wielu parametrach — kolejność przestaje mieć znaczenie. - Obiekt przypisany do drugiej zmiennej to ten sam obiekt, nie kopia.
- Typ właściwości to nie walidacja — dane z pliku i formularza nadal sprawdzamy sami.
- Klasa musi być dołączona (
require_once), inaczejClass "..." not found. extends— jedna klasa bazowa;implements— wiele interfejsów.constistaticwywołujemy przezself::(w klasie) lubNazwaKlasy::(poza nią) — nie przez$this.
Ściągawka – najważniejsze polecenia i składnia
Klasa, obiekt, właściwości
class Odznaka
{
private string $nazwa; // właściwość z typem, tylko dla metod tej klasy
private int $xp = 0; // wartość domyślna
public function __construct(string $nazwa, int $xp)
{
$this->nazwa = $nazwa; // $nazwa — parametr, $this->nazwa — właściwość obiektu
$this->xp = $xp;
}
public function pobierzXp(): int // getter
{
return $this->xp;
}
public function czyCenna(): bool // metoda zwracająca true/false
{
return $this->xp >= 50;
}
public function zwiekszNagrode(int $dodatkoweXp): void // zmiana stanu wg reguły
{
if ($dodatkoweXp <= 0) {
throw new Exception('Nagrodę można tylko zwiększyć.');
}
$this->xp += $dodatkoweXp;
}
}
$odznaka = new Odznaka('Streak 7 dni', 50);
$odznaka = new Odznaka(nazwa: 'Streak 7 dni', xp: 50); // argumenty nazwane (PHP 8)
echo $odznaka->pobierzXp();
Modyfikatory dostępu
| Modyfikator | Dostęp z metod klasy | Z klasy dziedziczącej | Spoza klasy |
|---|---|---|---|
public | tak | tak | tak |
protected | tak | tak | nie |
private | tak | nie | nie |
Obiekty w tablicy
$odznaki = [new Odznaka('A', 10), new Odznaka('B', 50)];
$suma = 0;
foreach ($odznaki as $odznaka) {
$suma += $odznaka->pobierzXp(); // było: $odznaka['xp']
}
Klasa w osobnym pliku
require_once __DIR__ . '/classes/Zadanie.php';
$zadanie = new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);
Dziedziczenie i final
class Postac { protected int $hp = 100; public function czyMartwy(): bool { return $this->hp <= 0; } }
class Wojownik extends Postac { } // dziedziczy hp i czyMartwy()
class Profil { final public function nazwa(): string { return 'Użytkownik'; } }
// klasa potomna nie nadpisze nazwa() — Fatal error: Cannot override final method
Stałe i elementy statyczne
class Konfiguracja
{
const MAX_GRACZY = 100;
public static int $licznik = 0;
public function limit(): int { return self::MAX_GRACZY; } // w klasie: self::
}
echo Konfiguracja::MAX_GRACZY; // poza klasą: NazwaKlasy::
Konfiguracja::$licznik++; // bez tworzenia obiektu
Klasa abstrakcyjna i interfejs
abstract class PostacGry {
abstract public function atak(): string; // brak ciała — musi pojawić się w klasie potomnej
}
interface Zniszczalny {
public function znisz(): void; // zawsze public, zawsze bez ciała
}
class Mag extends PostacGry implements Zniszczalny { /* musi mieć atak() i znisz() */ }
Metody magiczne
public function __construct() { } // przy new
public function __destruct() { } // gdy obiekt przestaje istnieć (unset() lub koniec skryptu)
Komunikaty błędów i ich przyczyny
| Komunikat | Przyczyna |
|---|---|
Class "Zadanie" not found | brak require_once pliku klasy |
Cannot access private property Zadanie::$tytul | odwołanie do private spoza klasy — użyj gettera |
Cannot use object of type Zadanie as array | $zadanie['tytul'] zamiast $zadanie->pobierzTytul() |
Too few arguments to function ...::__construct() | brakujący argument przy new |
must not be accessed before initialization | konstruktor nie ustawił właściwości albo nazwany _construct |
Undefined variable $xp w metodzie | brak $this-> |
Cannot override final method | próba nadpisania metody final |
Quiz – sprawdź się
1. Co wyświetli kod?
class Odznaka {
private int $xp = 10;
public function pobierzXp(): int { return $this->xp; }
}
$o = new Odznaka();
echo $o->pobierzXp() + 5;
2. Która z linii A–C spowoduje błąd i jaki?
$zadanie = new Zadanie(id: 1, tytul: 'Nauka', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);
echo $zadanie->pobierzTytul(); // A
echo $zadanie->tytul; // B
echo $zadanie->czyWykonane() ? 'tak' : 'nie'; // C
3. Znajdź błąd. Kiedy się ujawni?
class Odznaka {
private string $nazwa;
public function _construct(string $nazwa) { $this->nazwa = $nazwa; }
public function pobierzNazwe(): string { return $this->nazwa; }
}
$o = new Odznaka('Pierwsze zadanie');
echo $o->pobierzNazwe();
4. Co wyświetli ostatnia linia?
class Licznik {
public static int $ile = 0;
public function __construct() { self::$ile++; }
}
new Licznik();
new Licznik();
echo Licznik::$ile;
5. Uzupełnij metodę klasy Zadanie tak, aby zwracała true dla zadania niewykonanego z nagrodą większą niż zero:
public function czyDoZrobienia(): bool
{
return ______ && ______;
}
6. Dlaczego ta metoda nie działa i jak ją poprawić?
public function opis(): string
{
return $nazwa . ' (+' . $xp . ' XP)';
}
7. Co wyświetli kod? Co się zmieni, jeżeli w klasie Profil dopiszemy final przed public function nazwa()?
class Profil { public function nazwa(): string { return 'Użytkownik'; } }
class ProfilPremium extends Profil { public function nazwa(): string { return 'Premium'; } }
$p = new ProfilPremium();
echo $p->nazwa();
8. Która wersja tworzy obiekt poprawnie?
a) new Zadanie($id: 1, $tytul: 'A', $opis: '', $termin: '2026-10-01', $wykonane: false, $xp: 10);
b) new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);
c) new Zadanie(id => 1, tytul => 'A', opis => '', termin => '2026-10-01', wykonane => false, xp => 10);
9. Ten kod nie działa. Popraw jedną linię:
class Konfiguracja {
const MAX_GRACZY = 100;
public function limit(): int { return $this->MAX_GRACZY; }
}
10. Prawda czy fałsz?
- a) Klasa może implementować wiele interfejsów.
- b) Klasa może dziedziczyć po wielu klasach.
- c) Można utworzyć obiekt klasy abstrakcyjnej.
- d) Metody zadeklarowane w interfejsie są publiczne.
11. Co wypisze ten fragment i dlaczego?
$a = new Odznaka(nazwa: 'A', xp: 10);
$b = $a;
$b->zwiekszNagrode(5);
echo $a->pobierzXp();
12. Po zamianie tablic na obiekty ta pętla przerywa skrypt. Podaj komunikat błędu i popraw kod:
foreach ($zadania as $zadanie) {
echo $zadanie['tytul'] . ' — ' . $zadanie['xp'] . ' XP<br>';
}
13. Zadanie praktyczne. Dopisz do klasy Zadanie metodę czyPilne(int $dni): bool, która zwraca true, gdy zadanie nie jest wykonane, a jego termin przypada najpóźniej za $dni dni (termin na dzisiaj też jest pilny). Wykorzystaj DateTime z Lekcji 7.
Odpowiedzi
15. Właściwość ma wartość domyślną10, a getterpobierzXp()jest publiczny, więc odczyt z zewnątrz jest dozwolony.- Linia B:
Fatal error: Cannot access private property Zadanie::$tytul. Właściwości sąprivate— z zewnątrz dostępne tylko przez metody. _constructma jeden podkreślnik, więc jest zwykłą metodą i PHP nie wywoła jej przynew(argument'Pierwsze zadanie'zostanie po prostu zignorowany). Błąd ujawni się dopiero przy odczycie:Typed property Odznaka::$nazwa must not be accessed before initialization.2. Właściwośćstaticnależy do klasy, nie do obiektu — oba wywołania konstruktora zwiększają ten sam licznik. Wewnątrz klasy odwołujemy się do niej przezself::$ile.return !$this->wykonane && $this->xp > 0;- W metodzie
$nazwai$xpto zmienne lokalne, których nie ma (Undefined variable) — do właściwości obiektu sięgamy przez$this:return $this->nazwa . ' (+' . $this->xp . ' XP)'; - Wyświetli
Premium— klasa potomna nadpisała metodę. Po dodaniufinalw klasie bazowej kod przestanie działać:Fatal error: Cannot override final method Profil::nazwa(). - b). W argumentach nazwanych podajemy nazwę parametru bez
$i po dwukropku;=>służy do tablic, nie do wywołań. constnależy do klasy, a nie do obiektu:return self::MAX_GRACZY;(spoza klasy —Konfiguracja::MAX_GRACZY).- a) prawda, b) fałsz (tylko jedna klasa bazowa), c) fałsz (klasa abstrakcyjna to tylko baza dla potomnych), d) prawda.
- Wypisze
15.$b = $a;nie tworzy kopii obiektu — obie zmienne wskazują ten sam obiekt, więc zmiana wykonana przez$bjest widoczna przez$a. Gdyby$abyło tablicą,$bbyłoby kopią i$apozostałoby bez zmian. Fatal error: Uncaught Error: Cannot use object of type Zadanie as array. Poprawnie:foreach ($zadania as $zadanie) { echo $zadanie->pobierzTytul() . ' — ' . $zadanie->pobierzXp() . ' XP<br>'; }- Przykładowe rozwiązanie:
public function czyPilne(int $dni): bool { if ($this->wykonane) { return false; } $granica = new DateTime('today'); $granica->modify('+' . $dni . ' days'); $termin = new DateTime($this->termin); return $termin <= $granica; }Sprawdź na granicach: termin wczoraj iczyPilne(3)→true(zadanie po terminie też jest pilne), termin dokładnie za 3 dni iczyPilne(3)→true, termin za 4 dni →false, zadanie wykonane → zawszefalse. Tak jak wczyPoTerminie()używamynew DateTime('today'), żeby porównywać daty o tej samej godzinie (00:00), a nie datę z aktualnym czasem.