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
, указывающее на то, что класс является статическим.