SmartObject: расширение объекта в PHP

SmartObject расширяет объектные возможности PHP с помощью нескольких синтаксических возможностей. Вы любите конфеты? Читайте дальше, чтобы узнать

  • почему полезно использовать Nette\SmartObject
  • что такое имущество
  • как инициировать события

В этой главе мы сосредоточимся в основном на свойстве Nette\SmartObject, которое расширяет объектные возможности PHP. Этот признак используется почти всеми классами Nette Framework. В то же время он достаточно прозрачен, чтобы вы могли использовать его для своих собственных занятий. Давайте попробуем разобраться, почему вы должны это делать.

Признак Nette\SmartObject является упрощенным преемником ныне устаревшего класса Nette\Object из Nette 2.x.

Установка:

composer require nette/utils

Строгие классы

PHP – это язык для устранения ошибок, поскольку он дает разработчику большую свободу. Как положить этому конец и начать писать программы без труднообнаруживаемых ошибок? Просто установите более жесткую планку и следуйте определенным правилам.

Можете ли вы найти недостаток в этом примере?

class Circle
{
	public $radius = 0.0;

	public function getArea(): float
	{
		return $this->radius * $this->radius * M_PI;
	}
}

$circle = new Circle;
$circle->raduis = 10;
echo $circle->getArea(); // 10² * π ≈ 314

На первый взгляд кажется, что код должен выводить примерно 314, однако он выводит ноль. Как это возможно? По ошибке вместо $circle->radius в код попал raduis- незначительная опечатка. Коварство этой ошибки заключается в том, что PHP не предупреждает о ней и не помогает отследить ошибки Warning или Notice. С точки зрения языка, это не ошибка, хотя для ее обнаружения требуется много усилий.

Мы бы сразу обнаружили ошибку, если бы класс Circle использовал Nette\SmartObject:

class Circle
{
	use Nette\SmartObject;
}

Если до сих пор код выполнялся тихо (но ошибочно), то теперь ситуация изменилась:

Трейта Nette\SmartObject сделала Circle более строгим и отреагировала на использование необъявленной переменной, выбросив исключение, которое показала Трейси. Строка с фатальной опечаткой обнаруживается и помечается, а текстовое сообщение описывает ситуацию словами: Невозможно записать в необъявленное свойство Circle::$raduis, вы имели в виду $radius?. Программист звонит “ага” и может оперативно отреагировать. Ошибка, которую он мог долго не замечать и обнаружить только с большим усилием, была явно подана на красном блюдечке.

Первая возможность Nette\SmartObject, которую мы продемонстрировали, – это выбрасывание исключений при обращении к необъявленному члену класса.

$circle = new Circle;
echo $circle->undeclared; // выбрасывает Nette\MemberAccessException
$circle->undeclared = 1; // выбрасывает Nette\MemberAccessException
$circle->unknownMethod(); // выбрасывает Nette\MemberAccessException

На этом его возможности далеко не исчерпываются.

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

Вы хотели сказать?

Если вы допустили опечатку при обращении к переменной объекта или при вызове метода, появляется исключение, которое пытается указать вам, где ошибка. Он содержит культовое дополнение “Вы хотели сказать?”.

class Foo
{
	use Nette\SmartObject;

	public static function from($var)
	{
		// ...
	}
}

$foo = Foo::form($var);
// выбрасывает Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Некоторые опечатки могут быть буквально незаметны. Мозг привык видеть слова form и from, и вполне может случиться так, что вы смотрите на название метода и просто не замечаете ошибки, без дислексии. Но если вы прочитаете Foo::form(), did you mean from()? в тексте исключения, то сразу поймете опечатку.

SmartObject включает в подсказку не только все методы и свойства класса, но и магические/виртуальные члены, определенные аннотациями @method и @property. И самое приятное: Tracy может автоматически исправить эти ошибки.

Свойства, геттеры и сеттеры

(Для более опытных программистов)

В современных объектно-ориентированных языках (например, C#, Python, Ruby, JavaScript) термин свойство относится к специальным членам классов, которые выглядят как переменные, но на самом деле представлены методами. Когда значение этой “переменной” присваивается или считывается, вызывается соответствующий метод (называемый getter или setter). Это очень удобная вещь, она дает нам полный контроль над доступом к переменным. Мы можем проверять вводимые данные или генерировать результаты только тогда, когда свойство прочитано.

Любой класс, использующий признак Nette\SmartObject, может имитировать это свойство. Как это сделать?

  • Добавить аннотацию формы @property <type> $xyz
  • Создайте геттер с именем getXyz() или isXyz(), сеттер с именем setXyz()
  • getter и setter должны быть public или protected и оба необязательны, поэтому может быть свойство только для чтения или только для записи.

Мы будем использовать свойство для класса Circle, чтобы убедиться, что в переменную $radius заносятся только неотрицательные числа. Замените public $radius на собственность:

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

	private float $radius = 0.0; // больше не является публичным!

	// getter pro property $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter pro property $radius
	protected function setRadius(float $radius): void
	{
		// hodnotu před uložením sanitizujeme
		$this->radius = max(0.0, $radius);
	}

	// getter pro property $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()

Свойства – это в первую очередь “синтаксический сахар”, который призван сделать жизнь программиста слаще, упростив код. Если они вам не нужны, вы не обязаны их использовать.

События

(Для более опытных программистов)

Создайте очередь функций, которые будут вызываться при изменении радиуса окружности. Назовем изменение радиуса событием change, а отдельные функции – обработчиками событий:

class Circle
{
	use Nette\SmartObject;

	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		// вызов обратных вызовов в $onChange с параметрами $this, $radius
		$this->onChange($this, $radius);
		// lépe: Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);

		$this->radius = max(0.0, $radius);
	}
}

$circle = new Circle;

// добавить обработчик события
$circle->onChange[] = function (Circle $circle, float $newValue): void {
	echo "Произошло изменение";
};

$circle->setRadius(10);

Синтаксический сахар можно увидеть в коде метода setRadius – вместо итерации по массиву $onChange и вызова отдельных обратных вызовов, достаточно написать лаконичный onChange(...) и указать параметры для передачи каждому обратному вызову. Поэтому SmartObject создает фиктивный метод onChange() с тем же именем, что и поле $onChange. Условие вида on + слово является условием.

Ради читабельности кода мы рекомендуем избегать этого синтаксического сахара и использовать функцию Nette\Utils\Arrays::invoke для вызова обратных вызовов.

Статические классы

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

class Strings
{
	use Nette\StaticClass;
}

Когда вы пытаетесь создать экземпляр, возникает исключение Error, указывающее на то, что класс является статическим.