Nette Documentation Preview

syntax
SmartObject
***********

.[perex]
Раніше SmartObject виправляв поведінку об'єктів багатьма способами, але сьогоднішній PHP вже містить більшість з цих покращень вбудовано. Проте, він все ще додає підтримку *property*.


Встановлення:

```shell
composer require nette/utils
```


Властивості, геттери та сеттери .[#toc-properties-getters-and-setters]
======================================================================

У сучасних об'єктно-орієнтованих мовах (наприклад, C#, Python, Ruby, JavaScript) термін *властивість* відноситься до [спеціальних членів класів |https://en.wikipedia.org/wiki/Property_(programming)], що виглядають як змінні, але насправді представлені методами. Коли значення такої "змінної" присвоюється або зчитується, викликається відповідний метод (званий getter або setter). Це дуже зручна річ, вона дає нам повний контроль над доступом до змінних. Ми можемо перевіряти дані, що вводяться, або генерувати результати тільки тоді, коли властивість прочитано.

Властивості PHP не підтримуються, але trait `Nette\SmartObject` може їх імітувати. Як це використовувати?

- Додайте анотацію до класу у вигляді `@property <type> $xyz`
- Створіть геттер з іменем `getXyz()` або `isXyz()`, сеттер з іменем `setXyz()`
- Геттер і сеттер мають бути *публічними* або *захищеними* і необов'язковими, тому може бути властивість *тільки для читання* або *тільки для запису*.

Ми будемо використовувати властивість для класу Circle, щоб гарантувати, що в змінну `$radius` будуть поміщатися тільки невід'ємні числа. Замініть `public $radius` на property:

```php
/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // not 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()
```

Властивості - це насамперед "синтаксичний цукор"((syntactic sugar)), який покликаний зробити життя програміста солодшим за рахунок спрощення коду. Якщо вони вам не потрібні, ви не зобов'язані їх використовувати.


Погляд в історію .[#toc-a-glimpse-into-history]
===============================================

SmartObject використовувався для покращення поведінки об'єктів багатьма способами, але сьогоднішній PHP вже включає в себе більшість з цих покращень нативно. Наступний текст - це ностальгічний погляд в історію, що нагадує нам про те, як все розвивалося.

Від самого початку об'єктна модель PHP страждала від безлічі серйозних недоліків і недоробок. Це призвело до створення класу `Nette\Object` (у 2007 році), який мав на меті виправити ці проблеми та підвищити комфорт використання PHP. Все, що було потрібно - це щоб інші класи успадковували його, і вони отримували переваги, які він пропонував. Коли в PHP 5.4 було введено підтримку трейтів, клас `Nette\Object` було замінено на трейт `Nette\SmartObject`. Це усунуло необхідність успадковувати від спільного предка. Крім того, ознаку можна було використовувати в класах, які вже були успадковані від іншого класу. Остаточний кінець `Nette\Object` настав з виходом PHP 7.2, який заборонив класам називатися `Object`.

З розвитком PHP його об'єктна модель і можливості мови покращувалися. Різні функції класу `SmartObject` стали надлишковими. З виходом PHP 8.2 залишилася лише одна функція, яка не підтримується безпосередньо в PHP: можливість використання так званих [властивостей |#Properties, getters, and setters].

Які ж можливості пропонували `Nette\Object` і, відповідно, `Nette\SmartObject`? Ось короткий огляд. (У прикладах використовується клас `Nette\Object`, але більшість можливостей також застосовні до властивості `Nette\SmartObject` ).


Непослідовні помилки .[#toc-inconsistent-errors]
------------------------------------------------
PHP мав непослідовну поведінку при зверненні до неоголошених членів. Стан на момент публікації `Nette\Object` був таким:

```php
echo $obj->undeclared; // E_NOTICE, пізніше E_WARNING
$obj->undeclared = 1; // проходить мовчки без повідомлення
$obj->unknownMethod(); // Фатальна помилка (не перехоплюється try/catch)
```

Фатальна помилка завершувала додаток без будь-якої можливості відреагувати. Тихий запис у неіснуючі члени без попередження міг призвести до серйозних помилок, які було важко виявити. `Nette\Object` Всі ці випадки були спіймані, і було викинуто виняток `MemberAccessException`.

```php
echo $obj->undeclared; // згенерувати виключення Nette\MemberAccessException
$obj->undeclared = 1; // згенерувати виключення Nette\MemberAccessException
$obj->unknownMethod(); // згенерувати виключення Nette\MemberAccessException
```
Починаючи з PHP 7.0, PHP більше не спричиняє неперехоплюваних фатальних помилок, а доступ до неоголошених членів став помилкою, починаючи з PHP 8.2.


Ви мали на увазі? .[#toc-did-you-mean]
--------------------------------------
Якщо виникала помилка `Nette\MemberAccessException`, можливо, через друкарську помилку під час звернення до об'єктної змінної або виклику методу, `Nette\Object` намагався дати підказку в повідомленні про помилку, як виправити помилку, у вигляді знакового доповнення "Ви мали на увазі?".

```php
class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// throw Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
```

Хоча сучасний PHP не має функції "ви мали на увазі?", цю фразу можна додати до помилок за допомогою [Трейсі |tracy:]. Він навіть може [автоматично виправляти такі помилки |tracy:open-files-in-ide#toc-demos].


Методи розширення .[#toc-extension-methods]
-------------------------------------------
Натхненний методами розширення з C#. Вони дають можливість додавати нові методи до наявних класів. Наприклад, ви можете додати метод `addDateTime()` до форми, щоб додати свій власний DateTimePicker.

```php
Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');
```

Методи розширення виявилися непрактичними, оскільки їхні імена не автозаповнювалися редакторами, натомість вони повідомляли, що метод не існує. Тому їхню підтримку було припинено.


Отримання імені класу .[#toc-getting-the-class-name]
----------------------------------------------------

```php
$class = $obj->getClass(); // використання Nette\Object
$class = $obj::class; // починаючи з PHP 8.0
```


Доступ до роздумів та анотацій .[#toc-access-to-reflection-and-annotations]
---------------------------------------------------------------------------

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


Геттери методів .[#toc-method-getters]
--------------------------------------

`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, ви можете використовувати так званий "першокласний синтаксис методів, що викликаються":https://www.php.net/manual/en/functions.first_class_callable_syntax:

```php
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
```


Події .[#toc-events]
--------------------

`Nette\Object` пропонує синтаксичний цукор для запуску [події |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);
```

SmartObject

Раніше SmartObject виправляв поведінку об'єктів багатьма способами, але сьогоднішній PHP вже містить більшість з цих покращень вбудовано. Проте, він все ще додає підтримку property.

Встановлення:

composer require nette/utils

Властивості, геттери та сеттери

У сучасних об'єктно-орієнтованих мовах (наприклад, C#, Python, Ruby, JavaScript) термін властивість відноситься до спеціальних членів класів, що виглядають як змінні, але насправді представлені методами. Коли значення такої „змінної“ присвоюється або зчитується, викликається відповідний метод (званий getter або setter). Це дуже зручна річ, вона дає нам повний контроль над доступом до змінних. Ми можемо перевіряти дані, що вводяться, або генерувати результати тільки тоді, коли властивість прочитано.

Властивості PHP не підтримуються, але trait Nette\SmartObject може їх імітувати. Як це використовувати?

  • Додайте анотацію до класу у вигляді @property <type> $xyz
  • Створіть геттер з іменем getXyz() або isXyz(), сеттер з іменем setXyz()
  • Геттер і сеттер мають бути публічними або захищеними і необов'язковими, тому може бути властивість тільки для читання або тільки для запису.

Ми будемо використовувати властивість для класу Circle, щоб гарантувати, що в змінну $radius будуть поміщатися тільки невід'ємні числа. Замініть public $radius на property:

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // not 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()

Властивості – це насамперед синтаксичний цукор, який покликаний зробити життя програміста солодшим за рахунок спрощення коду. Якщо вони вам не потрібні, ви не зобов'язані їх використовувати.

Погляд в історію

SmartObject використовувався для покращення поведінки об'єктів багатьма способами, але сьогоднішній PHP вже включає в себе більшість з цих покращень нативно. Наступний текст – це ностальгічний погляд в історію, що нагадує нам про те, як все розвивалося.

Від самого початку об'єктна модель PHP страждала від безлічі серйозних недоліків і недоробок. Це призвело до створення класу Nette\Object (у 2007 році), який мав на меті виправити ці проблеми та підвищити комфорт використання PHP. Все, що було потрібно – це щоб інші класи успадковували його, і вони отримували переваги, які він пропонував. Коли в PHP 5.4 було введено підтримку трейтів, клас Nette\Object було замінено на трейт Nette\SmartObject. Це усунуло необхідність успадковувати від спільного предка. Крім того, ознаку можна було використовувати в класах, які вже були успадковані від іншого класу. Остаточний кінець Nette\Object настав з виходом PHP 7.2, який заборонив класам називатися Object.

З розвитком PHP його об'єктна модель і можливості мови покращувалися. Різні функції класу SmartObject стали надлишковими. З виходом PHP 8.2 залишилася лише одна функція, яка не підтримується безпосередньо в PHP: можливість використання так званих властивостей.

Які ж можливості пропонували Nette\Object і, відповідно, Nette\SmartObject? Ось короткий огляд. (У прикладах використовується клас Nette\Object, але більшість можливостей також застосовні до властивості Nette\SmartObject ).

Непослідовні помилки

PHP мав непослідовну поведінку при зверненні до неоголошених членів. Стан на момент публікації Nette\Object був таким:

echo $obj->undeclared; // E_NOTICE, пізніше E_WARNING
$obj->undeclared = 1; // проходить мовчки без повідомлення
$obj->unknownMethod(); // Фатальна помилка (не перехоплюється try/catch)

Фатальна помилка завершувала додаток без будь-якої можливості відреагувати. Тихий запис у неіснуючі члени без попередження міг призвести до серйозних помилок, які було важко виявити. Nette\Object Всі ці випадки були спіймані, і було викинуто виняток MemberAccessException.

echo $obj->undeclared; // згенерувати виключення Nette\MemberAccessException
$obj->undeclared = 1; // згенерувати виключення Nette\MemberAccessException
$obj->unknownMethod(); // згенерувати виключення Nette\MemberAccessException

Починаючи з PHP 7.0, PHP більше не спричиняє неперехоплюваних фатальних помилок, а доступ до неоголошених членів став помилкою, починаючи з PHP 8.2.

Ви мали на увазі?

Якщо виникала помилка Nette\MemberAccessException, можливо, через друкарську помилку під час звернення до об'єктної змінної або виклику методу, Nette\Object намагався дати підказку в повідомленні про помилку, як виправити помилку, у вигляді знакового доповнення „Ви мали на увазі?“.

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// throw Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Хоча сучасний PHP не має функції „ви мали на увазі?“, цю фразу можна додати до помилок за допомогою Трейсі. Він навіть може автоматично виправляти такі помилки.

Методи розширення

Натхненний методами розширення з C#. Вони дають можливість додавати нові методи до наявних класів. Наприклад, ви можете додати метод addDateTime() до форми, щоб додати свій власний DateTimePicker.

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

Методи розширення виявилися непрактичними, оскільки їхні імена не автозаповнювалися редакторами, натомість вони повідомляли, що метод не існує. Тому їхню підтримку було припинено.

Отримання імені класу

$class = $obj->getClass(); // використання Nette\Object
$class = $obj::class; // починаючи з PHP 8.0

Доступ до роздумів та анотацій

Nette\Object запропоновано доступ до роздумів і анотацій за допомогою методів getReflection() і getAnnotation():

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // повертає 'John Doe'

Починаючи з PHP 8.0, з'явилася можливість доступу до мета-інформації у вигляді атрибутів:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Геттери методів

Nette\Object пропонують елегантний спосіб роботи з методами, наче вони є змінними:

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, ви можете використовувати так званий першокласний синтаксис методів, що викликаються:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Події

Nette\Object пропонує синтаксичний цукор для запуску події:

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) еквівалентний наступному:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Для ясності ми рекомендуємо уникати магічного методу $this->onChange(). Хорошою заміною буде Nette\Utils\Arrays::invoke:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);