15. PHP – obiektowo


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
  1. Za pierwszym razem (brak), za drugim ciemny. setcookie() wysyła nagłówek do przeglądarki, a $_COOKIE zawiera to, co przyszło w tym żądaniu — nowe ciasteczko pojawi się tam dopiero w kolejnym.
  2. session_start() jest po wypisaniu HTML, a wysyła nagłówek (ciasteczko PHPSESSID): Warning: Cannot modify header information - headers already sent. Sesję trzeba uruchomić przed jakąkolwiek treścią strony.
  3. 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.
  4. Po pierwsze: brak ?? — przy braku ciasteczka Undefined array key 'rola'. Po drugie: ufanie danym z cookie — użytkownik wpisze sobie admin w F12. O uprawnieniach decyduje sesja, nie ciasteczko.
  5. 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ę.
  6. Sesja: int(99) — sesja zachowuje typy PHP. Cookie: string(2) "99" — wszystko, co przychodzi od przeglądarki, jest tekstem.
  7. Wyświetli 2. Bez session_start() tablica $_SESSION nie zostanie wypełniona danymi z sesji — zapis nie przetrwa żądania i zawsze zobaczymy 1 (a w zależności od konfiguracji także ostrzeżenie o nieuruchomionej sesji).

TaskQuest

Dodatek

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)

Więcej w dokumentacji

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ła MAX_CAPACITY okreś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 $brand i $producedCars:
    • $brand to nazwa marki fabryki, w tym przypadku „Chevrolet”.
    • $producedCars to 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łaby MAX_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 __get i __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 __set zostanie wywołana i wyświetli komunikat.


W naszym przykładzie mamy dwie klasy pochodne:

  • Camaro: Reprezentuje model samochodu Camaro. Klasa ta dziedziczy po klasie ChevroletFactory i dodaje specyficzną właściwość $engineType (typ silnika).

    Konstruktor klasy Camaro wywołuje konstruktor klasy ChevroletFactory za pomocą parent::__construct(), a następnie ustawia właściwość $engineType i wyświetla komunikat o rozpoczęciu produkcji.

  • Corvette: Reprezentuje model samochodu Corvette. Podobnie jak w przypadku Camaro, dziedziczy po klasie ChevroletFactory, ale dodaje właściwość $color (kolor samochodu).

    Konstruktor klasy Corvette również wywołuje konstruktor klasy ChevroletFactory, 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 pomocy echo i metody __toString().
  • 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 pomocy echo i metody __toString().

Podsumowanie:

  • Klasa ChevroletFactory reprezentuje fabrykę samochodów i zawiera metody do produkcji, statusu fabryki oraz metody magiczne do zarządzania właściwościami obiektów.
  • Klasy pochodne (Camaro i Corvette) 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 — obiektem Uzytkownik.

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). Obiekt Uzytkownik jest budowany od nowa przy każdym żądaniu z danych w sesji i tych symulowanych wartości. W sesji nadal trzymamy tylko login i imie — nie cały obiekt.
  • Klasy dostaną metody oznaczJakoWykonane() i dodajXp(). 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:

  1. Dlaczego zwykła zmienna PHP (np. $komunikat) znika po wysłaniu strony, a wartość zapisana w $_SESSION jest dostępna w następnym żądaniu?
  2. W index.php czytamy $_SESSION['imie'] bez ??, a $_COOKIE['filtr'] — z ??. Dlaczego?
  3. Co robi pobierzKomunikat() i dlaczego wywołujemy ją w index.php dokładnie raz?
  4. Od Lekcji 7 piszesz new DateTime(...), a od Lekcji 12 — new Exception(...) i $wyjatek->getMessage(). Co Twoim zdaniem robi słowo new, 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

PlikCo się zmienia
classes/Zadanie.phpnowa klasa: dane zadania + metody (pobierzTytul(), czyWykonane(), czyPoTerminie()…)
classes/Uzytkownik.phpnowa klasa: login, imię, XP, streak + metody (pobierzPoziom(), dodajXp()…)
includes/funkcje_plikow.phpwczytajZadania() zwraca obiekty; zapiszZadania() czyta dane z obiektów
includes/funkcje_zadan.phpodwołania do kluczy tablicy zamienione na metody
index.phpobiekt $uzytkownik; lista zadań czytana metodami; zadania po terminie wyróżnione
dodaj-zadanie.phpnowe zadanie tworzone przez new Zadanie(...)
header.php, footer.php, diagnostyka.phpstyl, numer wersji, lista wymaganych plików

Przygotowanie (zrób to teraz)

  1. Zrób kopię zapasową katalogu taskquest/ z Lekcji 14 — jako taskquest_lekcja14/ (obok, nie w środku). Przyda się do testu regresji.
  2. Zrób kopię data/zadania.csv poza katalogiem projektu.
  3. Zaloguj się i sprawdź, czy index.php działa, a diagnostyka.php pokazuje 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 warunku if jest traktowany jak true.
  • 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:

  1. 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),
  2. 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,
  3. żeby pomyłka w nazwie (tytuł zamiast tytul) 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 — $odznaka i $druga są 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:

ModyfikatorKto ma dostęp
publickażdy kod — także spoza klasy
privatetylko 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 przez zwiekszNagrode() — 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żenia Undefined variable i Undefined property.
  • Brak $this-> w metodzie — return $xp; zamiast return $this->xp;. Wewnątrz metody $xp to zwykła zmienna lokalna, której nie ma (Eksperyment 2).
  • _construct z jednym podkreślnikiem — to wtedy zwykła metoda, PHP jej nie wywoła przy new. 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śćTypZnaczenie
$idintnumer zadania
$tytulstringtytuł (surowy, „brudny” — czyścimy go przy wyświetlaniu, jak dotąd)
$opisstringopis
$terminstringtermin w formacie RRRR-MM-DD
$wykonaneboolczy zadanie jest wykonane
$xpintnagroda 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):

MetodaZwracaCo robi
pobierzId()intnumer zadania
pobierzTytul()stringtytuł
pobierzOpis()stringopis
pobierzTermin()stringtermin
czyWykonane()boolczy zadanie jest wykonane
pobierzXp()intnagroda XP
oznaczJakoWykonane()voidustawia 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(). W funkcje_grywalizacji.php istnieje funkcja pobierzTytul(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 $tytul i $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.

ZadanieczyPoTerminie()
niewykonane, termin wczorajtrue
niewykonane, termin dzisiajfalse
niewykonane, termin jutrofalse
wykonane, termin wczorajfalse

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.

FunkcjaByłoJest
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”).

  1. 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.
  2. Nowość dla użytkownika: zadanie po terminie ma dostać klasę CSS zadanie po-terminie (zamiast zadanie). Użyj czyPoTerminie() — to jedyne miejsce w aplikacji, gdzie ta metoda jest dziś potrzebna.
  3. 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
$loginstring
$imieUzytkownikastring
$xpint
$streakint
$dataOstatniejAktywnoscistring

Konstruktor — pięć parametrów w tej kolejności.

Metody (public):

MetodaZwracaCo robi
pobierzLogin()stringlogin
pobierzImie()stringimię
pobierzXp()intłączne XP
pobierzStreak()intstreak
pobierzPoziom()intpoziom — tak jak obliczPoziom() dla XP tego użytkownika
pobierzTytulGracza()stringtytuł gracza — tak jak pobierzTytul() dla jego poziomu
pobierzXpWAktualnymPoziomie()inttak jak obliczXpWAktualnymPoziomie()
pobierzProcentPostepu()floattak jak obliczProcentPostepu()
pobierzBrakujaceXp()inttak jak ileBrakujeXp()
dodajXp(int $xp)voiddodaje 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)voidzmienia streak użytkownika według zasad z funkcji aktualizujStreak()

Zasady:

  • Nie przepisuj logiki funkcji z funkcje_grywalizacji.php i funkcje_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 14 walidujDaneLogowania() wywoływała walidujLogin(), 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().
  • $dataOstatniejAktywnosci nie 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 obiekt Uzytkownik, 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.

  1. W bloku // --- pliki aplikacji ---, pod require_once klasy Zadanie, dołącz classes/Uzytkownik.php.
  2. 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, XP 99, streak 5, data '2026-09-14' (te same symulowane wartości co dotąd). Użyj argumentów nazwanych.
  3. W bloku „wywołania funkcji: grywalizacja” pięć wywołań funkcji zastąp metodami obiektu (zmienne $poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp zostają — zmienia się tylko prawa strona). Zmień też komentarz bloku na // --- wywołania metod: grywalizacja ---.
  4. Linię $streak = aktualizujStreak(...) zastąp wywołaniem metody.
  5. 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>";
PrzypadekOczekiwany wynik
niewykonane, termin wczorajpo terminie
niewykonane, termin dzisiajw terminie
niewykonane, termin jutrow terminie
wykonane, termin wczorajw terminie
niewykonane, termin 2020-01-01po 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>";
LiniaOczekiwane
Po +30129
Po +1130
Dla 0Liczba XP do dodania musi być większa od zera.
Dla -50Liczba XP do dodania musi być większa od zera.
Na końcu130 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ń:

  1. 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.
  2. Widoki „wszystkie”, „aktywne”, „wykonane” pokazują właściwe zadania, posortowane wg terminu; widok nadal jest pamiętany w ciasteczku.
  3. Dodaj dwa zadania z formularza. W data/zadania.csv: nowe linie mają id o 1 i o 2 większe od największego wcześniejszego, wykonane = 0, format taki sam jak starszych linii.
  4. Formularz z błędami (pusty tytuł, XP 0) — nadal lista błędów, nic nie trafia do pliku.
  5. 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ę.
  6. 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

  1. Dodaj przez formularz trzy zadania: „Termin wczoraj”, „Termin dziś”, „Termin jutro” (daty ustaw odpowiednio — formularz pozwala na datę z przeszłości).
  2. Czerwoną ramkę ma tylko „Termin wczoraj” (oraz starsze niewykonane zadania, jeśli są).
  3. W edytorze kodu zmień w data/zadania.csv pole wykonane zadania „Termin wczoraj” na 1. Po odświeżeniu: zadanie jest wykonane ([X], styl wykonanego zadania) i nie ma czerwonej ramki.
  4. 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:

  1. usuń brudnopis.php (i ewentualne inne brudnopisy),
  2. przywróć data/zadania.csv z kopii albo zostaw zadania, które chcesz mieć,
  3. sprawdź index.php i diagnostyka.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ów Zadanie, 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 CSS zadanie po-terminie,
  • dodaj-zadanie.php: dołączona klasa Zadanie, nowe zadanie tworzone przez new Zadanie(...),
  • includes/header.php, includes/footer.php (TaskQuest 1.5), diagnostyka.php (17 plików, katalog classes).

Usuwamy

  • Tablica z kluczami 'id', 'tytul', … jako zadanie — zastąpiona obiektem Zadanie. Tablica pozwalała na literówki w kluczach, brak kluczy i dowolne zmiany wartości; obiekt ma zawsze komplet danych i pozwala tylko na oznaczJakoWykonane().
  • Zmienne $imieUzytkownika, $login w index.php oraz $xp, $streak, $dataOstatniejAktywnosci — przeniesione do obiektu $uzytkownik jako właściwości o tych samych nazwach. Zostawienie ich obok obiektu oznaczałoby dwie wersje tych samych danych (Eksperyment 6). W zaloguj.php zmienne $login i $imieUzytkownika zostają (dane z formularza przed zapisem do sesji).
  • Linia $streak = aktualizujStreak(...) — zastąpiona przez $uzytkownik->aktualizujStreak($zadanieWykonane). Funkcja aktualizujStreak() 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

  1. Czym różni się klasa od obiektu? Ile klas Zadanie, a ile obiektów Zadanie istnieje w TaskQuest, gdy w pliku CSV jest 5 zadań?

  2. W metodzie dodajXp(int $xp) występują $xp i $this->xp. Co oznacza każde z nich? Co by się stało, gdyby w metodzie napisać $xp += $xp;?

  3. Przewidź wynik:$a = new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);
    $b = $a;
    $b->oznaczJakoWykonane();
    var_dump($a->czyWykonane());
    Czy wynik byłby taki sam, gdyby $a była tablicą, a zamiast metody — $b['wykonane'] = true;?

  4. Kolega proponuje: „Zróbmy wszystkie właściwości Zadanie jako public, będzie mniej pisania — nie trzeba getterów”. Podaj dwa konkretne problemy z sekcji 2, które wtedy wrócą.

  5. 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ła XP_NA_POZIOM albo sposób liczenia poziomu?

  6. 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);
    }

  7. Zadanie ma termin na dzisiaj. Co zwróci czyPoTerminie()? Jak zmieniłby się wynik, gdyby w metodzie było new DateTime() zamiast new DateTime('today')? Kiedy (o której godzinie) byłoby to widać?

  8. Po zmianie wczytajZadania() (krok 2) strona pokazała Cannot use object of type Zadanie as array in ...funkcje_zadan.php. Dlaczego błąd pojawił się w funkcje_zadan.php, a nie w funkcje_plikow.php, gdzie wprowadziliśmy zmianę?

  9. 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)?

  10. Po co w index.php zostały zmienne $poziom i $tytul, skoro można w prezentacji pisać $uzytkownik->pobierzPoziom()? A dlaczego usunęliśmy $streak?

  11. Dlaczego obiekt Uzytkownik budujemy w index.php przy 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,
  • id z 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 nim oznaczJakoWykonane() i zapisz całą listę (zapiszZadania()) — wszystko w jednym try, jak w dodaj-zadanie.php,
  • ustaw komunikat jednorazowy Zadanie wykonane! +N XP., gdzie N pochodzi z pobierzXp() znalezionego zadania, i przekieruj na index.php,
  • gdy zadania o takim id nie ma — komunikat Nie 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:

  1. Utwórz classes/BladPlikuZadan.php z powyższą klasą.
  2. W wczytajZadania() i zapiszZadania() zamień throw new Exception(...) na throw new BladPlikuZadan(...) (komunikaty bez zmian); dołącz nową klasę wszędzie, gdzie dołączasz funkcje_plikow.php.
  3. Sprawdź test awarii z Lekcji 12 (zmiana nazwy pliku) — czy catch (Exception $wyjatek) w index.php nadal łapie wyjątek? Wyjaśnij dlaczego.
  4. W brudnopisie napisz try z dwoma blokami catch: najpierw catch (BladPlikuZadan $wyjatek), potem catch (Exception $wyjatek), i sprawdź, który zadziała dla throw new BladPlikuZadan('x'), a który dla throw 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żyjemy new.

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 $xp to zwykła zmienna lokalna, której zwykle nie ma.
  • Metoda nie potrzebuje parametrów, żeby poznać dane swojego obiektu — dlatego $uzytkownik->pobierzPoziom() zastępuje obliczPoziom($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 przy new, 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, metody public. 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. Zadanie ma oznaczJakoWykonane(), bo tylko to wolno z nim zrobić — nie ma „ustaw XP” ani „odznacz”.
  • Nazewnictwo: pobierz...() zwraca daną, czy...() zwraca true/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 $b widoczna 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 na int/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; final blokuje nadpisanie metody (lub dziedziczenie klasy).
  • const i static należą do klasy, nie do obiektu: w środku self::, na zewnątrz NazwaKlasy:: — 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:

  1. Klasa to plan, obiekt to egzemplarz. new tworzy obiekt, -> sięga do jego właściwości i metod (po strzałce nie ma $).
  2. $this to „ten obiekt”. Wewnątrz metody zawsze $this->wlasciwosc.
  3. __construct (dwa podkreślniki) wykonuje się automatycznie przy new i ma zrobić obiekt kompletnym.
  4. Właściwości private, metody public. Dane czytamy getterem, zmieniamy tylko metodą, która pilnuje reguł.
  5. Argumenty nazwane (new Zadanie(id: 1, tytul: 'A', ...)) przy wielu parametrach — kolejność przestaje mieć znaczenie.
  6. Obiekt przypisany do drugiej zmiennej to ten sam obiekt, nie kopia.
  7. Typ właściwości to nie walidacja — dane z pliku i formularza nadal sprawdzamy sami.
  8. Klasa musi być dołączona (require_once), inaczej Class "..." not found.
  9. extends — jedna klasa bazowa; implements — wiele interfejsów.
  10. const i static wywołujemy przez self:: (w klasie) lub NazwaKlasy:: (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

ModyfikatorDostęp z metod klasyZ klasy dziedziczącejSpoza klasy
publictaktaktak
protectedtaktaknie
privatetaknienie

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

KomunikatPrzyczyna
Class "Zadanie" not foundbrak require_once pliku klasy
Cannot access private property Zadanie::$tytulodwoł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 initializationkonstruktor nie ustawił właściwości albo nazwany _construct
Undefined variable $xp w metodziebrak $this->
Cannot override final methodpró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
  1. 15. Właściwość ma wartość domyślną 10, a getter pobierzXp() jest publiczny, więc odczyt z zewnątrz jest dozwolony.
  2. Linia B: Fatal error: Cannot access private property Zadanie::$tytul. Właściwości są private — z zewnątrz dostępne tylko przez metody.
  3. _construct ma jeden podkreślnik, więc jest zwykłą metodą i PHP nie wywoła jej przy new (argument 'Pierwsze zadanie' zostanie po prostu zignorowany). Błąd ujawni się dopiero przy odczycie: Typed property Odznaka::$nazwa must not be accessed before initialization.
  4. 2. Właściwość static należy do klasy, nie do obiektu — oba wywołania konstruktora zwiększają ten sam licznik. Wewnątrz klasy odwołujemy się do niej przez self::$ile.
  5. return !$this->wykonane && $this->xp > 0;
  6. W metodzie $nazwa i $xp to zmienne lokalne, których nie ma (Undefined variable) — do właściwości obiektu sięgamy przez $this: return $this->nazwa . ' (+' . $this->xp . ' XP)';
  7. Wyświetli Premium — klasa potomna nadpisała metodę. Po dodaniu final w klasie bazowej kod przestanie działać: Fatal error: Cannot override final method Profil::nazwa().
  8. b). W argumentach nazwanych podajemy nazwę parametru bez $ i po dwukropku; => służy do tablic, nie do wywołań.
  9. const należy do klasy, a nie do obiektu: return self::MAX_GRACZY; (spoza klasy — Konfiguracja::MAX_GRACZY).
  10. a) prawda, b) fałsz (tylko jedna klasa bazowa), c) fałsz (klasa abstrakcyjna to tylko baza dla potomnych), d) prawda.
  11. Wypisze 15. $b = $a; nie tworzy kopii obiektu — obie zmienne wskazują ten sam obiekt, więc zmiana wykonana przez $b jest widoczna przez $a. Gdyby $a było tablicą, $b byłoby kopią i $a pozostałoby bez zmian.
  12. Fatal error: Uncaught Error: Cannot use object of type Zadanie as array. Poprawnie: foreach ($zadania as $zadanie) { echo $zadanie->pobierzTytul() . ' — ' . $zadanie->pobierzXp() . ' XP<br>'; }
  13. 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 i czyPilne(3) → true (zadanie po terminie też jest pilne), termin dokładnie za 3 dni i czyPilne(3) → true, termin za 4 dni → false, zadanie wykonane → zawsze false. Tak jak w czyPoTerminie() używamy new DateTime('today'), żeby porównywać daty o tej samej godzinie (00:00), a nie datę z aktualnym czasem.