Wprowadzenie do programowania obiektowego

Skrót “OOP” oznacza programowanie obiektowe (Object-Oriented Programming), czyli sposób organizowania i strukturyzowania kodu. OOP pozwala nam patrzeć na program jak na zbiór obiektów komunikujących się ze sobą, a nie jak na sekwencję poleceń i funkcji.

W OOP “obiekt” to jednostka zawierająca dane i funkcje operujące na tych danych. Obiekty tworzone są na podstawie “klas”, które można rozumieć jako plany albo szablony obiektów. Gdy mamy klasę, możemy utworzyć jej “instancję”, czyli konkretny obiekt wykonany z tej klasy.

Zobaczmy, jak możemy utworzyć w PHP prostą klasę. Przy definiowaniu klasy używamy słowa kluczowego “class”, po którym następuje nazwa klasy, a następnie nawiasy klamrowe obejmujące funkcje klasy (zwane “metodami”) i zmienne klasy (zwane “właściwościami” albo “atrybutami”):

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

W tym przykładzie utworzyliśmy klasę o nazwie Car z jedną funkcją (albo “metodą”) o nazwie honk.

Każda klasa powinna rozwiązywać tylko jedno główne zadanie. Jeśli klasa robi zbyt wiele rzeczy, warto podzielić ją na mniejsze, wyspecjalizowane klasy.

Klasy przechowuje się zwykle w osobnych plikach, żeby kod pozostał uporządkowany i łatwy do przeglądania. Nazwa pliku powinna odpowiadać nazwie klasy, więc dla klasy Car nazwą pliku byłoby Car.php.

Przy nazywaniu klas dobrze jest trzymać się konwencji “PascalCase”, czyli każde słowo w nazwie zaczyna się wielką literą i nie ma podkreśleń ani innych separatorów. Metody i właściwości trzymają się konwencji “camelCase”, czyli zaczynają się małą literą.

Niektóre metody w PHP mają specjalne role i są poprzedzone __ (dwoma podkreśleniami). Jedną z najważniejszych metod specjalnych jest “konstruktor” oznaczany jako __construct. Konstruktor to metoda wywoływana automatycznie przy tworzeniu nowej instancji klasy.

Konstruktora używamy często do ustawienia początkowego stanu obiektu. Na przykład przy tworzeniu obiektu reprezentującego osobę możesz użyć konstruktora do ustawienia jej wieku, imienia albo innych atrybutów.

Zobaczmy, jak użyć konstruktora w PHP:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Wypisze: 25

W tym przykładzie klasa Person ma właściwość (zmienną) $age i konstruktor, który tę właściwość ustawia. Metoda howOldAreYou() daje potem dostęp do wieku osoby.

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

Słowo kluczowe new służy do utworzenia nowej instancji klasy. W powyższym przykładzie utworzyliśmy nową osobę w wieku 25 lat.

Możesz też ustawić parametrom konstruktora wartości domyślne, jeśli nie zostaną podane przy tworzeniu obiektu. Na przykład:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // jeśli nie przekazujemy argumentu, nawiasy można pominąć
echo $person->howOldAreYou(); // Wypisze: 20

W tym przykładzie, jeśli przy tworzeniu obiektu Person nie podasz wieku, użyta zostanie wartość domyślna 20.

Miłe jest to, że definicję właściwości wraz z jej inicjalizacją przez konstruktor da się skrócić i uprościć tak:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

Dla kompletności: oprócz konstruktorów obiekty mogą mieć destruktory (metoda __destruct) wywoływane przed zwolnieniem obiektu z pamięci.

Przestrzenie nazw

Przestrzenie nazw pozwalają nam organizować i grupować powiązane klasy, funkcje i stałe, unikając przy tym konfliktów nazw. Możesz myśleć o nich jak o folderach na komputerze, gdzie każdy folder zawiera pliki związane z konkretnym projektem albo tematem.

Przestrzenie nazw są szczególnie przydatne w większych projektach albo przy używaniu bibliotek zewnętrznych, gdzie mogą powstać konflikty nazw klas.

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

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

Jeśli chcesz użyć klasy Car w innym pliku, musisz podać, z której przestrzeni nazw klasa pochodzi:

$car = new Transport\Car;

Dla uproszczenia możesz na początku pliku podać, której klasy z danej przestrzeni nazw chcesz używać, dzięki czemu możesz tworzyć instancje bez podawania pełnej ścieżki:

use Transport\Car;

$car = new Car;

Dziedziczenie

Dziedziczenie to narzędzie programowania obiektowego pozwalające tworzyć nowe klasy na podstawie istniejących, dziedzicząc ich właściwości i metody oraz rozszerzając je albo redefiniując według potrzeb. Dziedziczenie zapewnia ponowne wykorzystanie kodu i hierarchię klas.

Upraszczając, jeśli mamy jedną klasę i chcemy utworzyć inną, wyprowadzoną z niej, ale z pewnymi modyfikacjami, możemy nową klasę “odziedziczyć” po pierwotnej.

W PHP dziedziczenie realizuje się słowem kluczowym extends.

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

Spójrzmy na przykład:

class Person
{
	private $age;

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

	function printInformation()
	{
		echo "Age: {$this->age} years\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		parent::printInformation();
		echo "Field of study: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

Jak działa ten kod?

  • Użyliśmy słowa kluczowego extends do rozszerzenia klasy Person, czyli klasa Student dziedziczy wszystkie metody i właściwości po Person.
  • Słowo kluczowe parent:: pozwala nam wywoływać metody klasy nadrzędnej. W tym przypadku wywołaliśmy konstruktor z klasy Person, zanim dodaliśmy własną funkcjonalność do klasy Student. I podobnie metodę przodka printInformation() przed wypisaniem informacji o studencie.

Dziedziczenie przeznaczone jest do sytuacji, w których między klasami zachodzi relacja “jest”. Na przykład Student jest Person. Kot jest zwierzęciem. Pozwala nam to w miejscach, gdzie w kodzie oczekujemy jednego obiektu (np. “Person”), użyć zamiast niego obiektu wyprowadzonego (np. “Student”).

Ważne jest, żeby zdać sobie sprawę, że głównym celem dziedziczenia nie jest zapobieganie duplikowaniu kodu. Wręcz przeciwnie, niewłaściwe użycie dziedziczenia może prowadzić do złożonego i trudnego w utrzymaniu kodu. Jeśli między klasami nie zachodzi relacja “jest”, powinniśmy rozważyć kompozycję zamiast dziedziczenia.

Zauważ, że metody printInformation() w klasach Person i Student wypisują nieco inne informacje. Możemy dodać kolejne klasy (jak Employee), które dostarczą inne implementacje tej metody. Zdolność obiektów różnych klas do reagowania na tę samą metodę na różne sposoby nazywa się polimorfizmem:

$people = [
	new Person(30),
	new Student(20, 'Computer Science'),
	new Employee(45, 'Director'),
];

foreach ($people as $person) {
	$person->printInformation();
}

Kompozycja

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

Na przykład mamy klasę Engine i klasę Car. Zamiast mówić “samochód jest silnikiem”, mówimy “samochód ma silnik”, co jest typową relacją kompozycji.

class Engine
{
	function start()
	{
		echo 'Engine is running.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo 'The car is ready to drive!';
	}
}

$car = new Car;
$car->start();

Tutaj Car nie ma wszystkich właściwości i metod Engine, ale ma do niego dostęp przez właściwość $engine.

Zaletą kompozycji jest większa elastyczność projektowa i lepsza podatność na przyszłe zmiany.

Widoczność

W PHP możesz definiować “widoczność” właściwości, metod i stałych klasy. Widoczność określa, skąd możesz sięgać po te elementy.

  1. Public: jeśli element jest oznaczony jako public, oznacza to, że możesz sięgać po niego skądkolwiek, także spoza klasy.
  2. Protected: element oznaczony jako protected jest dostępny tylko wewnątrz klasy i wszystkich jej potomków (klas, które po niej dziedziczą).
  3. Private: jeśli element jest private, możesz sięgać po niego tylko z wnętrza klasy, w której został zdefiniowany.

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

Spójrzmy na przykładowy kod:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Działa
		echo $this->protectedProperty;  // Działa
		echo $this->privateProperty;    // Działa
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Działa
// echo $object->protectedProperty;   // Rzuci błąd
// echo $object->privateProperty;     // Rzuci błąd

Kontynuując dziedziczeniem klasy:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Działa
		echo $this->protectedProperty;  // Działa
		// echo $this->privateProperty;   // Rzuci błąd
	}
}

W tym przypadku metoda printProperties() w klasie ChildClass może sięgać po właściwości publiczne i chronione, ale nie może sięgać po prywatne właściwości klasy nadrzędnej.

Dane i metody powinny być jak najbardziej ukryte i dostępne tylko przez zdefiniowany interfejs. Pozwala Ci to zmieniać wewnętrzną implementację klasy bez wpływu na resztę kodu.

Słowo kluczowe final

W PHP możemy użyć słowa kluczowego final, jeśli chcemy zapobiec dziedziczeniu albo nadpisaniu klasy, metody czy stałej. Gdy klasa jest oznaczona jako final, nie da się jej rozszerzyć. Gdy metoda jest oznaczona jako final, nie da się jej nadpisać w klasie potomnej.

Świadomość, że pewna klasa albo metoda nie będzie już modyfikowana, pozwala nam łatwiej wprowadzać zmiany bez obaw o ewentualne konflikty. Możemy na przykład dodać nową metodę bez obawy, że potomek może już mieć metodę o tej samej nazwie, co prowadziłoby do kolizji. Albo możemy zmienić parametry metody, znów bez ryzyka spowodowania niezgodności z nadpisaną metodą u potomka.

final class FinalClass
{
}

// Poniższy kod rzuci błąd, bo nie możemy dziedziczyć po klasie final.
class ChildOfFinalClass extends FinalClass
{
}

W tym przykładzie próba odziedziczenia po klasie final FinalClass zakończy się błędem.

Statyczne właściwości i metody

Gdy mówimy o “statycznych” elementach klasy w PHP, mamy na myśli metody i właściwości należące do samej klasy, a nie do konkretnej instancji klasy. Oznacza to, że nie musisz tworzyć instancji klasy, żeby po nie sięgnąć. Zamiast tego wywołujesz je albo sięgasz po nie bezpośrednio przez nazwę klasy.

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

Używanie statycznych właściwości prowadzi do zaciemnionego kodu pełnego pułapek, więc nigdy nie powinieneś ich używać i przykładu tu nie pokażemy. Metody statyczne są natomiast przydatne. Oto przykład:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

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

// Użycie metody statycznej bez tworzenia instancji klasy
echo Calculator::add(5, 3); // Wynik: 8
echo Calculator::subtract(5, 3); // Wynik: 2

W tym przykładzie utworzyliśmy klasę Calculator z dwiema metodami statycznymi. Metody te możemy wywołać bezpośrednio, bez tworzenia instancji klasy, za pomocą operatora ::. Metody statyczne są szczególnie przydatne dla operacji niezależnych od stanu konkretnej instancji klasy.

Stałe klasy

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

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): void
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Wynik: 4

W tym przykładzie mamy klasę Car ze stałą NumberOfWheels. Przy dostępie do stałej wewnątrz klasy możemy zamiast nazwy klasy użyć słowa kluczowego self.

Interfejsy obiektowe

Interfejsy obiektowe działają jak “kontrakty” dla klas. Jeśli klasa ma implementować interfejs obiektowy, musi zawierać wszystkie metody, które interfejs definiuje. To świetny sposób, żeby zapewnić, że pewne klasy trzymają się tego samego “kontraktu” albo tej samej struktury.

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

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

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

Klasa może implementować wiele interfejsów naraz, co różni się od dziedziczenia, gdzie klasa może dziedziczyć tylko po jednej klasie:

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Bark';
	}

	public function guardHouse()
	{
		echo 'Dog diligently guards the house';
	}
}

Klasy abstrakcyjne

Klasy abstrakcyjne służą jako bazowe szablony dla innych klas, ale nie możesz tworzyć ich instancji bezpośrednio. Zawierają mieszankę metod kompletnych i metod abstrakcyjnych, które nie mają zdefiniowanej treści. Klasy dziedziczące po klasach abstrakcyjnych muszą dostarczyć definicje wszystkich metod abstrakcyjnych przodka.

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

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

W tym przykładzie mamy klasę abstrakcyjną z jedną metodą zwykłą i jedną abstrakcyjną. Potem mamy klasę Child, która dziedziczy po AbstractClass i dostarcza implementację metody abstrakcyjnej.

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

Kontrola typów

W programowaniu kluczowe jest zapewnienie, że dane, z którymi pracujemy, są właściwego typu. W PHP mamy narzędzia dające tę pewność. Weryfikację tego, że dane są właściwego typu, nazywamy “kontrolą typów”.

Typy, które możemy w PHP napotkać:

  1. Typy podstawowe: należą do nich int (liczby całkowite), float (liczby zmiennoprzecinkowe), bool (wartości logiczne), string (ciągi), array (tablice) i null.
  2. Klasy: gdy chcemy, żeby wartość była instancją konkretnej klasy.
  3. Interfejsy: definiują zestaw metod, które klasa musi zaimplementować. Wartość spełniająca interfejs musi mieć te metody.
  4. Typy mieszane: możemy podać, że zmienna może mieć wiele dozwolonych typów.
  5. Void: ten specjalny typ wskazuje, że funkcja albo metoda nie zwraca żadnej wartości.

Zobaczmy, jak zmodyfikować kod, żeby zawierał typy:

class Person
{
	private int $age;

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

	public function printAge(): void
	{
		echo "This person is {$this->age} years old.";
	}
}

/**
 * Funkcja przyjmująca obiekt Person i wypisująca wiek osoby.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

W ten sposób zapewniamy, że nasz kod oczekuje danych właściwego typu i z nimi pracuje, co pomaga nam zapobiegać ewentualnym błędom.

Niektórych typów nie da się w PHP zapisać bezpośrednio. W takim przypadku podaje się je w komentarzu phpDoc, który jest standardowym formatem dokumentowania kodu PHP, zaczynającym się od /** i kończącym */. Pozwala on dodawać opisy klas, metod i tak dalej. A także podawać złożone typy za pomocą tak zwanych adnotacji @var, @param i @return. Typy te wykorzystywane są potem przez narzędzia statycznej analizy kodu, ale samo PHP ich nie sprawdza.

class Registry
{
	/** @var array<Person>  wskazuje, że to tablica obiektów Person */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

Porównanie 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 we właściwościach.
  2. Tożsamość ===: sprawdza, czy to ta sama instancja obiektu.
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, bo mają tę samą wartość
var_dump($car1 === $car2);  // false, bo nie są tą samą instancją
var_dump($car1 === $car3);  // true, bo $car3 to ta sama instancja co $car1

Operator instanceof

Operator instanceof pozwala ustalić, czy dany obiekt jest instancją konkretnej klasy, potomkiem tej klasy albo czy implementuje określony interfejs.

Wyobraźmy sobie, że mamy klasę Person i inną klasę Student, będącą potomkiem Person:

class Person
{
	private int $age;

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

class Student extends Person
{
	private string $major;

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

$student = new Student(20, 'Computer Science');

// Sprawdzamy, czy $student jest instancją klasy Student
var_dump($student instanceof Student);  // Wynik: bool(true)

// Sprawdzamy, czy $student jest instancją klasy Person (bo Student jest potomkiem Person)
var_dump($student instanceof Person);   // Wynik: bool(true)

Z wyników wynika, że obiekt $student uznawany jest za instancję zarówno klasy Student, jak i Person.

Interfejsy płynne

“Fluent Interface” (interfejs płynny) to technika w OOP pozwalająca łączyć metody w łańcuch w jednym wywołaniu. Często upraszcza to i uczytelnia kod.

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

Zobaczmy, jak mógłby wyglądać interfejs płynny do wysyłania e-maili:

public function sendMessage()
{
	$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ą bieżący obiekt ($email), co pozwala nam dokleić za nim kolejną metodę. Na koniec wywołujemy metodę send(), która faktycznie wysyła e-mail.

Dzięki interfejsom płynnym możemy pisać kod intuicyjny i łatwy do odczytania.

Kopiowanie przez clone

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

Jeśli przy kopiowaniu obiektu potrzebujemy zmodyfikować niektóre z jego właściwości, możemy zdefiniować w klasie specjalną metodę __clone(). Metoda ta wywoływana jest automatycznie przy klonowaniu obiektu.

class Sheep
{
	public string $name;

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

	public function __clone()
	{
		$this->name = 'Clone of ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Wypisze: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Wypisze: Clone of Dolly

W tym przykładzie mamy klasę Sheep z jedną właściwością $name. Gdy klonujemy instancję tej klasy, metoda __clone() zapewnia, że imię sklonowanej owcy otrzymuje przedrostek “Clone of”.

Traity

Traity w PHP to narzędzie pozwalające dzielić metody, właściwości i stałe między klasami i zapobiegające duplikowaniu kodu. Możesz myśleć o nich jak o mechanizmie “kopiuj i wklej” (Ctrl-C i Ctrl-V), gdzie zawartość traitu jest “wklejana” do klas. Pozwala Ci to ponownie wykorzystywać kod bez potrzeby tworzenia skomplikowanych hierarchii klas.

Spójrzmy na prosty przykład użycia traitów w PHP:

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Wypisze 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Też wypisze 'Beep beep!'

W tym przykładzie mamy trait o nazwie Honking zawierający jedną metodę honk(). Potem mamy dwie klasy: Car i Truck, z których obie używają traitu Honking. W rezultacie obie klasy “mają” metodę honk() i możemy wywołać ją na obiektach obu klas.

Traity pozwalają łatwo i efektywnie dzielić kod między klasami. Nie wchodzą do hierarchii dziedziczenia, czyli $car instanceof Honking zwróci false.

Wyjątki

Wyjątki w OOP pozwalają nam elegancko obsługiwać błędy i nieoczekiwane sytuacje w naszym kodzie. To obiekty niosące informację o błędzie albo nietypowej sytuacji.

W PHP mamy wbudowaną klasę Exception, która służy jako podstawa wszystkich wyjątków. Ma ona kilka metod pozwalających nam uzyskać więcej informacji o wyjątku, jak komunikat o błędzie, plik i linia, w której doszło do błędu, itd.

Gdy w kodzie dochodzi do błędu, możemy “rzucić” wyjątek słowem kluczowym throw.

function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('Division by zero!');
	}
	return $a / $b;
}

Gdy funkcja division() otrzyma jako drugi argument zero, rzuca wyjątek z komunikatem o błędzie 'Division by zero!'. Żeby przy rzuceniu wyjątku program się nie wysypał, przechwytujemy go w bloku try/catch:

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

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

Za blokami try i catch możemy dodać opcjonalny blok finally, który wykonuje się zawsze, niezależnie od tego, czy wyjątek został rzucony, czy nie (nawet jeśli w bloku try albo catch użyjemy return, break czy continue):

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

Możemy też tworzyć własne klasy wyjątków (hierarchię) dziedziczące po klasie Exception. Jako przykład rozważmy prostą aplikację bankową pozwalającą wpłacać i wypłacać:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

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

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

W tym przykładzie ważna jest kolejność bloków catch. Ponieważ wszystkie wyjątki dziedziczą po BankingException, gdybyśmy mieli ten blok jako pierwszy, wszystkie wyjątki zostałyby w nim przechwycone, a kod nie dotarłby do kolejnych bloków catch. Dlatego ważne jest, żeby bardziej konkretne wyjątki (czyli te dziedziczące po innych) były w kolejności bloków catch wyżej niż ich wyjątki nadrzędne.

Iteracje

W PHP możesz przechodzić obiekty pętlą foreach podobnie jak tablice. Żeby to działało, obiekt musi implementować specjalny interfejs.

Pierwszą możliwością jest zaimplementowanie interfejsu Iterator, który ma metody current() zwracającą bieżącą wartość, key() zwracającą klucz, next() przechodzącą do kolejnej wartości, rewind() przechodzącą na początek i valid() sprawdzającą, czy nie jesteśmy już na końcu.

Drugą możliwością jest zaimplementowanie interfejsu IteratorAggregate, który ma tylko jedną metodę getIterator(). Zwraca ona albo obiekt zastępczy, który zapewni przechodzenie, albo może to być generator, czyli specjalna funkcja używająca yield do zwracania kolejno kluczy i wartości:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

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

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Age: {$person->age} years\n";
}

Dobre praktyki

Gdy masz już za sobą podstawowe zasady programowania obiektowego, kluczowe jest skupienie się na dobrych praktykach w OOP. Pomogą Ci one pisać kod, który jest nie tylko funkcjonalny, ale też czytelny, zrozumiały i łatwy w utrzymaniu.

  1. Separacja odpowiedzialności: każda klasa powinna mieć jasno zdefiniowaną odpowiedzialność i rozwiązywać tylko jedno główne zadanie. Jeśli klasa robi zbyt wiele rzeczy, warto podzielić ją na mniejsze, wyspecjalizowane klasy.
  2. Enkapsulacja: dane i metody powinny być jak najbardziej ukryte i dostępne tylko przez zdefiniowany interfejs. Pozwala Ci to zmieniać wewnętrzną implementację klasy bez wpływu na resztę kodu.
  3. Dependency Injection: zamiast tworzyć zależności bezpośrednio w klasie, powinieneś “wstrzykiwać” je z zewnątrz. Dla głębszego zrozumienia tej zasady zalecamy rozdziały o Dependency Injection.
wersja: 4.x