SmartObject і StaticClass

SmartObject додає підтримку властивостей у класи PHP. StaticClass використовується для позначення статичних класів.

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

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

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

Статичні класи

Статичні класи, тобто класи, які не призначені для інстанціювання, можуть бути позначені ознакою Nette\StaticClass:

class Strings
{
	use Nette\StaticClass;
}

Коли ви намагаєтеся створити екземпляр, виникає виняток Error, який вказує на те, що клас є статичним.

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

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

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

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

Які функції пропонували Nette\Object і Nette\Object? Ось огляд. (У прикладах використовується клас 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, можливо, не має форми “Ви мали на увазі?”, але Tracy додає це доповнення до помилок. І він навіть може сам виправляти такі помилки.

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

Натхненний методами розширення з 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);
версію: 4.0