Wprowadzenie do programowania obiektowego

Termin “OOP” oznacza programowanie obiektowe, które jest sposobem organizacji i strukturyzacji kodu. OOP pozwala nam postrzegać program jako zbiór obiektów, które komunikują się ze sobą, zamiast sekwencji poleceń i funkcji.

W OOP “obiekt” to jednostka, która zawiera dane i funkcje, które operują na tych danych. Obiekty są tworzone na podstawie “klas”, które możemy rozumieć jako plany lub szablony dla obiektów. Kiedy mamy klasę, możemy utworzyć jej “instancję”, czyli konkretny obiekt stworzony na podstawie tej klasy.

Pokażmy, jak możemy stworzyć prostą klasę w PHP. Podczas definiowania klasy użyjemy słowa kluczowego “class”, następnie nazwy klasy, a potem nawiasów klamrowych, które otaczają funkcje (nazywane “metodami”) i zmienne klasy (nazywane “właściwościami” lub po angielsku “property”):

class Samochod
{
	function zatrab()
	{
		echo 'Bip bip!';
	}
}

W tym przykładzie stworzyliśmy klasę o nazwie Samochod z jedną funkcją (lub “metodą”) o nazwie zatrab.

Każda klasa powinna rozwiązywać tylko jedno główne zadanie. Jeśli klasa robi zbyt wiele rzeczy, może być wskazane podzielenie jej na mniejsze, wyspecjalizowane klasy.

Klasy zazwyczaj przechowujemy w osobnych plikach, aby kod był zorganizowany i łatwo się w nim orientować. Nazwa pliku powinna odpowiadać nazwie klasy, więc dla klasy Samochod nazwa pliku byłaby Samochod.php.

Podczas nazywania klas dobrze jest trzymać się konwencji “PascalCase”, co oznacza, że każde słowo w nazwie zaczyna się wielką literą i nie ma między nimi żadnych podkreśleń ani innych separatorów. Metody i właściwości używają konwencji “camelCase”, co oznacza, że zaczynają się małą literą.

Niektóre metody w PHP mają specjalne zadania i są oznaczone prefiksem __ (dwa podkreślenia). Jedną z najważniejszych specjalnych metod jest “konstruktor”, który jest oznaczony jako __construct. Konstruktor to metoda, która jest automatycznie wywoływana, gdy tworzysz nową instancję klasy.

Konstruktor często używamy do ustawienia początkowego stanu obiektu. Na przykład, tworząc obiekt reprezentujący osobę, możesz wykorzystać konstruktor do ustawienia jej wieku, imienia lub innych właściwości.

Pokażmy, jak użyć konstruktora w PHP:

class Osoba
{
	private $wiek;

	function __construct($wiek)
	{
		$this->wiek = $wiek;
	}

	function ileMaszLat()
	{
		return $this->wiek;
	}
}

$osoba = new Osoba(25);
echo $osoba->ileMaszLat(); // Wyświetli: 25

W tym przykładzie klasa Osoba ma właściwość (zmienną) $wiek oraz konstruktor, który ustawia tę właściwość. Metoda ileMaszLat() następnie umożliwia dostęp do wieku osoby.

Pseudozmienna $this jest używana wewnątrz klasy do uzyskania dostępu do właściwości i metod obiektu.

Słowo kluczowe new jest używane do tworzenia nowej instancji klasy. W powyższym przykładzie stworzyliśmy nową osobę w wieku 25 lat.

Możesz również ustawić wartości domyślne dla parametrów konstruktora, jeśli nie są one określone podczas tworzenia obiektu. Na przykład:

class Osoba
{
	private $wiek;

	function __construct($wiek = 20)
	{
		$this->wiek = $wiek;
	}

	function ileMaszLat()
	{
		return $this->wiek;
	}
}

$osoba = new Osoba;  // jeśli nie przekazujemy żadnego argumentu, nawiasy można pominąć
echo $osoba->ileMaszLat(); // Wyświetli: 20

W tym przykładzie, jeśli nie podasz wieku podczas tworzenia obiektu Osoba, zostanie użyta wartość domyślna 20.

Przyjemne jest to, że definicję właściwości wraz z jej inicjalizacją przez konstruktor można skrócić i uprościć w ten sposób:

class Osoba
{
	function __construct(
		private $wiek = 20,
	) {
	}
}

Dla kompletności, oprócz konstruktorów obiekty mogą mieć również destruktory (metoda __destruct), które są wywoływane przed zwolnieniem obiektu z pamięci.

Przestrzenie nazw

Przestrzenie nazw (lub “namespaces” po angielsku) pozwalają nam organizować i grupować powiązane klasy, funkcje i stałe, jednocześnie unikając konfliktów nazw. Możesz je sobie wyobrazić jako foldery na komputerze, gdzie każdy folder zawiera pliki należące do określonego projektu lub tematu.

Przestrzenie nazw są szczególnie przydatne w większych projektach lub gdy używasz bibliotek stron trzecich, gdzie mogą wystąpić konflikty nazw klas.

Wyobraź sobie, że masz klasę o nazwie Samochod w swoim projekcie i chcesz ją umieścić w przestrzeni nazw o nazwie Transport. Zrobisz to w ten sposób:

namespace Transport;

class Samochod
{
	function zatrab()
	{
		echo 'Bip bip!';
	}
}

Jeśli chcesz użyć klasy Samochod w innym pliku, musisz określić, z jakiej przestrzeni nazw pochodzi klasa:

$auto = new Transport\Samochod;

Dla uproszczenia możesz na początku pliku określić, której klasy z danej przestrzeni nazw chcesz używać, co pozwala tworzyć instancje bez konieczności podawania całej ścieżki:

use Transport\Samochod;

$auto = new Samochod;

Dziedziczenie

Dziedziczenie jest narzędziem programowania obiektowego, które pozwala tworzyć nowe klasy na podstawie już istniejących klas, przejmować ich właściwości i metody oraz rozszerzać lub przedefiniowywać je według potrzeb. Dziedziczenie pozwala zapewnić ponowne wykorzystanie kodu i hierarchię klas.

Upraszczając, jeśli mamy jedną klasę i chcielibyśmy stworzyć inną, pochodną od niej, ale z kilkoma zmianami, możemy nową klasę “odziedziczyć” z pierwotnej klasy.

W PHP dziedziczenie realizujemy za pomocą słowa kluczowego extends.

Nasza klasa Osoba przechowuje informację o wieku. Możemy mieć inną klasę Student, która rozszerza Osobę i dodaje informację o kierunku studiów.

Spójrzmy na przykład:

class Osoba
{
	private $wiek;

	function __construct($wiek)
	{
		$this->wiek = $wiek;
	}

	function wypiszInformacje()
	{
		echo "Wiek: {$this->wiek} lat\n";
	}
}

class Student extends Osoba
{
	private $kierunek;

	function __construct($wiek, $kierunek)
	{
		parent::__construct($wiek);
		$this->kierunek = $kierunek;
	}

	function wypiszInformacje()
	{
		parent::wypiszInformacje();
		echo "Kierunek studiów: {$this->kierunek} \n";
	}
}

$student = new Student(20, 'Informatyka');
$student->wypiszInformacje();

Jak działa ten kod?

  • Użyliśmy słowa kluczowego extends do rozszerzenia klasy Osoba, co oznacza, że klasa Student odziedziczy wszystkie metody i właściwości z Osoby.
  • Słowo kluczowe parent:: pozwala nam wywoływać metody z klasy nadrzędnej. W tym przypadku wywołaliśmy konstruktor z klasy Osoba przed dodaniem własnej funkcjonalności do klasy Student. Podobnie wywołaliśmy metodę wypiszInformacje() przodka przed wypisaniem informacji o studencie.

Dziedziczenie jest przeznaczone dla sytuacji, gdy istnieje relacja “jest” między klasami. Na przykład Student jest Osobą. Kot jest zwierzęciem. Daje nam to możliwość w przypadkach, gdy w kodzie oczekujemy jednego obiektu (np. “Osoba”), użycia zamiast niego obiektu dziedziczonego (np. “Student”).

Ważne jest, aby zdać sobie sprawę, że głównym celem dziedziczenia nie jest zapobieganie duplikacji kodu. Wręcz przeciwnie, niewłaściwe wykorzystanie dziedziczenia może prowadzić do skomplikowanego i trudnego do utrzymania kodu. Jeśli relacja “jest” między klasami nie istnieje, powinniśmy zamiast dziedziczenia rozważyć kompozycję.

Zauważ, że metody wypiszInformacje() w klasach Osoba i Student wypisują nieco inne informacje. Możemy dodać kolejne klasy (na przykład Pracownik), które będą dostarczać kolejne implementacje tej metody. Zdolność obiektów różnych klas do reagowania na tę samą metodę na różne sposoby nazywa się polimorfizmem:

$osoby = [
	new Osoba(30),
	new Student(20, 'Informatyka'),
	new Pracownik(45, 'Dyrektor'),
];

foreach ($osoby as $osoba) {
	$osoba->wypiszInformacje();
}

Kompozycja

Kompozycja to technika, w której zamiast dziedziczyć właściwości i metody innej klasy, po prostu wykorzystujemy jej instancję w naszej klasie. Pozwala to łączyć funkcjonalności i właściwości wielu klas bez konieczności tworzenia złożonych struktur dziedziczenia.

Spójrzmy na przykład. Mamy klasę Silnik i klasę Samochod. Zamiast mówić “Samochód jest Silnikiem”, mówimy “Samochód ma Silnik”, co jest typową relacją kompozycji.

class Silnik
{
	function wlacz()
	{
		echo 'Silnik pracuje.';
	}
}

class Samochod
{
	private $silnik;

	function __construct()
	{
		$this->silnik = new Silnik;
	}

	function start()
	{
		$this->silnik->wlacz();
		echo 'Samochód jest gotowy do jazdy!';
	}
}

$samochod = new Samochod;
$samochod->start();

Tutaj Samochod nie ma wszystkich właściwości i metod Silnika, ale ma do niego dostęp za pośrednictwem właściwości $silnik.

Zaletą kompozycji jest większa elastyczność w projektowaniu i lepsza możliwość modyfikacji w przyszłości.

Widoczność

W PHP możesz zdefiniować “widoczność” dla właściwości, metod i stałych klasy. Widoczność określa, skąd możesz uzyskać dostęp do tych elementów.

  1. Public: Jeśli element jest oznaczony jako public, oznacza to, że możesz uzyskać do niego dostęp z dowolnego miejsca, nawet spoza klasy.
  2. Protected: Element oznaczony jako protected jest dostępny tylko w ramach danej klasy i wszystkich jej potomków (klas, które dziedziczą z tej klasy).
  3. Private: Jeśli element jest private, możesz uzyskać do niego dostęp tylko z wnętrza klasy, w której został zdefiniowany.

Jeśli nie określisz widoczności, PHP automatycznie ustawi ją na public.

Spójrzmy na przykładowy kod:

class PrzykladWidocznosci
{
	public $wlasciwoscPubliczna = 'Publiczna';
	protected $wlasciwoscChroniona = 'Chroniona';
	private $wlasciwoscPrywatna = 'Prywatna';

	public function wypiszWlasciwosci()
	{
		echo $this->wlasciwoscPubliczna;  // Działa
		echo $this->wlasciwoscChroniona; // Działa
		echo $this->wlasciwoscPrywatna; // Działa
	}
}

$obiekt = new PrzykladWidocznosci;
$obiekt->wypiszWlasciwosci();
echo $obiekt->wlasciwoscPubliczna;      // Działa
// echo $obiekt->wlasciwoscChroniona;  // Zgłosi błąd
// echo $obiekt->wlasciwoscPrywatna;  // Zgłosi błąd

Kontynuujemy z dziedziczeniem klasy:

class PotomekKlasy extends PrzykladWidocznosci
{
	public function wypiszWlasciwosci()
	{
		echo $this->wlasciwoscPubliczna;   // Działa
		echo $this->wlasciwoscChroniona;  // Działa
		// echo $this->wlasciwoscPrywatna;  // Zgłosi błąd
	}
}

W tym przypadku metoda wypiszWlasciwosci() w klasie PotomekKlasy może uzyskać dostęp do publicznych i chronionych właściwości, ale nie może uzyskać dostępu do prywatnych właściwości klasy rodzicielskiej.

Dane i metody powinny być jak najbardziej ukryte i dostępne tylko za pośrednictwem zdefiniowanego interfejsu. Pozwoli to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.

Słowo kluczowe final

W PHP możemy użyć słowa kluczowego final, jeśli chcemy uniemożliwić dziedziczenie lub nadpisywanie klasy, metody lub stałej. Kiedy oznaczymy klasę jako final, nie może być ona rozszerzana. Kiedy oznaczymy metodę jako final, nie może być ona nadpisana w klasie potomnej.

Świadomość, że dana klasa lub metoda nie będzie dalej modyfikowana, pozwala nam łatwiej wprowadzać zmiany, nie martwiąc się o możliwe konflikty. Na przykład możemy dodać nową metodę bez obaw, że któryś z jej potomków ma już metodę o tej samej nazwie i doszłoby do kolizji. Lub możemy zmienić parametry metody, ponieważ znów nie ma ryzyka, że spowodujemy niezgodność z nadpisaną metodą w potomku.

final class KlasaFinalna
{
}

// Poniższy kod spowoduje błąd, ponieważ nie możemy dziedziczyć z klasy finalnej.
class PotomekKlasyFinalnej extends KlasaFinalna
{
}

W tym przykładzie próba dziedziczenia z finalnej klasy KlasaFinalna spowoduje błąd.

Statyczne właściwości i metody

Kiedy w PHP mówimy o “statycznych” elementach klasy, mamy na myśli metody i właściwości, które należą do samej klasy, a nie do konkretnej instancji tej klasy. Oznacza to, że nie musisz tworzyć instancji klasy, aby mieć do nich dostęp. Zamiast tego wywołujesz je lub uzyskujesz do nich dostęp bezpośrednio przez nazwę klasy.

Pamiętaj, że ponieważ elementy statyczne należą do klasy, a nie do jej instancji, nie możesz używać pseudozmiennej $this wewnątrz metod statycznych.

Używanie właściwości statycznych prowadzi do nieprzejrzystego kodu pełnego pułapek, dlatego nigdy nie powinieneś ich używać i nawet nie będziemy tutaj pokazywać przykładu użycia. Natomiast metody statyczne są przydatne. Przykład użycia:

class Kalkulator
{
	public static function dodawanie($a, $b)
	{
		return $a + $b;
	}

	public static function odejmowanie($a, $b)
	{
		return $a - $b;
	}
}

// Użycie metody statycznej bez tworzenia instancji klasy
echo Kalkulator::dodawanie(5, 3); // Wynik: 8
echo Kalkulator::odejmowanie(5, 3); // Wynik: 2

W tym przykładzie stworzyliśmy klasę Kalkulator z dwiema metodami statycznymi. Możemy wywoływać te metody bezpośrednio bez tworzenia instancji klasy za pomocą operatora ::. Metody statyczne są szczególnie przydatne do operacji, które nie zależą od stanu konkretnej instancji klasy.

Stałe klasowe

W ramach klas mamy możliwość definiowania stałych. Stałe to wartości, które nigdy się nie zmienią podczas działania programu. W przeciwieństwie do zmiennych, wartość stałej pozostaje zawsze taka sama.

class Samochod
{
	public const LiczbaKol = 4;

	public function wyswietlLiczbeKol(): int
	{
		echo self::LiczbaKol;
	}
}

echo Samochod::LiczbaKol;  // Wyjście: 4

W tym przykładzie mamy klasę Samochod ze stałą LiczbaKol. Kiedy chcemy uzyskać dostęp do stałej wewnątrz klasy, możemy użyć słowa kluczowego self zamiast nazwy klasy.

Interfejsy obiektowe

Interfejsy obiektowe działają jak “kontrakty” dla klas. Jeśli klasa ma implementować interfejs obiektowy, musi zawierać wszystkie metody, które ten interfejs definiuje. Jest to świetny sposób na zapewnienie, że określone klasy przestrzegają tej samej “umowy” lub struktury.

W PHP interfejs definiuje się słowem kluczowym interface. Wszystkie metody zdefiniowane w interfejsie są publiczne (public). Kiedy klasa implementuje interfejs, używa słowa kluczowego implements.

interface Zwierze
{
	function wydajDzwiek();
}

class Kot implements Zwierze
{
	public function wydajDzwiek()
	{
		echo 'Miau';
	}
}

$kot = new Kot;
$kot->wydajDzwiek();

Jeśli klasa implementuje interfejs, ale nie są w niej zdefiniowane wszystkie oczekiwane metody, PHP zgłosi błąd.

Klasa może implementować wiele interfejsów jednocześnie, co stanowi różnicę w porównaniu do dziedziczenia, gdzie klasa może dziedziczyć tylko z jednej klasy:

interface Stroz
{
	function pilnujDomu();
}

class Pies implements Zwierze, Stroz
{
	public function wydajDzwiek()
	{
		echo 'Hau';
	}

	public function pilnujDomu()
	{
		echo 'Pies uważnie pilnuje domu';
	}
}

Klasy abstrakcyjne

Klasy abstrakcyjne służą jako podstawowe szablony dla innych klas, ale nie można tworzyć ich instancji bezpośrednio. Zawierają kombinację kompletnych metod i metod abstrakcyjnych, które nie mają zdefiniowanej zawartości. Klasy, które dziedziczą z klas abstrakcyjnych, muszą dostarczyć definicje dla wszystkich metod abstrakcyjnych z przodka.

Do definiowania klasy abstrakcyjnej używamy słowa kluczowego abstract.

abstract class KlasaAbstrakcyjna
{
	public function zwyklaMetoda()
	{
		echo 'To jest zwykła metoda';
	}

	abstract public function metodaAbstrakcyjna();
}

class Potomek extends KlasaAbstrakcyjna
{
	public function metodaAbstrakcyjna()
	{
		echo 'To jest implementacja metody abstrakcyjnej';
	}
}

$instancja = new Potomek;
$instancja->zwyklaMetoda();
$instancja->metodaAbstrakcyjna();

W tym przykładzie mamy klasę abstrakcyjną z jedną zwykłą i jedną abstrakcyjną metodą. Następnie mamy klasę Potomek, która dziedziczy z KlasaAbstrakcyjna i dostarcza implementację dla metody abstrakcyjnej.

Jak właściwie różnią się interfejsy i klasy abstrakcyjne? Klasy abstrakcyjne mogą zawierać zarówno abstrakcyjne, jak i konkretne metody, podczas gdy interfejsy jedynie definiują, jakie metody musi implementować klasa, ale nie dostarczają żadnej implementacji. Klasa może dziedziczyć tylko z jednej klasy abstrakcyjnej, ale może implementować dowolną liczbę interfejsów.

Kontrola typów

W programowaniu bardzo ważne jest, aby mieć pewność, że dane, z którymi pracujemy, są odpowiedniego typu. W PHP mamy narzędzia, które nam to zapewniają. Weryfikacja, czy dane mają poprawny typ, nazywa się “kontrolą typów”.

Typy, na które możemy natknąć się w PHP:

  1. Typy podstawowe: Obejmują int (liczby całkowite), float (liczby dziesiętne), bool (wartości logiczne), string (ciągi znaków), array (tablice) i null.
  2. Klasy: Jeśli chcemy, aby wartość była instancją określonej klasy.
  3. Interfejsy: Definiuje zestaw metod, które klasa musi implementować. Wartość, która spełnia interfejs, musi mieć te metody.
  4. Typy mieszane: Możemy określić, że zmienna może mieć więcej niż jeden dozwolony typ.
  5. Void: Ten specjalny typ oznacza, że funkcja lub metoda nie zwraca żadnej wartości.

Pokażmy, jak zmodyfikować kod, aby zawierał typy:

class Osoba
{
	private int $wiek;

	public function __construct(int $wiek)
	{
		$this->wiek = $wiek;
	}

	public function wypiszWiek(): void
	{
		echo "Ta osoba ma {$this->wiek} lat.";
	}
}

/**
 * Funkcja, która przyjmuje obiekt klasy Osoba i wyświetla wiek osoby.
 */
function wypiszWiekOsoby(Osoba $osoba): void
{
	$osoba->wypiszWiek();
}

W ten sposób zapewniliśmy, że nasz kod oczekuje i pracuje z danymi odpowiedniego typu, co pomaga nam zapobiegać potencjalnym błędom.

Niektórych typów nie można bezpośrednio zapisać w PHP. W takim przypadku podaje się je w komentarzu phpDoc, który jest standardowym formatem dokumentacji kodu PHP zaczynającym się od /** i kończącym */. Umożliwia dodawanie opisów klas, metod itp. A także podawanie złożonych typów za pomocą tzw. adnotacji @var, @param i @return. Te typy są następnie wykorzystywane przez narzędzia do statycznej analizy kodu, ale samo PHP ich nie kontroluje.

class Lista
{
	/** @var array<Osoba> zapis mówi, że jest to tablica obiektów Osoba */
	private array $osoby = [];

	public function dodajOsobe(Osoba $osoba): void
	{
		$this->osoby[] = $osoba;
	}
}

Porównywanie i tożsamość

W PHP możesz porównywać obiekty na dwa sposoby:

  1. Porównanie wartości ==: Sprawdza, czy obiekty są tej samej klasy i mają te same wartości w swoich właściwościach.
  2. Tożsamość ===: Sprawdza, czy chodzi o tę samą instancję obiektu.
class Samochod
{
	public string $marka;

	public function __construct(string $marka)
	{
		$this->marka = $marka;
	}
}

$samochod1 = new Samochod('Skoda');
$samochod2 = new Samochod('Skoda');
$samochod3 = $samochod1;

var_dump($samochod1 == $samochod2);   // true, ponieważ mają tę samą wartość
var_dump($samochod1 === $samochod2);  // false, ponieważ nie są tą samą instancją
var_dump($samochod1 === $samochod3);  // true, ponieważ $samochod3 jest tą samą instancją co $samochod1

Operator instanceof

Operator instanceof pozwala sprawdzić, czy dany obiekt jest instancją określonej klasy, potomkiem tej klasy, lub czy implementuje określony interfejs.

Wyobraźmy sobie, że mamy klasę Osoba i inną klasę Student, która jest potomkiem klasy Osoba:

class Osoba
{
	private int $wiek;

	public function __construct(int $wiek)
	{
		$this->wiek = $wiek;
	}
}

class Student extends Osoba
{
	private string $kierunek;

	public function __construct(int $wiek, string $kierunek)
	{
		parent::__construct($wiek);
		$this->kierunek = $kierunek;
	}
}

$student = new Student(20, 'Informatyka');

// Sprawdzenie, czy $student jest instancją klasy Student
var_dump($student instanceof Student);  // Wyjście: bool(true)

// Sprawdzenie, czy $student jest instancją klasy Osoba (ponieważ Student jest potomkiem Osoba)
var_dump($student instanceof Osoba);     // Wyjście: bool(true)

Z wyników widać, że obiekt $student jest jednocześnie uważany za instancję obu klas – Student i Osoba.

Fluent Interfaces

“Płynny interfejs” (ang. “Fluent Interface”) to technika w OOP, która pozwala łączyć metody w łańcuch w jednym wywołaniu. To często upraszcza i uczytelnia kod.

Kluczowym elementem płynnego interfejsu jest to, że każda metoda w łańcuchu zwraca odwołanie do bieżącego obiektu. Osiągamy to, używając return $this; na końcu metody. Ten styl programowania jest często kojarzony z metodami zwanymi “setterami”, które ustawiają wartości właściwości obiektu.

Pokażemy, jak może wyglądać płynny interfejs na przykładzie wysyłania e-maili:

public function wyslijWiadomosc()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

W tym przykładzie metody setFrom(), setRecipient() i setMessage() służą do ustawienia odpowiednich wartości (nadawcy, odbiorcy, treści wiadomości). Po ustawieniu każdej z tych wartości metody zwracają nam bieżący obiekt ($email), co pozwala nam połączyć kolejną metodę za nią. Na końcu wywołujemy metodę send(), która faktycznie wysyła e-mail.

Dzięki płynnym interfejsom możemy pisać kod, który jest intuicyjny i łatwy do odczytania.

Kopiowanie za pomocą clone

W PHP możemy utworzyć kopię obiektu za pomocą operatora clone. W ten sposób otrzymujemy nową instancję o identycznej zawartości.

Jeśli podczas kopiowania obiektu potrzebujemy zmodyfikować niektóre jego właściwości, możemy zdefiniować w klasie specjalną metodę __clone(). Ta metoda jest automatycznie wywoływana, gdy obiekt jest klonowany.

class Owca
{
	public string $imie;

	public function __construct(string $imie)
	{
		$this->imie = $imie;
	}

	public function __clone()
	{
		$this->imie = 'Klon ' . $this->imie;
	}
}

$oryginal = new Owca('Dolly');
echo $oryginal->imie . "\n";  // Wyświetli: Dolly

$klon = clone $oryginal;
echo $klon->imie . "\n";      // Wyświetli: Klon Dolly

W tym przykładzie mamy klasę Owca z jedną właściwością $imie. Kiedy klonujemy instancję tej klasy, metoda __clone() dba o to, aby nazwa sklonowanej owcy otrzymała prefiks “Klon”.

Traity

Traity w PHP to narzędzie, które pozwala współdzielić metody, właściwości i stałe między klasami oraz zapobiegać duplikacji kodu. Możesz je sobie wyobrazić jako mechanizm “kopiuj i wklej” (Ctrl-C i Ctrl-V), gdzie zawartość trait jest “wklejana” do klas. Pozwala to na ponowne wykorzystanie kodu bez konieczności tworzenia skomplikowanych hierarchii klas.

Pokażmy prosty przykład, jak używać traitów w PHP:

trait Trabienie
{
	public function zatrab()
	{
		echo 'Bip bip!';
	}
}

class Samochod
{
	use Trabienie;
}

class Ciezarowka
{
	use Trabienie;
}

$samochod = new Samochod;
$samochod->zatrab(); // Wyświetli 'Bip bip!'

$ciezarowka = new Ciezarowka;
$ciezarowka->zatrab(); // Również wyświetli 'Bip bip!'

W tym przykładzie mamy trait o nazwie Trabienie, który zawiera jedną metodę zatrab(). Następnie mamy dwie klasy: Samochod i Ciezarowka, które obie używają traitu Trabienie. Dzięki temu obie klasy “mają” metodę zatrab(), i możemy ją wywoływać na obiektach obu klas.

Traity pozwalają łatwo i efektywnie współdzielić kod między klasami. Jednocześnie nie wchodzą one w hierarchię dziedziczenia, tj. $samochod instanceof Trabienie zwróci false.

Wyjątki

Wyjątki w OOP pozwalają nam elegancko obsługiwać błędy i nieoczekiwane sytuacje w naszym kodzie. Są to obiekty, które niosą informacje o błędzie lub nietypowej sytuacji.

W PHP mamy wbudowaną klasę Exception, która służy jako podstawa dla wszystkich wyjątków. Ma ona kilka metod, które pozwalają nam uzyskać więcej informacji o wyjątku, takich jak komunikat o błędzie, plik i linia, w której wystąpił błąd, itp.

Kiedy w kodzie wystąpi błąd, możemy “rzucić” wyjątek za pomocą słowa kluczowego throw.

function dzielenie(float $a, float $b): float
{
	if ($b === 0.0) { // Porównanie z float
		throw new Exception('Dzielenie przez zero!');
	}
	return $a / $b;
}

Kiedy funkcja dzielenie() otrzyma jako drugi argument zero, rzuci wyjątek z komunikatem o błędzie 'Dzielenie przez zero!'. Aby zapobiec awarii programu po rzuceniu wyjątku, przechwytujemy go w bloku try/catch:

try {
	echo dzielenie(10, 0);
} catch (Exception $e) {
	echo 'Wyjątek przechwycony: '. $e->getMessage();
}

Kod, który może rzucić wyjątek, jest opakowany w blok try. Jeśli wyjątek zostanie rzucony, wykonanie kodu przenosi się do bloku catch, gdzie możemy przetworzyć wyjątek (np. wypisać komunikat o błędzie).

Po blokach try i catch możemy dodać opcjonalny blok finally, który wykona się zawsze, niezależnie od tego, czy wyjątek został rzucony, czy nie (nawet w przypadku, gdy w bloku try lub catch użyjemy instrukcji return, break lub continue):

try {
	echo dzielenie(10, 0);
} catch (Exception $e) {
	echo 'Wyjątek przechwycony: '. $e->getMessage();
} finally {
	// Kod, który wykona się zawsze, niezależnie od tego, czy wyjątek został rzucony, czy nie
}

Możemy również tworzyć własne klasy (hierarchię) wyjątków, które dziedziczą z klasy Exception. Jako przykład wyobraźmy sobie prostą aplikację bankową, która pozwala dokonywać wpłat i wypłat:

class WyjatekBankowy extends Exception {}
class WyjatekNiewystarczajacychSrodkow extends WyjatekBankowy {}
class WyjatekPrzekroczeniaLimitu extends WyjatekBankowy {}

class KontoBankowe
{
	private int $saldo = 0;
	private int $limitDzienny = 1000;

	public function wplac(int $kwota): int
	{
		$this->saldo += $kwota;
		return $this->saldo;
	}

	public function wyplac(int $kwota): int
	{
		if ($kwota > $this->saldo) {
			throw new WyjatekNiewystarczajacychSrodkow('Na koncie nie ma wystarczających środków.');
		}

		if ($kwota > $this->limitDzienny) {
			throw new WyjatekPrzekroczeniaLimitu('Przekroczono dzienny limit wypłat.');
		}

		$this->saldo -= $kwota;
		return $this->saldo;
	}
}

Dla jednego bloku try można podać wiele bloków catch, jeśli oczekujesz różnych typów wyjątków.

$konto = new KontoBankowe;
$konto->wplac(500);

try {
	$konto->wyplac(1500);
} catch (WyjatekPrzekroczeniaLimitu $e) {
	echo $e->getMessage();
} catch (WyjatekNiewystarczajacychSrodkow $e) {
	echo $e->getMessage();
} catch (WyjatekBankowy $e) {
	echo 'Wystąpił błąd podczas wykonywania operacji.';
}

W tym przykładzie ważne jest zwrócenie uwagi na kolejność bloków catch. Ponieważ wszystkie wyjątki dziedziczą z WyjatekBankowy, gdybyśmy mieli ten blok jako pierwszy, przechwyciłby on wszystkie wyjątki, uniemożliwiając kodowi dotarcie do kolejnych bloków catch. Dlatego ważne jest, aby bardziej specyficzne wyjątki (tj. te, które dziedziczą z innych) znajdowały się w bloku catch wyżej w kolejności niż ich wyjątki nadrzędne.

Iteracja

W PHP możesz przechodzić przez obiekty za pomocą pętli foreach, podobnie jak przechodzisz przez tablice. Aby to działało, obiekt musi implementować specjalny interfejs.

Pierwszą opcją jest implementacja interfejsu Iterator, który ma metody current() zwracającą bieżącą wartość, key() zwracającą klucz, next() przechodzącą do następnej wartości, rewind() przechodzącą na początek i valid() sprawdzającą, czy jeszcze nie jesteśmy na końcu.

Drugą opcją jest implementacja interfejsu IteratorAggregate, który ma tylko jedną metodę getIterator(). Zwraca ona albo obiekt zastępczy, który będzie zapewniał iterację, albo może reprezentować generator, czyli specjalną funkcję, w której używa się yield do stopniowego zwracania kluczy i wartości:

class Osoba
{
	public function __construct(
		public int $wiek,
	) {
	}
}

class Lista implements IteratorAggregate
{
	private array $osoby = [];

	public function dodajOsobe(Osoba $osoba): void
	{
		$this->osoby[] = $osoba;
	}

	public function getIterator(): Generator
	{
		foreach ($this->osoby as $osoba) {
			yield $osoba;
		}
	}
}

$lista = new Lista;
$lista->dodajOsobe(new Osoba(30));
$lista->dodajOsobe(new Osoba(25));

foreach ($lista as $osoba) {
	echo "Wiek: {$osoba->wiek} lat \n";
}

Dobre praktyki

Kiedy masz już za sobą podstawowe zasady programowania obiektowego, ważne jest, aby skupić się na dobrych praktykach w OOP. Pomogą ci one pisać kod, który jest nie tylko funkcjonalny, ale także czytelny, zrozumiały i łatwy do utrzymania.

  1. Podział odpowiedzialności (Separation of Concerns): Każda klasa powinna mieć jasno zdefiniowaną odpowiedzialność i powinna rozwiązywać tylko jedno główne zadanie. Jeśli klasa robi zbyt wiele rzeczy, może być wskazane podzielenie jej na mniejsze, wyspecjalizowane klasy.
  2. Hermetyzacja (Encapsulation): Dane i metody powinny być jak najbardziej ukryte i dostępne tylko za pośrednictwem zdefiniowanego interfejsu. Pozwoli to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.
  3. Wstrzykiwanie zależności (Dependency Injection): Zamiast tworzyć zależności bezpośrednio w klasie, powinieneś je “wstrzykiwać” z zewnątrz. Aby lepiej zrozumieć tę zasadę, polecamy rozdziały o wstrzykiwaniu zależności.
wersja: 4.0