Nette Documentation Preview

syntax
Въведение в обектно-ориентираното програмиране
**********************************************

.[perex]
Терминът "ООП" означава обектно-ориентирано програмиране, което е начин за организиране и структуриране на кода. ООП ни позволява да разглеждаме една програма като колекция от обекти, които комуникират помежду си, а не като последователност от команди и функции.

В ООП "обект" е единица, която съдържа данни и функции, които работят с тези данни. Обектите се създават въз основа на "класове", които могат да се разбират като проекти или шаблони за обекти. След като разполагаме с клас, можем да създадем негов "екземпляр", който е конкретен обект, създаден от този клас.

Нека разгледаме как можем да създадем прост клас в PHP. Когато дефинираме клас, използваме ключовата дума "class", последвана от името на класа, и след това къдрави скоби, които ограждат функциите на класа (наречени "методи") и променливите на класа (наречени "свойства" или "атрибути"):

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

В този пример създадохме клас на име `Car` с една функция (или "метод"), наречена `honk`.

Всеки клас трябва да реши само една основна задача. Ако един клас решава твърде много задачи, може да е подходящо да го разделите на по-малки, специализирани класове.

Обикновено класовете се съхраняват в отделни файлове, за да се запази организацията на кода и да се улесни навигацията в него. Името на файла трябва да съвпада с името на класа, така че за класа `Car` името на файла ще бъде `Car.php`.

При именуването на класовете е добре да се следва конвенцията "PascalCase", което означава, че всяка дума в името започва с главна буква и няма подчертавания или други разделители. Методите и свойствата следват конвенцията "camelCase", което означава, че започват с малка буква.

Някои методи в PHP имат специални роли и се предхождат от `__` (две подчертавания). Един от най-важните специални методи е "конструкторът", обозначен като `__construct`. Конструкторът е метод, който се извиква автоматично при създаването на нова инстанция на даден клас.

Често използваме конструктора, за да зададем първоначалното състояние на даден обект. Например, когато създавате обект, представляващ човек, може да използвате конструктора, за да зададете неговата възраст, име или други атрибути.

Нека видим как да използваме конструктор в 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
```

В този пример класът `Person` има свойство `$age` и конструктор, който задава това свойство. След това методът `howOldAreYou()` осигурява достъп до възрастта на лицето.

Ключовата дума `new` се използва за създаване на нов екземпляр на даден клас. В горния пример създадохме ново лице на възраст 25 години.

Можете също така да зададете стойности по подразбиране за параметрите на конструктора, ако те не са зададени при създаването на обект. Например:

```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
```

В този пример, ако не посочите възраст при създаването на обект `Person`, ще бъде използвана стойността по подразбиране 20.

И накрая, дефинирането на свойството с неговата инициализация чрез конструктора може да бъде съкратено и опростено по следния начин:

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


Пространства от имена .[#toc-namespaces]
----------------------------------------

Пространствата от имена ни позволяват да организираме и групираме свързани класове, функции и константи, като избягваме конфликти в наименованията. Можете да си ги представите като папки на компютъра, където всяка папка съдържа файлове, свързани с конкретен проект или тема.

Пространствата от имена са особено полезни в по-големи проекти или при използване на библиотеки на трети страни, където могат да възникнат конфликти в наименованията на класовете.

Представете си, че в проекта ви има клас с име `Car` и искате да го поставите в пространство от имена, наречено `Transport`. Ще го направите по следния начин:

```php
namespace Transport;

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

Ако искате да използвате класа `Car` в друг файл, трябва да посочите от кое пространство от имена произхожда класът:

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

За улеснение можете да посочите в началото на файла кой клас от определено пространство от имена искате да използвате, което ви позволява да създавате екземпляри, без да посочвате пълния път:

```php
use Transport\Car;

$car = new Car;
```


Наследяване .[#toc-inheritance]
-------------------------------

Наследяването е инструмент на обектно-ориентираното програмиране, който позволява създаването на нови класове на базата на съществуващи, като се наследяват техните свойства и методи и се разширяват или предефинират при необходимост. Наследяването осигурява повторна използваемост на кода и йерархичност на класовете.

Казано по-просто, ако имаме един клас и искаме да създадем друг, производен от него, но с някои модификации, можем да "наследим" новия клас от оригиналния.

В PHP наследяването се реализира с помощта на ключовата дума `extends`.

Нашият клас `Person` съхранява информация за възрастта. Можем да имаме друг клас, `Student`, който разширява `Person` и добавя информация за областта на обучение.

Нека разгледаме един пример:

```php
class Person
{
	private $age;

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

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

class Student extends Person
{
	private $fieldOfStudy;

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

	function printInformation()
	{
		echo 'Age of student: ', $this->howOldAreYou();
		echo 'Field of study: ', $this->fieldOfStudy;
	}
}

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

Как работи този код?

- Използвахме ключовата дума `extends`, за да разширим класа `Person`, което означава, че класът `Student` наследява всички методи и свойства от `Person`.

- Ключовата дума `parent::` ни позволява да извикваме методи от родителския клас. В този случай извикахме конструктора от класа `Person`, преди да добавим наша собствена функционалност към класа `Student`.

Наследяването е предназначено за ситуации, в които между класовете има връзка "е а". Например, клас `Student` е клас `Person`. Котката е животно. То ни позволява в случаите, когато в кода очакваме един обект (например "Човек"), да използваме вместо него производен обект (например "Ученик").

От съществено значение е да осъзнаем, че основната цел на наследяването **не е** да предотврати дублирането на кода. Напротив, неправилното използване на наследяването може да доведе до сложен и труден за поддържане код. Ако между класовете няма връзка "е а", трябва да помислим за композиция вместо за наследяване.


Композиция .[#toc-composition]
------------------------------

Композицията е техника, при която вместо да наследяваме свойства и методи от друг клас, просто използваме негова инстанция в нашия клас. Това ни позволява да комбинираме функционалностите и свойствата на няколко класа, без да създаваме сложни структури на наследяване.

Например, имаме клас `Engine` и клас `Car`. Вместо да кажем "Колата е двигател", казваме "Колата има двигател", което е типична композиционна връзка.

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

Тук `Car` не притежава всички свойства и методи на `Engine`, но има достъп до тях чрез свойството `$engine`.

Предимството на композицията е по-голямата гъвкавост на дизайна и по-добрата адаптивност към бъдещи промени.


Видимост .[#toc-visibility]
---------------------------

В PHP можете да дефинирате "видимост" за свойствата, методите и константите на класа. Видимостта определя къде можете да получите достъп до тези елементи.

1. **Публично:** Ако даден елемент е отбелязан като `public`, това означава, че имате достъп до него отвсякъде, дори извън класа.

2. **Защитен:** Елемент, маркиран като `protected`, е достъпен само в рамките на класа и всички негови наследници (класове, които наследяват от него).

3. **Приватно:** Ако даден елемент е обозначен като `private`, можете да имате достъп до него само в рамките на класа, в който е дефиниран.

Ако не посочите видимост, PHP автоматично ще я зададе на `public`.

Нека разгледаме примерен код:

```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
```

Продължаваме с наследяването на класове:

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

В този случай методът `printProperties()` в `ChildClass` има достъп до публичните и защитените свойства, но няма достъп до частните свойства на родителския клас.

Данните и методите трябва да са възможно най-скрити и достъпни само чрез определен интерфейс. Това ви позволява да променяте вътрешната имплементация на класа, без да засягате останалата част от кода.


Финална ключова дума .[#toc-final-keyword]
------------------------------------------

В PHP можем да използваме ключовата дума `final`, ако искаме да предотвратим наследяването или пренаписването на даден клас, метод или константа. Когато даден клас е маркиран като `final`, той не може да бъде разширяван. Когато даден метод е маркиран като `final`, той не може да бъде презаписван в подклас.

Знанието, че даден клас или метод вече няма да бъде променян, ни позволява да правим промени по-лесно, без да се притесняваме за потенциални конфликти. Например можем да добавим нов метод, без да се страхуваме, че някой потомък вече има метод със същото име, което ще доведе до колизия. Или пък можем да променим параметрите на метод, отново без риск да предизвикаме несъответствие с надграден метод в наследник.

```php
final class FinalClass
{
}

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

В този пример опитът за наследяване от крайния клас `FinalClass` ще доведе до грешка.


Статични свойства и методи .[#toc-static-properties-and-methods]
----------------------------------------------------------------

Когато говорим за "статични" елементи на даден клас в PHP, имаме предвид методи и свойства, които принадлежат на самия клас, а не на конкретна инстанция на класа. Това означава, че не е необходимо да създавате инстанция на класа, за да имате достъп до тях. Вместо това ги извиквате или получавате достъп до тях директно чрез името на класа.

Имайте предвид, че тъй като статичните елементи принадлежат на класа, а не на неговите инстанции, не можете да използвате псевдопроменливата `$this` вътре в статичните методи.

Използването на статични свойства води до [затрупан код, пълен с капани |dependency-injection:global-state], затова никога не трябва да ги използвате и тук няма да показваме пример. От друга страна, статичните методи са полезни. Ето един пример:

```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
```

В този пример създадохме клас `Calculator` с два статични метода. Можем да извикаме тези методи директно, без да създаваме инстанция на класа, като използваме оператора `::`. Статичните методи са особено полезни за операции, които не зависят от състоянието на конкретна инстанция на класа.


Константи на класа .[#toc-class-constants]
------------------------------------------

В рамките на класовете имаме възможност да дефинираме константи. Константите са стойности, които никога не се променят по време на изпълнението на програмата. За разлика от променливите, стойността на една константа остава непроменена.

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

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

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

В този пример имаме клас `Car` с константата `NumberOfWheels`. Когато получаваме достъп до константата вътре в класа, можем да използваме ключовата дума `self` вместо името на класа.


Интерфейси на обекти .[#toc-object-interfaces]
----------------------------------------------

Интерфейсите на обектите действат като "договори" за класовете. Ако един клас трябва да имплементира обектния интерфейс, той трябва да съдържа всички методи, които интерфейсът дефинира. Това е чудесен начин да се гарантира, че определени класове се придържат към един и същ "договор" или структура.

В PHP интерфейсите се дефинират с помощта на ключовата дума `interface`. Всички методи, дефинирани в даден интерфейс, са публични (`public`). Когато даден клас имплементира интерфейс, той използва ключовата дума `implements`.

```php
interface Animal
{
	function makeSound();
}

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

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

Ако даден клас имплементира интерфейс, но не са дефинирани всички очаквани методи, PHP ще хвърли грешка. Един клас може да имплементира няколко интерфейса едновременно, което е различно от наследяването, при което класът може да наследява само от един клас.


Абстрактни класове .[#toc-abstract-classes]
-------------------------------------------

Абстрактните класове служат като базови шаблони за други класове, но не можете да създавате техни екземпляри директно. Те съдържат комбинация от пълни методи и абстрактни методи, които нямат определено съдържание. Класовете, които наследяват от абстрактни класове, трябва да предоставят дефиниции за всички абстрактни методи от родителя.

Използваме ключовата дума `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();
```

В този пример имаме абстрактен клас с един обикновен и един абстрактен метод. След това имаме клас `Child`, който наследява от `AbstractClass` и предоставя имплементация за абстрактния метод.


Проверка на типа .[#toc-type-checking]
--------------------------------------

В програмирането е изключително важно да се уверим, че данните, с които работим, са от правилния тип. В PHP разполагаме с инструменти, които осигуряват тази сигурност. Проверката на това дали данните са от правилния тип се нарича "проверка на типа".

Типове, които можем да срещнем в PHP:

1. **Основни типове**: Те включват `int` (цели числа), `float` (числа с плаваща запетая), `bool` (булеви стойности), `string` (низове), `array` (масиви) и `null`.
2. **Класове**: Когато искаме дадена стойност да бъде инстанция на определен клас.
3. **Интерфейси**: Определя набор от методи, които даден клас трябва да имплементира. Стойност, която отговаря на даден интерфейс, трябва да има тези методи.
4. **Смесени типове**: Можем да укажем, че дадена променлива може да има няколко разрешени типа.
5. **Void**: Този специален тип указва, че функцията или методът не връщат никаква стойност.

Нека да видим как да модифицираме кода, за да включим типовете:

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

По този начин гарантираме, че нашият код очаква и работи с данни от правилния тип, което ни помага да предотвратим потенциални грешки.


Сравнение и идентичност .[#toc-comparison-and-identity]
-------------------------------------------------------

В PHP можете да сравнявате обекти по два начина:

1. Сравняване на стойности `==`: Проверява се дали обектите са от един и същи клас и имат еднакви стойности в свойствата си.
2. Сравнение на идентичността `===`: Проверява се дали става въпрос за една и съща инстанция на обекта.

```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
```


Операторът на `instanceof` .[#toc-the-instanceof-operator]
----------------------------------------------------------

Операторът `instanceof` ви позволява да определите дали даден обект е инстанция на определен клас, потомък на този клас или дали реализира определен интерфейс.

Представете си, че имаме клас `Person` и друг клас `Student`, който е потомък на `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)
```

От изводите е видно, че обектът `$student` се счита за инстанция на двата класа `Student` и `Person`.


Флуентни интерфейси .[#toc-fluent-interfaces]
---------------------------------------------

"Флуентен интерфейс" е техника в ООП, която позволява верижно свързване на методи с едно извикване. Това често опростява и изяснява кода.

Ключовият елемент на плавния интерфейс е, че всеки метод във веригата връща референция към текущия обект. Това се постига чрез използване на `return $this;` в края на метода. Този стил на програмиране често се свързва с методите, наречени "setters", които задават стойностите на свойствата на даден обект.

Нека да видим как може да изглежда един плавен интерфейс за изпращане на имейли:

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

В този пример методите `setFrom()`, `setRecipient()` и `setMessage()` се използват за задаване на съответните стойности (изпращач, получател, съдържание на съобщението). След задаването на всяка от тези стойности методите връщат текущия обект (`$email`), което ни позволява да верижно да използваме друг метод след него. Накрая извикваме метода `send()`, който всъщност изпраща електронното писмо.

Благодарение на плавните интерфейси можем да пишем код, който е интуитивен и лесно четим.


Копиране с `clone` .[#toc-copying-with-clone]
---------------------------------------------

В PHP можем да създадем копие на даден обект, като използваме оператора `clone`. По този начин получаваме нов екземпляр с идентично съдържание.

Ако при копирането на обект трябва да променим някои от неговите свойства, можем да дефинираме специален метод `__clone()` в класа. Този метод се извиква автоматично, когато обектът се клонира.

```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
```

В този пример имаме клас `Sheep` с едно свойство `$name`. Когато клонираме екземпляр на този клас, методът `__clone()` гарантира, че името на клонираната овца ще получи префикс "Clone of".


Признаци .[#toc-traits]
-----------------------

Чертите в PHP са инструмент, който позволява споделянето на методи, свойства и константи между класовете и предотвратява дублирането на код. Можете да си ги представите като механизъм за "копиране и поставяне" (Ctrl-C и Ctrl-V), при който съдържанието на дадена черта се "вмъква" в класовете. Това ви позволява да използвате повторно кода, без да се налага да създавате сложни йерархии от класове.

Нека разгледаме прост пример за използване на черти в 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!'
```

В този пример имаме черта, наречена `Honking`, която съдържа един метод `honk()`. След това имаме два класа: `Car` и `Truck`, като и двата използват чертата `Honking`. В резултат на това и двата класа "притежават" метода `honk()` и можем да го извикаме върху обекти и от двата класа.

Характеристиките позволяват лесно и ефективно споделяне на код между класовете. Те не влизат в йерархията на наследяване, т.е. `$car instanceof Honking` ще върне `false`.


Изключения
----------

Изключенията в ООП ни позволяват да обработваме и управляваме грешки, които могат да възникнат по време на изпълнението на нашия код. По същество те са обекти, предназначени да регистрират грешки или неочаквани ситуации във вашата програма.

В PHP имаме вградения клас `Exception` за тези обекти. Той има няколко метода, които ни позволяват да получим повече информация за изключението, като например съобщение за грешка, файла и реда, където е възникнала грешката, и т.н.

Когато възникне проблем, можем да "хвърлим" изключение (с помощта на `throw`). Ако искаме да "хванем" и обработим това изключение, използваме блоковете `try` и `catch`.

Нека видим как работи това:

```php
try {
	throw new Exception('Message explaining the reason for the exception');

	// This code won't execute
	echo 'I am a message that nobody will read';

} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}
```

Важно е да се отбележи, че изключението може да бъде хвърлено по-надълбоко, по време на извикването на други методи.

За един блок `try` могат да бъдат зададени няколко блока `catch`, ако очаквате различни видове изключения.

Можем също така да създадем йерархия на изключенията, при която всеки клас на изключението наследява от предишния. Като пример, разгледайте едно просто банково приложение, което позволява депозити и тегления:

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

$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.';
}
```

В този пример е важно да се отбележи редът на блоковете `catch`. Тъй като всички изключения се наследяват от `BankingException`, ако имахме този блок първи, всички изключения щяха да бъдат уловени в него, без кодът да достига до следващите блокове `catch`. Затова е важно по-специфичните изключения (т.е. тези, които се наследяват от други) да бъдат по-нагоре в реда на блоковете `catch` от родителските си изключения.


Най-добри практики .[#toc-best-practices]
-----------------------------------------

След като вече сте усвоили основните принципи на обектно-ориентираното програмиране, е изключително важно да се съсредоточите върху най-добрите практики в ООП. Те ще ви помогнат да пишете код, който е не само функционален, но и четим, разбираем и лесен за поддържане.

1) **Отделяне на проблемите**: Всеки клас трябва да има ясно определена отговорност и да се занимава само с една основна задача. Ако даден клас върши твърде много неща, може да е подходящо да го разделите на по-малки, специализирани класове.
2) **Екапсулиране**: Данните и методите трябва да са възможно най-скрити и достъпни само чрез определен интерфейс. Това ви позволява да променяте вътрешната имплементация на даден клас, без да засягате останалата част от кода.
3) **Вкачване на зависимостта**: Вместо да създавате зависимости директно в класа, трябва да ги "инжектирате" отвън. За по-задълбочено разбиране на този принцип препоръчваме [главите, посветени на "Инжектиране на зависимости" (Dependency Injection) |dependency-injection:introduction].

Въведение в обектно-ориентираното програмиране

Терминът „ООП“ означава обектно-ориентирано програмиране, което е начин за организиране и структуриране на кода. ООП ни позволява да разглеждаме една програма като колекция от обекти, които комуникират помежду си, а не като последователност от команди и функции.

В ООП „обект“ е единица, която съдържа данни и функции, които работят с тези данни. Обектите се създават въз основа на „класове“, които могат да се разбират като проекти или шаблони за обекти. След като разполагаме с клас, можем да създадем негов „екземпляр“, който е конкретен обект, създаден от този клас.

Нека разгледаме как можем да създадем прост клас в PHP. Когато дефинираме клас, използваме ключовата дума „class“, последвана от името на класа, и след това къдрави скоби, които ограждат функциите на класа (наречени „методи“) и променливите на класа (наречени „свойства“ или „атрибути“):

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

В този пример създадохме клас на име Car с една функция (или „метод“), наречена honk.

Всеки клас трябва да реши само една основна задача. Ако един клас решава твърде много задачи, може да е подходящо да го разделите на по-малки, специализирани класове.

Обикновено класовете се съхраняват в отделни файлове, за да се запази организацията на кода и да се улесни навигацията в него. Името на файла трябва да съвпада с името на класа, така че за класа Car името на файла ще бъде Car.php.

При именуването на класовете е добре да се следва конвенцията „PascalCase“, което означава, че всяка дума в името започва с главна буква и няма подчертавания или други разделители. Методите и свойствата следват конвенцията „camelCase“, което означава, че започват с малка буква.

Някои методи в PHP имат специални роли и се предхождат от __ (две подчертавания). Един от най-важните специални методи е „конструкторът“, обозначен като __construct. Конструкторът е метод, който се извиква автоматично при създаването на нова инстанция на даден клас.

Често използваме конструктора, за да зададем първоначалното състояние на даден обект. Например, когато създавате обект, представляващ човек, може да използвате конструктора, за да зададете неговата възраст, име или други атрибути.

Нека видим как да използваме конструктор в 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

В този пример класът Person има свойство $age и конструктор, който задава това свойство. След това методът howOldAreYou() осигурява достъп до възрастта на лицето.

Ключовата дума new се използва за създаване на нов екземпляр на даден клас. В горния пример създадохме ново лице на възраст 25 години.

Можете също така да зададете стойности по подразбиране за параметрите на конструктора, ако те не са зададени при създаването на обект. Например:

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

В този пример, ако не посочите възраст при създаването на обект Person, ще бъде използвана стойността по подразбиране 20.

И накрая, дефинирането на свойството с неговата инициализация чрез конструктора може да бъде съкратено и опростено по следния начин:

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

Пространства от имена

Пространствата от имена ни позволяват да организираме и групираме свързани класове, функции и константи, като избягваме конфликти в наименованията. Можете да си ги представите като папки на компютъра, където всяка папка съдържа файлове, свързани с конкретен проект или тема.

Пространствата от имена са особено полезни в по-големи проекти или при използване на библиотеки на трети страни, където могат да възникнат конфликти в наименованията на класовете.

Представете си, че в проекта ви има клас с име Car и искате да го поставите в пространство от имена, наречено Transport. Ще го направите по следния начин:

namespace Transport;

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

Ако искате да използвате класа Car в друг файл, трябва да посочите от кое пространство от имена произхожда класът:

$car = new Transport\Car;

За улеснение можете да посочите в началото на файла кой клас от определено пространство от имена искате да използвате, което ви позволява да създавате екземпляри, без да посочвате пълния път:

use Transport\Car;

$car = new Car;

Наследяване

Наследяването е инструмент на обектно-ориентираното програмиране, който позволява създаването на нови класове на базата на съществуващи, като се наследяват техните свойства и методи и се разширяват или предефинират при необходимост. Наследяването осигурява повторна използваемост на кода и йерархичност на класовете.

Казано по-просто, ако имаме един клас и искаме да създадем друг, производен от него, но с някои модификации, можем да „наследим“ новия клас от оригиналния.

В PHP наследяването се реализира с помощта на ключовата дума extends.

Нашият клас Person съхранява информация за възрастта. Можем да имаме друг клас, Student, който разширява Person и добавя информация за областта на обучение.

Нека разгледаме един пример:

class Person
{
	private $age;

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

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

class Student extends Person
{
	private $fieldOfStudy;

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

	function printInformation()
	{
		echo 'Age of student: ', $this->howOldAreYou();
		echo 'Field of study: ', $this->fieldOfStudy;
	}
}

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

Как работи този код?

  • Използвахме ключовата дума extends, за да разширим класа Person, което означава, че класът Student наследява всички методи и свойства от Person.
  • Ключовата дума parent:: ни позволява да извикваме методи от родителския клас. В този случай извикахме конструктора от класа Person, преди да добавим наша собствена функционалност към класа Student.

Наследяването е предназначено за ситуации, в които между класовете има връзка „е а“. Например, клас Student е клас Person. Котката е животно. То ни позволява в случаите, когато в кода очакваме един обект (например „Човек“), да използваме вместо него производен обект (например „Ученик“).

От съществено значение е да осъзнаем, че основната цел на наследяването не е да предотврати дублирането на кода. Напротив, неправилното използване на наследяването може да доведе до сложен и труден за поддържане код. Ако между класовете няма връзка „е а“, трябва да помислим за композиция вместо за наследяване.

Композиция

Композицията е техника, при която вместо да наследяваме свойства и методи от друг клас, просто използваме негова инстанция в нашия клас. Това ни позволява да комбинираме функционалностите и свойствата на няколко класа, без да създаваме сложни структури на наследяване.

Например, имаме клас Engine и клас Car. Вместо да кажем „Колата е двигател“, казваме „Колата има двигател“, което е типична композиционна връзка.

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

Тук Car не притежава всички свойства и методи на Engine, но има достъп до тях чрез свойството $engine.

Предимството на композицията е по-голямата гъвкавост на дизайна и по-добрата адаптивност към бъдещи промени.

Видимост

В PHP можете да дефинирате „видимост“ за свойствата, методите и константите на класа. Видимостта определя къде можете да получите достъп до тези елементи.

  1. Публично: Ако даден елемент е отбелязан като public, това означава, че имате достъп до него отвсякъде, дори извън класа.
  2. Защитен: Елемент, маркиран като protected, е достъпен само в рамките на класа и всички негови наследници (класове, които наследяват от него).
  3. Приватно: Ако даден елемент е обозначен като private, можете да имате достъп до него само в рамките на класа, в който е дефиниран.

Ако не посочите видимост, PHP автоматично ще я зададе на public.

Нека разгледаме примерен код:

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

Продължаваме с наследяването на класове:

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

В този случай методът printProperties() в ChildClass има достъп до публичните и защитените свойства, но няма достъп до частните свойства на родителския клас.

Данните и методите трябва да са възможно най-скрити и достъпни само чрез определен интерфейс. Това ви позволява да променяте вътрешната имплементация на класа, без да засягате останалата част от кода.

Финална ключова дума

В PHP можем да използваме ключовата дума final, ако искаме да предотвратим наследяването или пренаписването на даден клас, метод или константа. Когато даден клас е маркиран като final, той не може да бъде разширяван. Когато даден метод е маркиран като final, той не може да бъде презаписван в подклас.

Знанието, че даден клас или метод вече няма да бъде променян, ни позволява да правим промени по-лесно, без да се притесняваме за потенциални конфликти. Например можем да добавим нов метод, без да се страхуваме, че някой потомък вече има метод със същото име, което ще доведе до колизия. Или пък можем да променим параметрите на метод, отново без риск да предизвикаме несъответствие с надграден метод в наследник.

final class FinalClass
{
}

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

В този пример опитът за наследяване от крайния клас FinalClass ще доведе до грешка.

Статични свойства и методи

Когато говорим за „статични“ елементи на даден клас в PHP, имаме предвид методи и свойства, които принадлежат на самия клас, а не на конкретна инстанция на класа. Това означава, че не е необходимо да създавате инстанция на класа, за да имате достъп до тях. Вместо това ги извиквате или получавате достъп до тях директно чрез името на класа.

Имайте предвид, че тъй като статичните елементи принадлежат на класа, а не на неговите инстанции, не можете да използвате псевдопроменливата $this вътре в статичните методи.

Използването на статични свойства води до затрупан код, пълен с капани, затова никога не трябва да ги използвате и тук няма да показваме пример. От друга страна, статичните методи са полезни. Ето един пример:

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

В този пример създадохме клас Calculator с два статични метода. Можем да извикаме тези методи директно, без да създаваме инстанция на класа, като използваме оператора ::. Статичните методи са особено полезни за операции, които не зависят от състоянието на конкретна инстанция на класа.

Константи на класа

В рамките на класовете имаме възможност да дефинираме константи. Константите са стойности, които никога не се променят по време на изпълнението на програмата. За разлика от променливите, стойността на една константа остава непроменена.

class Car
{
	public const NumberOfWheels = 4;

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

echo Car::NumberOfWheels;  // Output: 4

В този пример имаме клас Car с константата NumberOfWheels. Когато получаваме достъп до константата вътре в класа, можем да използваме ключовата дума self вместо името на класа.

Интерфейси на обекти

Интерфейсите на обектите действат като „договори“ за класовете. Ако един клас трябва да имплементира обектния интерфейс, той трябва да съдържа всички методи, които интерфейсът дефинира. Това е чудесен начин да се гарантира, че определени класове се придържат към един и същ „договор“ или структура.

В PHP интерфейсите се дефинират с помощта на ключовата дума interface. Всички методи, дефинирани в даден интерфейс, са публични (public). Когато даден клас имплементира интерфейс, той използва ключовата дума implements.

interface Animal
{
	function makeSound();
}

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

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

Ако даден клас имплементира интерфейс, но не са дефинирани всички очаквани методи, PHP ще хвърли грешка. Един клас може да имплементира няколко интерфейса едновременно, което е различно от наследяването, при което класът може да наследява само от един клас.

Абстрактни класове

Абстрактните класове служат като базови шаблони за други класове, но не можете да създавате техни екземпляри директно. Те съдържат комбинация от пълни методи и абстрактни методи, които нямат определено съдържание. Класовете, които наследяват от абстрактни класове, трябва да предоставят дефиниции за всички абстрактни методи от родителя.

Използваме ключовата дума 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();

В този пример имаме абстрактен клас с един обикновен и един абстрактен метод. След това имаме клас Child, който наследява от AbstractClass и предоставя имплементация за абстрактния метод.

Проверка на типа

В програмирането е изключително важно да се уверим, че данните, с които работим, са от правилния тип. В PHP разполагаме с инструменти, които осигуряват тази сигурност. Проверката на това дали данните са от правилния тип се нарича „проверка на типа“.

Типове, които можем да срещнем в PHP:

  1. Основни типове: Те включват int (цели числа), float (числа с плаваща запетая), bool (булеви стойности), string (низове), array (масиви) и null.
  2. Класове: Когато искаме дадена стойност да бъде инстанция на определен клас.
  3. Интерфейси: Определя набор от методи, които даден клас трябва да имплементира. Стойност, която отговаря на даден интерфейс, трябва да има тези методи.
  4. Смесени типове: Можем да укажем, че дадена променлива може да има няколко разрешени типа.
  5. Void: Този специален тип указва, че функцията или методът не връщат никаква стойност.

Нека да видим как да модифицираме кода, за да включим типовете:

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

По този начин гарантираме, че нашият код очаква и работи с данни от правилния тип, което ни помага да предотвратим потенциални грешки.

Сравнение и идентичност

В PHP можете да сравнявате обекти по два начина:

  1. Сравняване на стойности ==: Проверява се дали обектите са от един и същи клас и имат еднакви стойности в свойствата си.
  2. Сравнение на идентичността ===: Проверява се дали става въпрос за една и съща инстанция на обекта.
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

Операторът на instanceof

Операторът instanceof ви позволява да определите дали даден обект е инстанция на определен клас, потомък на този клас или дали реализира определен интерфейс.

Представете си, че имаме клас Person и друг клас Student, който е потомък на 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)

От изводите е видно, че обектът $student се счита за инстанция на двата класа Student и Person.

Флуентни интерфейси

„Флуентен интерфейс“ е техника в ООП, която позволява верижно свързване на методи с едно извикване. Това често опростява и изяснява кода.

Ключовият елемент на плавния интерфейс е, че всеки метод във веригата връща референция към текущия обект. Това се постига чрез използване на return $this; в края на метода. Този стил на програмиране често се свързва с методите, наречени „setters“, които задават стойностите на свойствата на даден обект.

Нека да видим как може да изглежда един плавен интерфейс за изпращане на имейли:

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

В този пример методите setFrom(), setRecipient() и setMessage() се използват за задаване на съответните стойности (изпращач, получател, съдържание на съобщението). След задаването на всяка от тези стойности методите връщат текущия обект ($email), което ни позволява да верижно да използваме друг метод след него. Накрая извикваме метода send(), който всъщност изпраща електронното писмо.

Благодарение на плавните интерфейси можем да пишем код, който е интуитивен и лесно четим.

Копиране с clone

В PHP можем да създадем копие на даден обект, като използваме оператора clone. По този начин получаваме нов екземпляр с идентично съдържание.

Ако при копирането на обект трябва да променим някои от неговите свойства, можем да дефинираме специален метод __clone() в класа. Този метод се извиква автоматично, когато обектът се клонира.

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

В този пример имаме клас Sheep с едно свойство $name. Когато клонираме екземпляр на този клас, методът __clone() гарантира, че името на клонираната овца ще получи префикс „Clone of“.

Признаци

Чертите в PHP са инструмент, който позволява споделянето на методи, свойства и константи между класовете и предотвратява дублирането на код. Можете да си ги представите като механизъм за „копиране и поставяне“ (Ctrl-C и Ctrl-V), при който съдържанието на дадена черта се „вмъква“ в класовете. Това ви позволява да използвате повторно кода, без да се налага да създавате сложни йерархии от класове.

Нека разгледаме прост пример за използване на черти в 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!'

В този пример имаме черта, наречена Honking, която съдържа един метод honk(). След това имаме два класа: Car и Truck, като и двата използват чертата Honking. В резултат на това и двата класа „притежават“ метода honk() и можем да го извикаме върху обекти и от двата класа.

Характеристиките позволяват лесно и ефективно споделяне на код между класовете. Те не влизат в йерархията на наследяване, т.е. $car instanceof Honking ще върне false.

Изключения

Изключенията в ООП ни позволяват да обработваме и управляваме грешки, които могат да възникнат по време на изпълнението на нашия код. По същество те са обекти, предназначени да регистрират грешки или неочаквани ситуации във вашата програма.

В PHP имаме вградения клас Exception за тези обекти. Той има няколко метода, които ни позволяват да получим повече информация за изключението, като например съобщение за грешка, файла и реда, където е възникнала грешката, и т.н.

Когато възникне проблем, можем да „хвърлим“ изключение (с помощта на throw). Ако искаме да „хванем“ и обработим това изключение, използваме блоковете try и catch.

Нека видим как работи това:

try {
	throw new Exception('Message explaining the reason for the exception');

	// This code won't execute
	echo 'I am a message that nobody will read';

} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

Важно е да се отбележи, че изключението може да бъде хвърлено по-надълбоко, по време на извикването на други методи.

За един блок try могат да бъдат зададени няколко блока catch, ако очаквате различни видове изключения.

Можем също така да създадем йерархия на изключенията, при която всеки клас на изключението наследява от предишния. Като пример, разгледайте едно просто банково приложение, което позволява депозити и тегления:

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

$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.';
}

В този пример е важно да се отбележи редът на блоковете catch. Тъй като всички изключения се наследяват от BankingException, ако имахме този блок първи, всички изключения щяха да бъдат уловени в него, без кодът да достига до следващите блокове catch. Затова е важно по-специфичните изключения (т.е. тези, които се наследяват от други) да бъдат по-нагоре в реда на блоковете catch от родителските си изключения.

Най-добри практики

След като вече сте усвоили основните принципи на обектно-ориентираното програмиране, е изключително важно да се съсредоточите върху най-добрите практики в ООП. Те ще ви помогнат да пишете код, който е не само функционален, но и четим, разбираем и лесен за поддържане.

  1. Отделяне на проблемите: Всеки клас трябва да има ясно определена отговорност и да се занимава само с една основна задача. Ако даден клас върши твърде много неща, може да е подходящо да го разделите на по-малки, специализирани класове.
  2. Екапсулиране: Данните и методите трябва да са възможно най-скрити и достъпни само чрез определен интерфейс. Това ви позволява да променяте вътрешната имплементация на даден клас, без да засягате останалата част от кода.
  3. Вкачване на зависимостта: Вместо да създавате зависимости директно в класа, трябва да ги „инжектирате“ отвън. За по-задълбочено разбиране на този принцип препоръчваме главите, посветени на „Инжектиране на зависимости“ (Dependency Injection).