Nette Documentation Preview

syntax
Wprowadzenie do programowania obiektowego
*****************************************

.[perex]
Termin "OOP" oznacza programowanie obiektowe (Object-Oriented Programming), które jest sposobem organizacji i strukturyzacji kodu. OOP pozwala nam postrzegać program jako zbiór obiektów, które komunikują się ze sobą, a nie jako sekwencję poleceń i funkcji.

W OOP "obiekt" to jednostka zawierająca dane i funkcje, które działają na tych danych. Obiekty są tworzone w oparciu o "klasy", które można rozumieć jako plany lub szablony obiektów. Gdy mamy już klasę, możemy utworzyć jej "instancję", która jest konkretnym obiektem utworzonym z tej klasy.

Przyjrzyjmy się jak możemy stworzyć prostą klasę w PHP. Podczas definiowania klasy używamy słowa kluczowego "class", po którym następuje nazwa klasy, a następnie nawiasy klamrowe, które otaczają funkcje klasy (zwane "metodami") i zmienne klasy (zwane "właściwościami" lub "atrybutami"):

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

W tym przykładzie utworzyliśmy klasę o nazwie `Car` z jedną funkcją (lub "metodą") o nazwie `honk`.

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

Klasy są zwykle przechowywane w oddzielnych plikach, aby kod był uporządkowany i łatwy w nawigacji. Nazwa pliku powinna być zgodna z nazwą klasy, więc dla klasy `Car` nazwa pliku brzmiałaby `Car.php`.

Podczas nazywania klas dobrze jest przestrzegać konwencji "PascalCase", co oznacza, że 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 są zgodne z konwencją "camelCase", co oznacza, że zaczynają się od małej litery.

Niektóre metody w PHP mają specjalne role i są poprzedzone `__` (dwa podkreślenia). Jedną z najważniejszych metod specjalnych jest "konstruktor", oznaczony jako `__construct`. Konstruktor jest metodą, która jest automatycznie wywoływana podczas tworzenia nowej instancji klasy.

Często używamy konstruktora do ustawienia początkowego stanu obiektu. Na przykład, podczas tworzenia obiektu reprezentującego osobę, można użyć konstruktora, aby ustawić jej wiek, imię lub inne atrybuty.

Zobaczmy jak używać konstruktora w PHP:

```php
class Person
{
	private $age;

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

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

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

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

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

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

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

```php
class Person
{
	private $age;

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

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

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20
```

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

Fajną rzeczą jest to, że definicję właściwości z jej inicjalizacją za pomocą konstruktora można skrócić i uprościć w następujący sposób:

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

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


Przestrzenie nazw .[#toc-namespaces]
------------------------------------

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

Przestrzenie nazw są szczególnie przydatne w większych projektach lub podczas korzystania z bibliotek innych firm, gdzie mogą wystąpić konflikty nazewnictwa klas.

Wyobraź sobie, że masz w projekcie klasę o nazwie `Car` i chcesz umieścić ją w przestrzeni nazw o nazwie `Transport`. Można to zrobić w następujący sposób:

```php
namespace Transport;

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

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

```php
$car = new Transport\Car;
```

Dla uproszczenia można określić na początku pliku, której klasy z danej przestrzeni nazw chcemy użyć, co pozwala na tworzenie instancji bez podawania pełnej ścieżki:

```php
use Transport\Car;

$car = new Car;
```


Dziedziczenie .[#toc-inheritance]
---------------------------------

Dziedziczenie jest narzędziem programowania obiektowego, które umożliwia tworzenie nowych klas na podstawie istniejących, dziedziczenie ich właściwości i metod oraz rozszerzanie lub redefiniowanie ich w razie potrzeby. Dziedziczenie zapewnia możliwość ponownego wykorzystania kodu i hierarchię klas.

Mówiąc prościej, jeśli mamy jedną klasę i chcemy utworzyć inną, pochodną od niej, ale z pewnymi modyfikacjami, możemy "odziedziczyć" nową klasę z oryginalnej.

W PHP dziedziczenie jest implementowane za pomocą słowa kluczowego `extends`.

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

Spójrzmy na przykład:

```php
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`, aby rozszerzyć klasę `Person`, co oznacza, że klasa `Student` dziedziczy wszystkie metody i właściwości z `Person`.

- Słowo kluczowe `parent::` pozwala nam wywoływać metody z klasy nadrzędnej. W tym przypadku wywołaliśmy konstruktor z klasy `Person` przed dodaniem naszej własnej funkcjonalności do klasy `Student`. I podobnie, metodę przodka `printInformation()` przed wyświetleniem informacji o uczniu.

Dziedziczenie jest przeznaczone dla sytuacji, w których istnieje relacja "is a" między klasami. Na przykład, klasa `Student` jest klasą `Person`. Kot jest zwierzęciem. Pozwala nam to w przypadkach, w których oczekujemy jednego obiektu (np. "Osoba") w kodzie na użycie obiektu pochodnego (np. "Student").

Ważne jest, aby zdać sobie sprawę, że głównym celem dziedziczenia **nie jest** zapobieganie powielaniu kodu. Wręcz przeciwnie, niewłaściwe wykorzystanie dziedziczenia może prowadzić do skomplikowanego i trudnego w utrzymaniu kodu. Jeśli między klasami nie ma relacji "is a", powinniśmy rozważyć kompozycję zamiast dziedziczenia.

Należy zauważyć, że metody `printInformation()` w klasach `Person` i `Student` wyświetlają nieco inne informacje. Możemy też dodać inne klasy (takie jak `Employee`), które zapewnią inne implementacje tej metody. Zdolność obiektów różnych klas do reagowania na tę samą metodę na różne sposoby nazywana jest polimorfizmem:

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

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


Kompozycja .[#toc-composition]
------------------------------

Kompozycja to technika, w której zamiast dziedziczyć właściwości i metody z innej klasy, 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.

```php
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();
```

W tym przypadku klasa `Car` nie ma wszystkich właściwości i metod klasy `Engine`, ale ma do nich dostęp poprzez właściwość `$engine`.

Zaletą kompozycji jest większa elastyczność projektu i lepsza zdolność adaptacji do przyszłych zmian.


Widoczność .[#toc-visibility]
-----------------------------

W PHP można zdefiniować "widoczność" dla właściwości, metod i stałych klasy. Widoczność określa, gdzie można uzyskać dostęp do tych elementów.

1. Jeśli element jest oznaczony jako `public`, oznacza to, że można uzyskać do niego dostęp z dowolnego miejsca, nawet spoza klasy.

2. Element oznaczony jako `protected` jest dostępny tylko w obrębie klasy i wszystkich jej klas potomnych (klas, które po niej dziedziczą).

3. Jeśli element jest `private`, można uzyskać do niego dostęp tylko w obrębie 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:

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

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

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error
```

Kontynuacja dziedziczenia klas:

```php
class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}
```

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

Dane i metody powinny być jak najbardziej ukryte i dostępne tylko poprzez zdefiniowany interfejs. Pozwala to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.


Końcowe słowo kluczowe .[#toc-final-keyword]
--------------------------------------------

W PHP możemy użyć słowa kluczowego `final`, jeśli chcemy zapobiec dziedziczeniu lub nadpisywaniu klasy, metody lub stałej. Gdy klasa jest oznaczona jako `final`, nie może zostać rozszerzona. Gdy metoda jest oznaczona jako `final`, nie może zostać nadpisana w podklasie.

Świadomość, że dana klasa lub metoda nie będzie już modyfikowana, pozwala nam łatwiej wprowadzać zmiany bez obawy o potencjalne konflikty. Na przykład, możemy dodać nową metodę bez obawy, że potomek może już mieć metodę o tej samej nazwie, co doprowadzi do kolizji. Możemy też zmienić parametry metody, ponownie bez ryzyka spowodowania niespójności z nadpisaną metodą w metodzie potomnej.

```php
final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}
```

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


Statyczne właściwości i metody .[#toc-static-properties-and-methods]
--------------------------------------------------------------------

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

Należy pamiętać, że ponieważ elementy statyczne należą do klasy, a nie do jej instancji, nie można używać pseudo-zmiennej `$this` wewnątrz metod statycznych.

Używanie właściwości statycznych prowadzi do [zaciemniania kodu pełnego pułapek |dependency-injection:global-state], więc nigdy nie powinieneś ich używać, a my nie pokażemy tutaj przykładu. Z drugiej strony, metody statyczne są użyteczne. Oto przykład:

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

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

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2
```

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


Stałe klasowe .[#toc-class-constants]
-------------------------------------

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

```php
class Car
{
	public const NumberOfWheels = 4;

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

echo Car::NumberOfWheels;  // Output: 4
```

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


Interfejsy obiektów .[#toc-object-interfaces]
---------------------------------------------

Interfejsy obiektowe działają jak "kontrakty" dla klas. Jeśli klasa ma zaimplementować interfejs obiektowy, musi zawierać wszystkie metody zdefiniowane przez interfejs. Jest to świetny sposób na zapewnienie, że niektóre klasy przestrzegają tego samego "kontraktu" lub struktury.

W PHP interfejsy definiowane są za pomocą słowa kluczowego `interface`. Wszystkie metody zdefiniowane w interfejsie są publiczne (`public`). Gdy klasa implementuje interfejs, używa słowa kluczowego `implements`.

```php
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 zgłosi błąd.

Klasa może implementować wiele interfejsów jednocześnie, co różni się od dziedziczenia, gdzie klasa może dziedziczyć tylko z jednej klasy:

```php
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 .[#toc-abstract-classes]
-------------------------------------------

Klasy abstrakcyjne służą jako szablony bazowe dla innych klas, ale nie można bezpośrednio tworzyć ich instancji. Zawierają one mieszankę kompletnych metod i metod abstrakcyjnych, które nie mają zdefiniowanej zawartości. Klasy dziedziczące po klasach abstrakcyjnych muszą zawierać definicje wszystkich metod abstrakcyjnych z klasy nadrzędnej.

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

```php
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ą zwykłą i jedną abstrakcyjną metodą. Następnie mamy klasę `Child`, która dziedziczy po `AbstractClass` i zapewnia implementację metody abstrakcyjnej.

Czym różnią się interfejsy od klas abstrakcyjnych? Klasy abstrakcyjne mogą zawierać zarówno metody abstrakcyjne, jak i konkretne, podczas gdy interfejsy definiują tylko metody, które klasa musi zaimplementować, ale nie zapewniają implementacji. Klasa może dziedziczyć tylko z jednej klasy abstrakcyjnej, ale może implementować dowolną liczbę interfejsów.


Sprawdzanie typu .[#toc-type-checking]
--------------------------------------

W programowaniu kluczowe jest upewnienie się, że dane, z którymi pracujemy, są poprawnego typu. W PHP mamy narzędzia, które to zapewniają. Weryfikacja poprawności typu danych nazywana jest "sprawdzaniem typu".

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

1. **Podstawowe typy**: Należą do nich `int` (liczby całkowite), `float` (liczby zmiennoprzecinkowe), `bool` (wartości logiczne), `string` (ciągi znaków), `array` (tablice) i `null`.
2. **Klasy**: Gdy chcemy, aby wartość była instancją określonej klasy.
3. **Interfaces**: Definiuje zestaw metod, które klasa musi zaimplementować. Wartość, która spełnia interfejs, musi mieć te metody.
4. **Typ mieszany**: Możemy określić, że zmienna może mieć wiele dozwolonych typów.
5. **Void**: Ten specjalny typ wskazuje, że funkcja lub metoda nie zwraca żadnej wartości.

Zobaczmy, jak zmodyfikować kod, aby uwzględnić typy:

```php
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.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}
```

W ten sposób upewniamy się, że nasz kod oczekuje i działa z danymi odpowiedniego typu, pomagając nam zapobiegać potencjalnym błędom.

Niektóre typy nie mogą być zapisane bezpośrednio w PHP. W takim przypadku są one wymienione w komentarzu phpDoc, który jest standardowym formatem dokumentowania kodu PHP, zaczynając od `/**` i kończąc na `*/`. Pozwala on na dodawanie opisów klas, metod itp. A także wylistować złożone typy za pomocą tak zwanych adnotacji `@var`, `@param` i `@return`. Typy te są następnie wykorzystywane przez narzędzia do statycznej analizy kodu, ale nie są sprawdzane przez sam PHP.

```php
class Registry
{
	/** @var array<Person>  indicates that it's an array of Person objects */
	private array $persons = [];

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


Porównanie i tożsamość .[#toc-comparison-and-identity]
------------------------------------------------------

W PHP można 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 jest to ta sama instancja obiektu.

```php
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, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1
```


Operator `instanceof` .[#toc-the-instanceof-operator]
-----------------------------------------------------

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

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

```php
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');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)
```

Z danych wyjściowych wynika, że obiekt `$student` jest uważany za instancję zarówno klasy `Student`, jak i `Person`.


Płynne interfejsy .[#toc-fluent-interfaces]
-------------------------------------------

"Fluent Interface" to technika w OOP, która pozwala na łączenie metod w jednym wywołaniu. Często upraszcza to i wyjaśnia kod.

Kluczowym elementem płynnego interfejsu jest to, że każda metoda w łańcuchu zwraca odniesienie do bieżącego obiektu. Osiąga się to za pomocą `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.

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

```php
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żywane do ustawiania odpowiednich wartości (nadawca, odbiorca, treść wiadomości). Po ustawieniu każdej z tych wartości, metody zwracają bieżący obiekt (`$email`), pozwalając nam na łańcuchowanie kolejnej metody po nim. Na koniec wywołujemy metodę `send()`, która faktycznie wysyła wiadomość e-mail.

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


Kopiowanie z `clone` .[#toc-copying-with-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 musimy zmodyfikować niektóre z jego właściwości, możemy zdefiniować specjalną metodę `__clone()` w klasie. Metoda ta jest automatycznie wywoływana, gdy obiekt jest klonowany.

```php
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";  // Outputs: Dolly

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

W tym przykładzie mamy klasę `Sheep` z jedną właściwością `$name`. Kiedy klonujemy instancję tej klasy, metoda `__clone()` zapewnia, że nazwa sklonowanej owcy otrzyma przedrostek "Clone of".


Cechy .[#toc-traits]
--------------------

Traity w PHP są narzędziem, które pozwala na współdzielenie metod, właściwości i stałych pomiędzy klasami i zapobiega duplikacji kodu. Można o nich myśleć jak o mechanizmie "kopiuj-wklej" (Ctrl-C i Ctrl-V), w którym zawartość cechy jest "wklejana" do klas. Pozwala to na ponowne wykorzystanie kodu bez konieczności tworzenia skomplikowanych hierarchii klas.

Przyjrzyjmy się prostemu przykładowi wykorzystania cech w PHP:

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

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

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

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'
```

W tym przykładzie mamy cechę o nazwie `Honking`, która zawiera jedną metodę `honk()`. Następnie mamy dwie klasy: `Car` i `Truck`, z których obie używają cechy `Honking`. W rezultacie obie klasy "mają" metodę `honk()` i możemy ją wywołać na obiektach obu klas.

Cechy pozwalają na łatwe i efektywne współdzielenie kodu pomiędzy klasami. Nie wchodzą one w hierarchię dziedziczenia, tzn. `$car instanceof Honking` zwróci `false`.


Wyjątki
-------

Wyjątki w OOP pozwalają nam z wdziękiem obsługiwać błędy i nieoczekiwane sytuacje w naszym kodzie. Są to obiekty, które przenoszą 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.

Gdy w kodzie wystąpi błąd, możemy "rzucić" wyjątek za pomocą słowa kluczowego `throw`.

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

Gdy funkcja `division()` otrzyma null jako drugi argument, rzuca wyjątek z komunikatem o błędzie `'Division by zero!'`. Aby zapobiec awarii programu, gdy wyjątek zostanie rzucony, uwięzimy go w bloku `try/catch`:

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

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

Po blokach `try` i `catch` możemy dodać opcjonalny blok `finally`, który jest zawsze wykonywany niezależnie od tego, czy wyjątek został rzucony, czy nie (nawet jeśli użyjemy `return`, `break` lub `continue` w bloku `try` lub `catch` ):

```php
try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// Code that is always executed whether the exception has been thrown or not
}
```

Możemy również tworzyć własne klasy wyjątków (hierarchie), które dziedziczą po klasie Exception. Jako przykład rozważmy prostą aplikację bankową, która umożliwia wpłaty i wypłaty:

```php
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;
	}
}
```

Można określić wiele bloków `catch` dla pojedynczego bloku `try`, jeśli spodziewane są różne typy wyjątków.

```php
$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żne jest, aby zwrócić uwagę na kolejność bloków `catch`. Ponieważ wszystkie wyjątki dziedziczą z `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, aby bardziej specyficzne wyjątki (tj. te, które dziedziczą po innych) znajdowały się wyżej w kolejności bloków `catch` niż ich wyjątki nadrzędne.


Iteracje .[#toc-iterations]
---------------------------

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

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

Inną opcją jest zaimplementowanie interfejsu `IteratorAggregate`, który ma tylko jedną metodę `getIterator()`. Zwraca ona albo obiekt zastępczy, który zapewni przechodzenie, albo może być generatorem, który jest specjalną funkcją, która używa `yield` do sekwencyjnego zwracania kluczy i wartości:

```php
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";
}
```


Najlepsze praktyki .[#toc-best-practices]
-----------------------------------------

Po opanowaniu podstawowych zasad programowania obiektowego ważne jest, aby skupić się na najlepszych praktykach OOP. Pomogą ci one pisać kod, który jest nie tylko funkcjonalny, ale także czytelny, zrozumiały i łatwy w utrzymaniu.

1) **Separation of Concerns**: Każda klasa powinna mieć jasno określoną odpowiedzialność i powinna zajmować się tylko jednym podstawowym zadaniem. Jeśli klasa robi zbyt wiele rzeczy, może być właściwe podzielenie jej na mniejsze, wyspecjalizowane klasy.
2) **Ekapsułkowanie**: Dane i metody powinny być jak najbardziej ukryte i dostępne tylko poprzez zdefiniowany interfejs. Pozwala to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.
3) **Wstrzykiwanie zależności**: Zamiast tworzyć zależności bezpośrednio w klasie, należy "wstrzykiwać" je z zewnątrz. Aby lepiej zrozumieć tę zasadę, zalecamy zapoznanie się z [rozdziałami dotyczącymi wst |dependency-injection:introduction]rzykiwania zależności.

Wprowadzenie do programowania obiektowego

Termin „OOP“ oznacza programowanie obiektowe (Object-Oriented Programming), które jest sposobem organizacji i strukturyzacji kodu. OOP pozwala nam postrzegać program jako zbiór obiektów, które komunikują się ze sobą, a nie jako sekwencję poleceń i funkcji.

W OOP „obiekt“ to jednostka zawierająca dane i funkcje, które działają na tych danych. Obiekty są tworzone w oparciu o „klasy“, które można rozumieć jako plany lub szablony obiektów. Gdy mamy już klasę, możemy utworzyć jej „instancję“, która jest konkretnym obiektem utworzonym z tej klasy.

Przyjrzyjmy się jak możemy stworzyć prostą klasę w PHP. Podczas definiowania klasy używamy słowa kluczowego „class“, po którym następuje nazwa klasy, a następnie nawiasy klamrowe, które otaczają funkcje klasy (zwane „metodami“) i zmienne klasy (zwane „właściwościami“ lub „atrybutami“):

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

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

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

Klasy są zwykle przechowywane w oddzielnych plikach, aby kod był uporządkowany i łatwy w nawigacji. Nazwa pliku powinna być zgodna z nazwą klasy, więc dla klasy Car nazwa pliku brzmiałaby Car.php.

Podczas nazywania klas dobrze jest przestrzegać konwencji „PascalCase“, co oznacza, że 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 są zgodne z konwencją „camelCase“, co oznacza, że zaczynają się od małej litery.

Niektóre metody w PHP mają specjalne role i są poprzedzone __ (dwa podkreślenia). Jedną z najważniejszych metod specjalnych jest „konstruktor“, oznaczony jako __construct. Konstruktor jest metodą, która jest automatycznie wywoływana podczas tworzenia nowej instancji klasy.

Często używamy konstruktora do ustawienia początkowego stanu obiektu. Na przykład, podczas tworzenia obiektu reprezentującego osobę, można użyć konstruktora, aby ustawić jej wiek, imię lub inne atrybuty.

Zobaczmy jak używać 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(); // Outputs: 25

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

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

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

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

class Person
{
	private $age;

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

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

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20

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

Fajną rzeczą jest to, że definicję właściwości z jej inicjalizacją za pomocą konstruktora można skrócić i uprościć w następujący sposób:

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

Dla kompletności, oprócz konstruktorów, obiekty mogą mieć destruktory (metoda __destruct), które są 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 jednocześnie konfliktów nazewnictwa. Można o nich myśleć jak o folderach na komputerze, gdzie każdy folder zawiera pliki związane z konkretnym projektem lub tematem.

Przestrzenie nazw są szczególnie przydatne w większych projektach lub podczas korzystania z bibliotek innych firm, gdzie mogą wystąpić konflikty nazewnictwa klas.

Wyobraź sobie, że masz w projekcie klasę o nazwie Car i chcesz umieścić ją w przestrzeni nazw o nazwie Transport. Można to zrobić w następujący sposób:

namespace Transport;

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

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

$car = new Transport\Car;

Dla uproszczenia można określić na początku pliku, której klasy z danej przestrzeni nazw chcemy użyć, co pozwala na tworzenie instancji bez podawania pełnej ścieżki:

use Transport\Car;

$car = new Car;

Dziedziczenie

Dziedziczenie jest narzędziem programowania obiektowego, które umożliwia tworzenie nowych klas na podstawie istniejących, dziedziczenie ich właściwości i metod oraz rozszerzanie lub redefiniowanie ich w razie potrzeby. Dziedziczenie zapewnia możliwość ponownego wykorzystania kodu i hierarchię klas.

Mówiąc prościej, jeśli mamy jedną klasę i chcemy utworzyć inną, pochodną od niej, ale z pewnymi modyfikacjami, możemy „odziedziczyć“ nową klasę z oryginalnej.

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

Nasza klasa Person przechowuje informacje o wieku. Możemy mieć inną klasę, Student, która rozszerza Person i dodaje informacje 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, aby rozszerzyć klasę Person, co oznacza, że klasa Student dziedziczy wszystkie metody i właściwości z Person.
  • Słowo kluczowe parent:: pozwala nam wywoływać metody z klasy nadrzędnej. W tym przypadku wywołaliśmy konstruktor z klasy Person przed dodaniem naszej własnej funkcjonalności do klasy Student. I podobnie, metodę przodka printInformation() przed wyświetleniem informacji o uczniu.

Dziedziczenie jest przeznaczone dla sytuacji, w których istnieje relacja „is a“ między klasami. Na przykład, klasa Student jest klasą Person. Kot jest zwierzęciem. Pozwala nam to w przypadkach, w których oczekujemy jednego obiektu (np. „Osoba“) w kodzie na użycie obiektu pochodnego (np. „Student“).

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

Należy zauważyć, że metody printInformation() w klasach Person i Student wyświetlają nieco inne informacje. Możemy też dodać inne klasy (takie jak Employee), które zapewnią inne implementacje tej metody. Zdolność obiektów różnych klas do reagowania na tę samą metodę na różne sposoby nazywana jest 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 z innej klasy, 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();

W tym przypadku klasa Car nie ma wszystkich właściwości i metod klasy Engine, ale ma do nich dostęp poprzez właściwość $engine.

Zaletą kompozycji jest większa elastyczność projektu i lepsza zdolność adaptacji do przyszłych zmian.

Widoczność

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

  1. Jeśli element jest oznaczony jako public, oznacza to, że można uzyskać do niego dostęp z dowolnego miejsca, nawet spoza klasy.
  2. Element oznaczony jako protected jest dostępny tylko w obrębie klasy i wszystkich jej klas potomnych (klas, które po niej dziedziczą).
  3. Jeśli element jest private, można uzyskać do niego dostęp tylko w obrębie 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 VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

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

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Kontynuacja dziedziczenia klas:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

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

Dane i metody powinny być jak najbardziej ukryte i dostępne tylko poprzez zdefiniowany interfejs. Pozwala to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.

Końcowe słowo kluczowe

W PHP możemy użyć słowa kluczowego final, jeśli chcemy zapobiec dziedziczeniu lub nadpisywaniu klasy, metody lub stałej. Gdy klasa jest oznaczona jako final, nie może zostać rozszerzona. Gdy metoda jest oznaczona jako final, nie może zostać nadpisana w podklasie.

Świadomość, że dana klasa lub metoda nie będzie już modyfikowana, pozwala nam łatwiej wprowadzać zmiany bez obawy o potencjalne konflikty. Na przykład, możemy dodać nową metodę bez obawy, że potomek może już mieć metodę o tej samej nazwie, co doprowadzi do kolizji. Możemy też zmienić parametry metody, ponownie bez ryzyka spowodowania niespójności z nadpisaną metodą w metodzie potomnej.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

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

Statyczne właściwości i metody

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

Należy pamiętać, że ponieważ elementy statyczne należą do klasy, a nie do jej instancji, nie można używać pseudo-zmiennej $this wewnątrz metod statycznych.

Używanie właściwości statycznych prowadzi do zaciemniania kodu pełnego pułapek, więc nigdy nie powinieneś ich używać, a my nie pokażemy tutaj przykładu. Z drugiej strony, metody statyczne są użyteczne. Oto przykład:

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

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

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

W tym przykładzie utworzyliśmy klasę Calculator z dwiema metodami statycznymi. Możemy wywołać te metody bezpośrednio bez tworzenia instancji klasy za pomocą operatora ::. Metody statyczne są szczególnie przydatne w przypadku 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 nie zmieniają się podczas wykonywania programu. W przeciwieństwie do zmiennych, wartość stałej pozostaje taka sama.

class Car
{
	public const NumberOfWheels = 4;

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

echo Car::NumberOfWheels;  // Output: 4

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

Interfejsy obiektów

Interfejsy obiektowe działają jak „kontrakty“ dla klas. Jeśli klasa ma zaimplementować interfejs obiektowy, musi zawierać wszystkie metody zdefiniowane przez interfejs. Jest to świetny sposób na zapewnienie, że niektóre klasy przestrzegają tego samego „kontraktu“ lub struktury.

W PHP interfejsy definiowane są za pomocą słowa kluczowego 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 zgłosi błąd.

Klasa może implementować wiele interfejsów jednocześnie, co różni się od dziedziczenia, gdzie klasa może dziedziczyć tylko z jednej klasy:

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 szablony bazowe dla innych klas, ale nie można bezpośrednio tworzyć ich instancji. Zawierają one mieszankę kompletnych metod i metod abstrakcyjnych, które nie mają zdefiniowanej zawartości. Klasy dziedziczące po klasach abstrakcyjnych muszą zawierać definicje wszystkich metod abstrakcyjnych z klasy nadrzędnej.

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ą zwykłą i jedną abstrakcyjną metodą. Następnie mamy klasę Child, która dziedziczy po AbstractClass i zapewnia implementację metody abstrakcyjnej.

Czym różnią się interfejsy od klas abstrakcyjnych? Klasy abstrakcyjne mogą zawierać zarówno metody abstrakcyjne, jak i konkretne, podczas gdy interfejsy definiują tylko metody, które klasa musi zaimplementować, ale nie zapewniają implementacji. Klasa może dziedziczyć tylko z jednej klasy abstrakcyjnej, ale może implementować dowolną liczbę interfejsów.

Sprawdzanie typu

W programowaniu kluczowe jest upewnienie się, że dane, z którymi pracujemy, są poprawnego typu. W PHP mamy narzędzia, które to zapewniają. Weryfikacja poprawności typu danych nazywana jest „sprawdzaniem typu“.

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

  1. Podstawowe typy: Należą do nich int (liczby całkowite), float (liczby zmiennoprzecinkowe), bool (wartości logiczne), string (ciągi znaków), array (tablice) i null.
  2. Klasy: Gdy chcemy, aby wartość była instancją określonej klasy.
  3. Interfaces: Definiuje zestaw metod, które klasa musi zaimplementować. Wartość, która spełnia interfejs, musi mieć te metody.
  4. Typ mieszany: Możemy określić, że zmienna może mieć wiele dozwolonych typów.
  5. Void: Ten specjalny typ wskazuje, że funkcja lub metoda nie zwraca żadnej wartości.

Zobaczmy, jak zmodyfikować kod, aby uwzględnić 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.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

W ten sposób upewniamy się, że nasz kod oczekuje i działa z danymi odpowiedniego typu, pomagając nam zapobiegać potencjalnym błędom.

Niektóre typy nie mogą być zapisane bezpośrednio w PHP. W takim przypadku są one wymienione w komentarzu phpDoc, który jest standardowym formatem dokumentowania kodu PHP, zaczynając od /** i kończąc na */. Pozwala on na dodawanie opisów klas, metod itp. A także wylistować złożone typy za pomocą tak zwanych adnotacji @var, @param i @return. Typy te są następnie wykorzystywane przez narzędzia do statycznej analizy kodu, ale nie są sprawdzane przez sam PHP.

class Registry
{
	/** @var array<Person>  indicates that it's an array of Person objects */
	private array $persons = [];

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

Porównanie i tożsamość

W PHP można 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 jest 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, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

Operator instanceof

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

Wyobraźmy sobie, że mamy klasę Person i inną klasę Student, która jest potomkiem klasy 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');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

Z danych wyjściowych wynika, że obiekt $student jest uważany za instancję zarówno klasy Student, jak i Person.

Płynne interfejsy

„Fluent Interface“ to technika w OOP, która pozwala na łączenie metod w jednym wywołaniu. Często upraszcza to i wyjaśnia kod.

Kluczowym elementem płynnego interfejsu jest to, że każda metoda w łańcuchu zwraca odniesienie do bieżącego obiektu. Osiąga się to za pomocą 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.

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

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żywane do ustawiania odpowiednich wartości (nadawca, odbiorca, treść wiadomości). Po ustawieniu każdej z tych wartości, metody zwracają bieżący obiekt ($email), pozwalając nam na łańcuchowanie kolejnej metody po nim. Na koniec wywołujemy metodę send(), która faktycznie wysyła wiadomość e-mail.

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

Kopiowanie z 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 musimy zmodyfikować niektóre z jego właściwości, możemy zdefiniować specjalną metodę __clone() w klasie. Metoda ta jest automatycznie wywoływana, gdy obiekt jest klonowany.

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";  // Outputs: Dolly

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

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

Cechy

Traity w PHP są narzędziem, które pozwala na współdzielenie metod, właściwości i stałych pomiędzy klasami i zapobiega duplikacji kodu. Można o nich myśleć jak o mechanizmie „kopiuj-wklej“ (Ctrl-C i Ctrl-V), w którym zawartość cechy jest „wklejana“ do klas. Pozwala to na ponowne wykorzystanie kodu bez konieczności tworzenia skomplikowanych hierarchii klas.

Przyjrzyjmy się prostemu przykładowi wykorzystania cech w PHP:

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

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

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

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

W tym przykładzie mamy cechę o nazwie Honking, która zawiera jedną metodę honk(). Następnie mamy dwie klasy: Car i Truck, z których obie używają cechy Honking. W rezultacie obie klasy „mają“ metodę honk() i możemy ją wywołać na obiektach obu klas.

Cechy pozwalają na łatwe i efektywne współdzielenie kodu pomiędzy klasami. Nie wchodzą one w hierarchię dziedziczenia, tzn. $car instanceof Honking zwróci false.

Wyjątki

Wyjątki w OOP pozwalają nam z wdziękiem obsługiwać błędy i nieoczekiwane sytuacje w naszym kodzie. Są to obiekty, które przenoszą 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.

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

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

Gdy funkcja division() otrzyma null jako drugi argument, rzuca wyjątek z komunikatem o błędzie 'Division by zero!'. Aby zapobiec awarii programu, gdy wyjątek zostanie rzucony, uwięzimy 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, jest zawijany w blok try. Jeśli wyjątek zostanie rzucony, wykonanie kodu przenosi się do bloku catch, gdzie możemy obsłużyć wyjątek (np. napisać komunikat o błędzie).

Po blokach try i catch możemy dodać opcjonalny blok finally, który jest zawsze wykonywany niezależnie od tego, czy wyjątek został rzucony, czy nie (nawet jeśli użyjemy return, break lub continue w bloku try lub catch ):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// Code that is always executed whether the exception has been thrown or not
}

Możemy również tworzyć własne klasy wyjątków (hierarchie), które dziedziczą po klasie Exception. Jako przykład rozważmy prostą aplikację bankową, która umożliwia wpłaty i wypłaty:

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;
	}
}

Można określić wiele bloków catch dla pojedynczego bloku try, jeśli spodziewane są różne typy 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żne jest, aby zwrócić uwagę na kolejność bloków catch. Ponieważ wszystkie wyjątki dziedziczą z 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, aby bardziej specyficzne wyjątki (tj. te, które dziedziczą po innych) znajdowały się wyżej w kolejności bloków catch niż ich wyjątki nadrzędne.

Iteracje

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

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

Inną opcją jest zaimplementowanie interfejsu IteratorAggregate, który ma tylko jedną metodę getIterator(). Zwraca ona albo obiekt zastępczy, który zapewni przechodzenie, albo może być generatorem, który jest specjalną funkcją, która używa yield do sekwencyjnego zwracania 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";
}

Najlepsze praktyki

Po opanowaniu podstawowych zasad programowania obiektowego ważne jest, aby skupić się na najlepszych praktykach OOP. Pomogą ci one pisać kod, który jest nie tylko funkcjonalny, ale także czytelny, zrozumiały i łatwy w utrzymaniu.

  1. Separation of Concerns: Każda klasa powinna mieć jasno określoną odpowiedzialność i powinna zajmować się tylko jednym podstawowym zadaniem. Jeśli klasa robi zbyt wiele rzeczy, może być właściwe podzielenie jej na mniejsze, wyspecjalizowane klasy.
  2. Ekapsułkowanie: Dane i metody powinny być jak najbardziej ukryte i dostępne tylko poprzez zdefiniowany interfejs. Pozwala to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.
  3. Wstrzykiwanie zależności: Zamiast tworzyć zależności bezpośrednio w klasie, należy „wstrzykiwać“ je z zewnątrz. Aby lepiej zrozumieć tę zasadę, zalecamy zapoznanie się z rozdziałami dotyczącymi wstrzykiwania zależności.