Lekcja 22 (OOP 2): Klasy i Obiekty - Definiowanie, Właściwości, Metody

Witaj w drugiej lekcji poświęconej Programowaniu Obiektowemu w PHP! W poprzedniej lekcji wprowadziliśmy teoretyczne podstawy OOP, omawiając kluczowe koncepcje takie jak klasy, obiekty, hermetyzacja, dziedziczenie i polimorfizm. Teraz nadszedł czas, aby przejść od teorii do praktyki i nauczyć się, jak definiować własne klasy, tworzyć z nich obiekty oraz pracować z ich właściwościami i metodami w PHP.

W tej lekcji skupimy się na fundamentalnych elementach tworzenia klas: jak wygląda ich składnia, jak deklarować właściwości (czyli zmienne przechowujące stan obiektu) oraz metody (czyli funkcje definiujące zachowanie obiektu). Zobaczymy również, jak tworzyć instancje klas (obiekty) i jak odwoływać się do ich składowych. To kluczowe umiejętności, które pozwolą nam budować pierwsze, proste struktury obiektowe.

Definiowanie Klasy w PHP

Klasa w PHP jest definiowana za pomocą słowa kluczowego class, po którym następuje nazwa klasy, a następnie para nawiasów klamrowych { }, wewnątrz których umieszcza się definicje właściwości i metod klasy. Nazwy klas w PHP, zgodnie z popularnymi konwencjami (np. PSR-1), powinny być pisane w stylu PascalCase (znanym też jako UpperCamelCase), co oznacza, że każdy wyraz w nazwie zaczyna się wielką literą, bez spacji, np. MojaPierwszaKlasa, Uzytkownik, ProduktSamochodowy.

Podstawowa składnia definicji klasy:

<?php
class NazwaKlasy 
{
    // Tutaj definicje właściwości (zmiennych członkowskich)
    
    // Tutaj definicje metod (funkcji członkowskich)
}
?>

Przykład prostej, pustej klasy:

<?php
class Produkt 
{
    // Na razie klasa jest pusta
}
?>

Taka klasa, mimo że pusta, jest już poprawną definicją i możemy tworzyć jej obiekty. Jednak aby klasa była użyteczna, powinna zawierać właściwości i metody.

Właściwości (Properties)

Właściwości (nazywane też atrybutami, polami lub zmiennymi członkowskimi) to zmienne zadeklarowane wewnątrz klasy. Przechowują one dane, które opisują stan obiektu. Każdą właściwość deklaruje się za pomocą jednego z modyfikatorów dostępu (public, protected lub private), po którym następuje nazwa właściwości (zaczynająca się od znaku dolara $, tak jak zwykłe zmienne w PHP).

Modyfikatory dostępu dla właściwości:

Na razie będziemy używać głównie modyfikatora public, aby ułatwić dostęp do właściwości. O hermetyzacji i znaczeniu pozostałych modyfikatorów będziemy mówić szczegółowo w kolejnych lekcjach.

Przykład deklaracji właściwości w klasie Produkt:

<?php
class Produkt 
{
    // Właściwości publiczne
    public $nazwa;
    public $cena;
    public $opis;
    public $dostepnaIlosc = 0; // Możemy nadać wartość domyślną

    // Właściwość chroniona (dostępna tylko w tej klasie i klasach potomnych)
    protected $kosztProdukcji;

    // Właściwość prywatna (dostępna tylko w tej klasie)
    private $numerSeryjnyUnikalny;
}
?>

Właściwościom można nadawać wartości domyślne bezpośrednio przy ich deklaracji, tak jak dla $dostepnaIlosc. Jeśli wartość domyślna nie zostanie podana, właściwość będzie miała wartość null do momentu jej pierwszego przypisania.

Typowanie Właściwości (Property Typing)

Od PHP 7.4 wprowadzono możliwość typowania właściwości klas. Oznacza to, że możemy określić, jakiego typu dane dana właściwość może przechowywać. Zwiększa to bezpieczeństwo typu i czytelność kodu, pomagając wykrywać błędy na wcześniejszym etapie.

<?php
class ProduktZaawansowany
{
    public string $nazwa; // Właściwość musi być typu string
    public float $cena;    // Właściwość musi być typu float
    public ?string $opis;  // Może być stringiem lub null (nullable type)
    public int $dostepnaIlosc = 0;

    // W PHP 8 wprowadzono również typy unijne (union types)
    public int|string $identyfikator; 
}

// Próba przypisania nieprawidłowego typu spowoduje błąd TypeError
// $produkt = new ProduktZaawansowany();
// $produkt->cena = "bardzo drogo"; // Błąd! "bardzo drogo" to nie float
?>

Typowanie właściwości jest dobrą praktyką i zaleca się jego stosowanie w nowoczesnym kodzie PHP.

Metody (Methods)

Metody to funkcje zdefiniowane wewnątrz klasy. Określają one zachowania obiektu lub operacje, które można na nim wykonać. Składnia definicji metody jest bardzo podobna do zwykłej funkcji PHP, ale poprzedzona jest modyfikatorem dostępu (public, protected lub private).

Modyfikatory dostępu dla metod działają analogicznie jak dla właściwości:

Przykład definicji metod w klasie Produkt:

<?php
class Produkt 
{
    public string $nazwa = "Nieznany produkt";
    public float $cena = 0.0;
    public int $dostepnaIlosc = 0;

    // Metoda publiczna do wyświetlania informacji o produkcie
    public function wyswietlInformacje(): string
    {
        // $this odnosi się do bieżącego obiektu (instancji klasy)
        return "Produkt: " . $this->nazwa . ", Cena: " . $this->cena . " PLN, Dostępna ilość: " . $this->dostepnaIlosc;
    }

    // Metoda publiczna do zmiany ceny
    public function zmienCene(float $nowaCena): void // void oznacza, że metoda nic nie zwraca
    {
        if ($nowaCena >= 0) {
            $this->cena = $nowaCena;
        } else {
            echo "Cena nie może być ujemna!<br>";
        }
    }

    // Metoda publiczna do sprzedaży produktu
    public function sprzedaj(int $ilosc = 1): bool
    {
        if ($ilosc > 0 && $this->dostepnaIlosc >= $ilosc) {
            $this->dostepnaIlosc -= $ilosc;
            echo $ilosc . " szt. produktu '" . $this->nazwa . "' zostało sprzedanych.<br>";
            return true;
        } else {
            echo "Nie można sprzedać produktu '" . $this->nazwa . "'. Niewystarczająca ilość lub błędna wartość.<br>";
            return false;
        }
    }
    
    // Prywatna metoda pomocnicza (nie będzie dostępna z zewnątrz)
    private function logujOperacje(string $operacja): void
    {
        // Tutaj mógłby być kod logujący operację np. do pliku
        echo "LOG: Wykonano operację '" . $operacja . "' na produkcie '" . $this->nazwa . "'.<br>";
    }
}
?>

Pseudo-zmienna $this

Wewnątrz metod obiektu (niestatycznych, o statycznych powiemy później) dostępna jest specjalna pseudo-zmienna $this. $this odnosi się do bieżącej instancji obiektu, czyli do obiektu, na rzecz którego dana metoda została wywołana. Używamy $this->nazwaWlasciwosci do odwołania się do właściwości obiektu i $this->nazwaMetody() do wywołania innej metody tego samego obiektu.

Typowanie Argumentów i Wartości Zwracanej przez Metody

Podobnie jak w przypadku zwykłych funkcji (co omawialiśmy w Lekcji 10), metody również mogą (i powinny) mieć typowane argumenty oraz określoną typ wartości zwracanej. W przykładzie powyżej, metoda wyswietlInformacje() ma zadeklarowany typ zwracany string, metoda zmienCene() przyjmuje argument $nowaCena typu float i zwraca void (czyli nic nie zwraca), a metoda sprzedaj() przyjmuje int i zwraca bool.

Tworzenie Obiektów (Instancji Klasy)

Sama definicja klasy jest tylko szablonem. Aby móc z niej korzystać, musimy stworzyć obiekt (nazywany też instancją lub egzemplarzem) tej klasy. Obiekty tworzy się w PHP za pomocą słowa kluczowego new, po którym następuje nazwa klasy i para nawiasów okrągłych () (nawet jeśli klasa nie ma zdefiniowanego konstruktora – o konstruktorach powiemy w następnej lekcji).

<?php
// Zakładając, że klasa Produkt jest zdefiniowana jak powyżej

// Tworzenie obiektu (instancji) klasy Produkt
$ksiazka = new Produkt();
$dlugopis = new Produkt();

// $ksiazka i $dlugopis to teraz dwa niezależne obiekty klasy Produkt.
// Każdy z nich ma własny zestaw właściwości ($nazwa, $cena, $dostepnaIlosc).
?>

Dostęp do Właściwości i Metod Obiektu

Po utworzeniu obiektu, możemy odwoływać się do jego publicznych właściwości i wywoływać jego publiczne metody za pomocą operatora obiektu -> (strzałka składająca się z myślnika i znaku większości).

Dostęp do właściwości: $nazwaObiektu->nazwaWlasciwosci

Wywołanie metody: $nazwaObiektu->nazwaMetody(argumenty)

Przykład użycia obiektu klasy Produkt:

<?php
// Zakładając, że klasa Produkt jest zdefiniowana

$ksiazka = new Produkt();

// Ustawianie wartości publicznych właściwości
$ksiazka->nazwa = "PHP dla Początkujących";
$ksiazka->cena = 49.99;
$ksiazka->dostepnaIlosc = 100;

// Wywoływanie publicznych metod
echo $ksiazka->wyswietlInformacje() . "<br>";
// Wynik: Produkt: PHP dla Początkujących, Cena: 49.99 PLN, Dostępna ilość: 100

$ksiazka->zmienCene(45.50);
echo "Po zmianie ceny: " . $ksiazka->wyswietlInformacje() . "<br>";
// Wynik: Po zmianie ceny: Produkt: PHP dla Początkujących, Cena: 45.5 PLN, Dostępna ilość: 100

$ksiazka->sprzedaj(5);
// Wynik: 5 szt. produktu 'PHP dla Początkujących' zostało sprzedanych.
echo "Po sprzedaży: " . $ksiazka->wyswietlInformacje() . "<br>";
// Wynik: Po sprzedaży: Produkt: PHP dla Początkujących, Cena: 45.5 PLN, Dostępna ilość: 95

// Próba odwołania się do właściwości chronionej lub prywatnej spoza klasy spowoduje błąd:
// echo $ksiazka->kosztProdukcji; // Błąd: Cannot access protected property Produkt::$kosztProdukcji
// echo $ksiazka->numerSeryjnyUnikalny; // Błąd: Cannot access private property Produkt::$numerSeryjnyUnikalny

// Próba wywołania metody prywatnej spoza klasy również spowoduje błąd:
// $ksiazka->logujOperacje("test logowania z zewnątrz"); // Błąd: Call to private method Produkt::logujOperacje()

$dlugopis = new Produkt();
$dlugopis->nazwa = "Elegancki Długopis";
$dlugopis->cena = 19.90;
$dlugopis->dostepnaIlosc = 250;

echo $dlugopis->wyswietlInformacje() . "<br>";
// Wynik: Produkt: Elegancki Długopis, Cena: 19.9 PLN, Dostępna ilość: 250
?>

Jak widać, każdy obiekt ($ksiazka, $dlugopis) utrzymuje swój własny, niezależny stan (wartości właściwości). Wywołanie metody na jednym obiekcie nie wpływa na stan innego obiektu tej samej klasy (chyba że metoda celowo operuje na danych współdzielonych, np. statycznych, ale o tym później).

Przykład Kompletnej Klasy i Jej Użycia

Połączmy wszystko w jeden większy przykład – klasa reprezentująca prostokąt.

<?php
class Prostokat
{
    // Właściwości z typowaniem
    public float $dlugoscBokuA;
    public float $dlugoscBokuB;

    // Metoda do ustawiania wymiarów
    public function ustawWymiary(float $a, float $b): void
    {
        if ($a > 0 && $b > 0) {
            $this->dlugoscBokuA = $a;
            $this->dlugoscBokuB = $b;
        } else {
            echo "Długości boków muszą być dodatnie!<br>";
            // Można by tu zainicjować domyślnymi wartościami lub rzucić wyjątek
            $this->dlugoscBokuA = 0;
            $this->dlugoscBokuB = 0;
        }
    }

    // Metoda obliczająca pole
    public function obliczPole(): float
    {
        return $this->dlugoscBokuA * $this->dlugoscBokuB;
    }

    // Metoda obliczająca obwód
    public function obliczObwod(): float
    {
        return 2 * ($this->dlugoscBokuA + $this->dlugoscBokuB);
    }

    // Metoda wyświetlająca informacje o prostokącie
    public function wyswietlDane(): string
    {
        if ($this->dlugoscBokuA > 0 && $this->dlugoscBokuB > 0) {
             return sprintf(
                "Prostokąt o bokach %.2f i %.2f: Pole = %.2f, Obwód = %.2f",
                $this->dlugoscBokuA,
                $this->dlugoscBokuB,
                $this->obliczPole(),
                $this->obliczObwod()
            );
        } else {
            return "Prostokąt ma niepoprawne wymiary.";
        }
    }
}

// Tworzenie i używanie obiektów klasy Prostokat
$prostokat1 = new Prostokat();
$prostokat1->ustawWymiary(5.0, 10.0);
echo $prostokat1->wyswietlDane() . "<br>";
// Wynik: Prostokąt o bokach 5.00 i 10.00: Pole = 50.00, Obwód = 30.00

$prostokat2 = new Prostokat();
$prostokat2->ustawWymiary(3.5, 7.2);
echo "Pole drugiego prostokąta: " . $prostokat2->obliczPole() . "<br>";
// Wynik: Pole drugiego prostokąta: 25.2

$prostokat3 = new Prostokat();
$prostokat3->ustawWymiary(-2.0, 5.0); // Niepoprawne wymiary
echo $prostokat3->wyswietlDane() . "<br>";
// Wynik: Długości boków muszą być dodatnie!
//         Prostokąt ma niepoprawne wymiary.

?>

Ten przykład pokazuje, jak klasa Prostokat enkapsuluje (łączy) dane (długości boków) z operacjami na tych danych (ustawianie wymiarów, obliczanie pola i obwodu, wyświetlanie informacji). Każdy obiekt klasy Prostokat jest niezależną jednostką.

Podsumowanie Lekcji

W tej lekcji nauczyliśmy się praktycznych aspektów definiowania klas i tworzenia obiektów w PHP. Dowiedzieliśmy się, jak deklarować właściwości (ze zwróceniem uwagi na modyfikatory dostępu i typowanie) oraz metody (również z modyfikatorami dostępu, typowaniem argumentów i wartości zwracanej). Kluczowym elementem jest zrozumienie roli pseudo-zmiennej $this, która pozwala metodom na interakcję ze stanem konkretnego obiektu.

Opanowaliśmy również proces tworzenia instancji klasy za pomocą operatora new oraz sposób odwoływania się do publicznych właściwości i metod obiektu za pomocą operatora ->.

Te podstawy są absolutnie fundamentalne dla dalszej nauki Programowania Obiektowego. W następnej lekcji zajmiemy się bardzo ważnym tematem konstruktorów i destruktorów, które pozwalają na inicjalizację obiektów przy ich tworzeniu oraz wykonywanie operacji porządkujących przy ich niszczeniu. Poznamy również inne metody magiczne w PHP.


Zadanie praktyczne

Stwórz klasę o nazwie Kolo, która będzie reprezentować koło geometryczne.

  1. Klasa powinna mieć jedną publiczną, typowaną właściwość $promien (typu float).
  2. Dodaj publiczną metodę ustawPromien(float $r): void, która ustawia wartość promienia. Metoda powinna sprawdzać, czy podany promień jest większy od zera. Jeśli nie, powinna wyświetlić komunikat błędu i ustawić promień na 0.
  3. Dodaj publiczną metodę obliczPole(): float, która oblicza i zwraca pole koła (wzór: π * r2). Użyj funkcji M_PI dla wartości π.
  4. Dodaj publiczną metodę obliczObwod(): float, która oblicza i zwraca obwód koła (wzór: 2 * π * r).
  5. Dodaj publiczną metodę wyswietlInformacje(): string, która zwraca sformatowany ciąg znaków z informacjami o kole (promień, pole, obwód).
  6. Utwórz dwa obiekty klasy Kolo, ustaw im różne promienie i wyświetl informacje o nich, korzystając z metody wyswietlInformacje(). Przetestuj również przypadek podania ujemnego promienia.

Kliknij, aby zobaczyć przykładowe rozwiązanie
<?php
class Kolo 
{
    public float $promien = 0.0;

    public function ustawPromien(float $r): void
    {
        if ($r > 0) {
            $this->promien = $r;
        } else {
            echo "Promień musi być wartością dodatnią! Ustawiono promień na 0.<br>";
            $this->promien = 0.0;
        }
    }

    public function obliczPole(): float
    {
        return M_PI * pow($this->promien, 2);
    }

    public function obliczObwod(): float
    {
        return 2 * M_PI * $this->promien;
    }

    public function wyswietlInformacje(): string
    {
        if ($this->promien > 0) {
            return sprintf(
                "Koło o promieniu %.2f: Pole = %.2f, Obwód = %.2f",
                $this->promien,
                $this->obliczPole(),
                $this->obliczObwod()
            );
        } else {
            return "Koło ma niepoprawny promień (musi być większy od 0).";
        }
    }
}

// Testowanie klasy Kolo
$kolo1 = new Kolo();
$kolo1->ustawPromien(5.0);
echo $kolo1->wyswietlInformacje() . "<br>";

$kolo2 = new Kolo();
$kolo2->ustawPromien(2.75);
echo $kolo2->wyswietlInformacje() . "<br>";

$kolo3 = new Kolo();
$kolo3->ustawPromien(-3.0);
echo $kolo3->wyswietlInformacje() . "<br>";

/* Przykładowy wynik:
Koło o promieniu 5.00: Pole = 78.54, Obwód = 31.42
Koło o promieniu 2.75: Pole = 23.76, Obwód = 17.28
Promień musi być wartością dodatnią! Ustawiono promień na 0.
Koło ma niepoprawny promień (musi być większy od 0).
*/
?>

Zadanie do samodzielnego wykonania

Stwórz klasę Samochod z następującymi elementami:

  1. Publiczne, typowane właściwości: $marka (string), $model (string), $rokProdukcji (int), $predkoscAktualna (int, domyślnie 0).
  2. Publiczną metodę przyspiesz(int $wartosc): void, która zwiększa $predkoscAktualna o podaną wartość, ale nie więcej niż do pewnej prędkości maksymalnej (np. 200). Jeśli prędkość maksymalna zostanie osiągnięta, ustaw $predkoscAktualna na tę wartość.
  3. Publiczną metodę hamuj(int $wartosc): void, która zmniejsza $predkoscAktualna o podaną wartość, ale nie mniej niż do 0.
  4. Publiczną metodę wyswietlStatus(): string, która zwraca informacje o marce, modelu, roku produkcji i aktualnej prędkości.
  5. Utwórz dwa różne obiekty klasy Samochod, ustaw im właściwości (marka, model, rok), a następnie przetestuj metody przyspiesz() i hamuj(), wyświetlając status po każdej operacji.


FAQ - Klasy i Obiekty w PHP

Czy nazwy właściwości i metod muszą zaczynać się od znaku dolara $?

Nazwy właściwości (zmiennych członkowskich) muszą zaczynać się od znaku dolara $, np. public $nazwa;. Natomiast nazwy metod (funkcji członkowskich) piszemy bez znaku dolara, np. public function mojaMetoda() {}. Odwołując się do właściwości wewnątrz metody za pomocą $this, również używamy znaku dolara: $this->$nazwaWlasciwosci.

Jaka jest różnica między public $zmienna; a var $zmienna; w deklaracji właściwości?

Słowo kluczowe var jest starszym sposobem deklarowania właściwości, pochodzącym z PHP 4. W PHP 5 i nowszych jest ono traktowane jako alias dla public. Zaleca się jednak używanie jawnych modyfikatorów dostępu (public, protected, private) dla lepszej czytelności i zgodności z nowoczesnymi praktykami.

Czy mogę zadeklarować właściwość bez modyfikatora dostępu?

Jeśli pominiesz modyfikator dostępu przy deklaracji właściwości (co nie jest zalecane), PHP domyślnie potraktuje ją jako public. Jednak dla jasności kodu zawsze powinno się jawnie określać modyfikator dostępu.

Co się stanie, jeśli spróbuję odwołać się do nieistniejącej właściwości lub metody obiektu?

Jeśli spróbujesz odczytać nieistniejącą właściwość, PHP wygeneruje ostrzeżenie (Notice) i zwróci null. Jeśli spróbujesz przypisać wartość do nieistniejącej właściwości publicznej, PHP ją utworzy dynamicznie (co jest często uważane za złą praktykę). Próba wywołania nieistniejącej metody spowoduje błąd krytyczny (Fatal error).

Czy typowanie właściwości i metod jest obowiązkowe?

Nie, typowanie w PHP jest opcjonalne (z wyjątkiem niektórych kontekstów w PHP 8+). Jednak jego stosowanie jest silnie zalecane, ponieważ poprawia czytelność kodu, pomaga wykrywać błędy na wczesnym etapie i ułatwia pracę narzędziom do analizy statycznej kodu. Jest to cecha nowoczesnego PHP.

Czy mogę mieć w klasie tylko właściwości lub tylko metody?

Tak, klasa może zawierać tylko właściwości, tylko metody, lub obie te rzeczy. Klasa może być nawet całkowicie pusta (choć rzadko jest to użyteczne samo w sobie, czasem puste klasy służą jako znaczniki lub są rozszerzane przez inne klasy).

Jakie są konwencje nazewnictwa dla metod?

Podobnie jak dla funkcji, dla metod najczęściej stosuje się konwencję camelCase (pierwszy wyraz małą literą, kolejne wyrazy zaczynają się wielką literą, np. obliczPoleKola, wyswietlDaneUzytkownika). Jest to zgodne z rekomendacjami PSR-1.