/
introduction-to-object-oriented-programming.texy
841 lines (609 loc) · 45.1 KB
/
introduction-to-object-oriented-programming.texy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
Въведение в обектно-ориентираното програмиране
**********************************************
.[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()` осигурява достъп до възрастта на лицето.
Псевдопроменливата `$this` се използва вътре в класа за достъп до свойствата и методите на обекта.
Ключовата дума `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,
) {
}
}
```
За пълнота, в допълнение към конструкторите обектите могат да имат деструктори (метод `__destruct`), които се извикват преди обектът да бъде освободен от паметта.
Пространства от имена .[#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 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();
```
Как работи този код?
- Използвахме ключовата дума `extends`, за да разширим класа `Person`, което означава, че класът `Student` наследява всички методи и свойства от `Person`.
- Ключовата дума `parent::` ни позволява да извикваме методи от родителския клас. В този случай извикахме конструктора от класа `Person`, преди да добавим наша собствена функционалност към класа `Student`. И по същия начин - метода на `printInformation()` класа-родител, преди да изведем информацията за учениците.
Наследяването е предназначено за ситуации, в които между класовете има връзка "е а". Например, клас `Student` е клас `Person`. Котката е животно. То ни позволява в случаите, когато в кода очакваме един обект (например "Човек"), да използваме вместо него производен обект (например "Ученик").
От съществено значение е да осъзнаем, че основната цел на наследяването **не е** да предотврати дублирането на кода. Напротив, неправилното използване на наследяването може да доведе до сложен и труден за поддържане код. Ако между класовете няма връзка "е а", трябва да помислим за композиция вместо за наследяване.
Обърнете внимание, че методите `printInformation()` в класовете `Person` и `Student` извеждат малко по-различна информация. Можем да добавим и други класове (например `Employee`), които ще предоставят други реализации на този метод. Възможността обекти от различни класове да реагират на един и същ метод по различни начини се нарича полиморфизъм:
```php
$people = [
new Person(30),
new Student(20, 'Computer Science'),
new Employee(45, 'Director'),
];
foreach ($people as $person) {
$person->printInformation();
}
```
Композиция .[#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 ще хвърли грешка.
Един клас може да имплементира няколко интерфейса едновременно, което е различно от наследяването, при което класът може да наследява само от един клас:
```php
interface Guardian
{
function guardHouse();
}
class Dog implements Animal, Guardian
{
public function makeSound()
{
echo 'Bark';
}
public function guardHouse()
{
echo 'Dog diligently guards the house';
}
}
```
Абстрактни класове .[#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();
}
```
По този начин гарантираме, че нашият код очаква и работи с данни от правилния тип, което ни помага да предотвратим потенциални грешки.
Някои типове не могат да бъдат записани директно в PHP. В този случай те се изписват в коментара phpDoc, който е стандартният формат за документиране на PHP код, започващ с `/**` и завършващ с `*/`. Той ви позволява да добавяте описания на класове, методи и т.н. А също и да се изброяват сложни типове, като се използват така наречените анотации `@var`, `@param` и `@return`. Тези типове след това се използват от инструментите за статичен анализ на кода, но не се проверяват от самия 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;
}
}
```
Сравнение и идентичност .[#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`.
```php
function division(float $a, float $b): float
{
if ($b === 0) {
throw new Exception('Division by zero!');
}
return $a / $b;
}
```
Когато функцията `division()` получи null като втори аргумент, тя хвърля изключение със съобщение за грешка `'Division by zero!'`. За да предотвратим срив на програмата при хвърляне на изключението, го улавяме в блока `try/catch`:
```php
try {
echo division(10, 0);
} catch (Exception $e) {
echo 'Exception caught: '. $e->getMessage();
}
```
Кодът, който може да хвърли изключение, е обвит в блок `try`. Ако изключението бъде хвърлено, изпълнението на кода се премества в блок `catch`, където можем да обработим изключението (например да напишем съобщение за грешка).
След блоковете `try` и `catch` можем да добавим незадължителен блок `finally`, който се изпълнява винаги, независимо дали изключението е хвърлено или не (дори ако използваме `return`, `break` или `continue` в блока `try` или `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
}
```
Можем също така да създадем свои собствени класове за изключения (йерархия), които наследяват класа Exception. Като пример, разгледайте едно просто банково приложение, което позволява депозити и тегления:
```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;
}
}
```
За един блок `try` могат да бъдат зададени няколко блока `catch`, ако очаквате различни видове изключения.
```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.';
}
```
В този пример е важно да се отбележи редът на блоковете `catch`. Тъй като всички изключения се наследяват от `BankingException`, ако имахме този блок първи, всички изключения щяха да бъдат уловени в него, без кодът да достига до следващите блокове `catch`. Затова е важно по-специфичните изключения (т.е. тези, които се наследяват от други) да бъдат по-нагоре в реда на блоковете `catch` от родителските си изключения.
Итерации .[#toc-iterations]
---------------------------
В PHP можете да правите цикли през обекти с помощта на цикъла `foreach`, подобно на цикъла през масив. За да работи това, обектът трябва да реализира специален интерфейс.
Първият вариант е да имплементирате интерфейса `Iterator`, който има методи `current()` за връщане на текущата стойност, `key()` за връщане на ключа, `next()` за преминаване към следващата стойност, `rewind()` за преминаване към началото и `valid()` за проверка дали вече сме в края.
Другият вариант е да реализираме интерфейс `IteratorAggregate`, който има само един метод `getIterator()`. Той или връща заместващ обект, който ще осигури обхождането, или може да бъде генератор, който е специална функция, използваща `yield`, за да връща последователно ключове и стойности:
```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";
}
```
Най-добри практики .[#toc-best-practices]
-----------------------------------------
След като вече сте усвоили основните принципи на обектно-ориентираното програмиране, е изключително важно да се съсредоточите върху най-добрите практики в ООП. Те ще ви помогнат да пишете код, който е не само функционален, но и четим, разбираем и лесен за поддържане.
1) **Отделяне на проблемите**: Всеки клас трябва да има ясно определена отговорност и да се занимава само с една основна задача. Ако даден клас върши твърде много неща, може да е подходящо да го разделите на по-малки, специализирани класове.
2) **Екапсулиране**: Данните и методите трябва да са възможно най-скрити и достъпни само чрез определен интерфейс. Това ви позволява да променяте вътрешната имплементация на даден клас, без да засягате останалата част от кода.
3) **Вкачване на зависимостта**: Вместо да създавате зависимости директно в класа, трябва да ги "инжектирате" отвън. За по-задълбочено разбиране на този принцип препоръчваме [главите, посветени на "Инжектиране на зависимости" (Dependency Injection) |dependency-injection:introduction].