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