-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathsmartobject.texy
More file actions
240 lines (180 loc) · 13.5 KB
/
smartobject.texy
File metadata and controls
240 lines (180 loc) · 13.5 KB
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
SmartObject
***********
.[perex]
SmartObject годами улучшал поведение объектов в PHP. Начиная с версии PHP 8.4, все его функции уже стали частью самого PHP, тем самым завершив свою историческую миссию быть пионером современного объектного подхода в PHP.
Установка:
```shell
composer require nette/utils
```
SmartObject был создан в 2007 году как революционное решение недостатков тогдашней объектной модели PHP. В то время, когда PHP страдал от множества проблем с объектным дизайном, он принес значительное улучшение и упрощение работы для разработчиков. Он стал легендарной частью фреймворка Nette. Он предлагал функциональность, которую PHP получил лишь много лет спустя — от контроля доступа к свойствам объектов до сложных синтаксических конструкций. С приходом PHP 8.4 он завершил свою историческую миссию, так как все его функции стали нативной частью языка. Он опередил развитие PHP на удивительные 17 лет.
Технически SmartObject прошел интересный путь развития. Изначально он был реализован как класс `Nette\Object`, от которого другие классы наследовали необходимую функциональность. Принципиальное изменение произошло с PHP 5.4, который принес поддержку трейтов. Это позволило трансформировать его в трейт `Nette\SmartObject`, что принесло большую гибкость — разработчики могли использовать функциональность и в классах, которые уже наследовались от другого класса. В то время как исходный класс `Nette\Object` исчез с приходом PHP 7.2 (который запретил именовать классы словом `Object`), трейт `Nette\SmartObject` продолжает существовать.
Давайте рассмотрим свойства, которые когда-то предлагали `Nette\Object`, а позже `Nette\SmartObject`. Каждая из этих функций в свое время представляла собой значительный шаг вперед в области объектно-ориентированного программирования в PHP.
Согласованные состояния ошибок
------------------------------
Одной из самых острых проблем раннего PHP было несогласованное поведение при работе с объектами. `Nette\Object` внес порядок и предсказуемость в этот хаос. Посмотрим, как выглядело исходное поведение PHP:
```php
echo $obj->undeclared; // E_NOTICE, позже E_WARNING
$obj->undeclared = 1; // проходит тихо без сообщения
$obj->unknownMethod(); // Fatal error (неперехватываемая с помощью try/catch)
```
Fatal error завершал приложение без возможности как-либо отреагировать. Тихая запись в несуществующие члены без предупреждения могла привести к серьезным ошибкам, которые было трудно обнаружить. `Nette\Object` перехватывал все эти случаи и выбрасывал исключение `MemberAccessException`, что позволяло программистам реагировать на ошибки и решать их.
```php
echo $obj->undeclared; // выбрасывает Nette\MemberAccessException
$obj->undeclared = 1; // выбрасывает Nette\MemberAccessException
$obj->unknownMethod(); // выбрасывает Nette\MemberAccessException
```
Начиная с PHP 7.0, язык больше не вызывает неперехватываемые фатальные ошибки, а с PHP 8.2 доступ к необъявленным членам считается ошибкой.
Подсказка "Did you mean?"
-------------------------
`Nette\Object` пришел с очень приятной функцией: интеллектуальной подсказкой при опечатках. Когда разработчик делал ошибку в названии метода или переменной, он не только сообщал об ошибке, но и предлагал руку помощи в виде предложения правильного названия. Это знаковое сообщение, известное как "did you mean?", сэкономило программистам часы поиска опечаток:
```php
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// выбрасывает Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
```
Хотя сегодняшнее PHP не имеет аналога „did you mean?“, это дополнение умеет добавлять в ошибки [Tracy |tracy:]. И даже такие ошибки [самостоятельно исправлять |tracy:open-files-in-ide#Примеры].
Свойства с контролируемым доступом
----------------------------------
Значительной инновацией, которую SmartObject привнес в PHP, были свойства с контролируемым доступом. Эта концепция, распространенная в языках вроде C# или Python, позволила разработчикам элегантно контролировать доступ к данным объекта и обеспечивать их согласованность. Свойства являются мощным инструментом объектно-ориентированного программирования. Они работают как переменные, но на самом деле представлены методами (геттерами и сеттерами). Это позволяет валидировать входы или генерировать значения только в момент чтения.
Для использования свойств необходимо:
- Добавить классу аннотацию в виде `@property <type> $xyz`
- Создать геттер с именем `getXyz()` или `isXyz()`, сеттер с именем `setXyz()`
- Убедиться, что геттер и сеттер являются *public* или *protected*. Они необязательны — могут существовать как *read-only* или *write-only* свойства
Покажем практический пример на классе Circle, где мы используем свойства для обеспечения того, чтобы радиус всегда был неотрицательным числом. Заменим исходное `public $radius` на свойство:
```php
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // не public!
// геттер для свойства $radius
protected function getRadius(): float
{
return $this->radius;
}
// сеттер для свойства $radius
protected function setRadius(float $radius): void
{
// санируем значение перед сохранением
$this->radius = max(0.0, $radius);
}
// геттер для свойства $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // на самом деле вызывает setRadius(10)
echo $circle->radius; // вызывает getRadius()
echo $circle->visible; // вызывает isVisible()
```
Начиная с PHP 8.4, можно достичь той же функциональности с помощью property hooks, которые предлагают гораздо более элегантный и краткий синтаксис:
```php
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
```
Методы расширения
-----------------
`Nette\Object` привнес в PHP еще одну интересную концепцию, вдохновленную современными языками программирования — методы расширения. Эта функция, заимствованная из C#, позволила разработчикам элегантно расширять существующие классы новыми методами без необходимости их изменять или наследоваться от них. Например, вы могли добавить в форму метод `addDateTime()`, который добавит собственный DateTimePicker:
```php
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
```
Методы расширения оказались непрактичными, так как их имена не подсказывались редакторами, наоборот, сообщали, что метод не существует. Поэтому их поддержка была прекращена. Сегодня чаще используется композиция или наследование для расширения функциональности классов.
Получение имени класса
----------------------
Для получения имени класса SmartObject предлагал простой метод:
```php
$class = $obj->getClass(); // с помощью Nette\Object
$class = $obj::class; // с PHP 8.0
```
Доступ к рефлексии и аннотациям
-------------------------------
`Nette\Object` предлагал доступ к рефлексии и аннотациям с помощью методов `getReflection()` и `getAnnotation()`. Этот подход значительно упростил работу с метаинформацией классов:
```php
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // возвращает 'John Doe'
```
Начиная с PHP 8.0, можно получать доступ к метаинформации в виде атрибутов, которые предлагают еще большие возможности и лучший контроль типов:
```php
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
```
Метод-геттеры
-------------
`Nette\Object` предлагал элегантный способ передавать методы так, как если бы это были переменные:
```php
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
```
Начиная с PHP 8.1, можно использовать так называемый "first-class callable syntax":https://www.php.net/manual/en/functions.first_class_callable_syntax, который развивает эту концепцию еще дальше:
```php
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
```
События
-------
SmartObject предлагает упрощенный синтаксис для работы с [событиями |nette:glossary#События Events]. События позволяют объектам информировать другие части приложения об изменениях своего состояния:
```php
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
```
Код `$this->onChange($this, $radius)` эквивалентен следующему циклу:
```php
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
```
Из соображений понятности рекомендуется избегать магического метода `$this->onChange()`. Практической заменой является, например, функция [Nette\Utils\Arrays::invoke |arrays#invoke]:
```php
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
```