SmartObject
SmartObject на протяжении многих лет улучшал поведение объектов в PHP. Начиная с версии PHP 8.4, все его функции стали встроенной частью самого PHP, тем самым завершив свою историческую миссию первопроходца современного объектно-ориентированного подхода в PHP.
Установка:
composer require nette/utils
SmartObject появился в 2007 году как революционное решение недостатков объектной модели PHP того времени. В период, когда PHP страдал от множества проблем с объектно-ориентированным дизайном, он принес значительные улучшения и упростил работу разработчиков. Он стал легендарной частью фреймворка Nette. SmartObject предлагал функциональность, которую PHP получил лишь много лет спустя – от валидации доступа к свойствам объектов до сложной обработки ошибок. С выходом PHP 8.4 он завершил свою историческую миссию, так как все его функции стали встроенной частью языка. SmartObject опередил развитие PHP на впечатляющие 17 лет.
Технически SmartObject прошел интересный путь развития. Изначально он был
реализован как класс Nette\Object
, от которого другие классы
наследовали необходимую функциональность. Значительные изменения
пришли с PHP 5.4, который ввел поддержку trait-ов. Это позволило
трансформировать его в trait Nette\SmartObject
, что принесло большую
гибкость – разработчики могли использовать функциональность даже в
классах, которые уже наследовались от другого класса. В то время как
оригинальный класс Nette\Object
прекратил существование с выходом PHP
7.2 (который запретил именование классов словом ‘Object’), trait
Nette\SmartObject
продолжает жить.
Давайте рассмотрим возможности, которые предлагали Nette\Object
и
позже Nette\SmartObject
. Каждая из этих функций в свое время
представляла значительный шаг вперед в области
объектно-ориентированного программирования в PHP.
Последовательная обработка ошибок
Одной из самых острых проблем раннего PHP было непоследовательное
поведение при работе с объектами. Nette\Object
внес порядок и
предсказуемость в этот хаос. Посмотрим, как вело себя PHP изначально:
echo $obj->undeclared; // E_NOTICE, позже E_WARNING
$obj->undeclared = 1; // проходит молча без предупреждения
$obj->unknownMethod(); // Fatal error (необрабатываемый try/catch)
Fatal error завершал приложение без возможности какой-либо реакции. Тихая
запись в несуществующие члены без предупреждения могла привести к
серьезным ошибкам, которые было трудно обнаружить. Nette\Object
отлавливал все эти случаи и выбрасывал исключение MemberAccessException
,
что позволяло программистам реагировать на ошибки и
обрабатывать их:
echo $obj->undeclared; // выбрасывает Nette\MemberAccessException
$obj->undeclared = 1; // выбрасывает Nette\MemberAccessException
$obj->unknownMethod(); // выбрасывает Nette\MemberAccessException
Начиная с PHP 7.0, язык больше не вызывает необрабатываемые fatal error, а с PHP 8.2 доступ к необъявленным членам считается ошибкой.
Подсказка “Did you mean?”
Nette\Object
пришел с очень удобной функцией: интеллектуальными
подсказками при опечатках. Когда разработчик делал ошибку в названии
метода или переменной, он не только сообщал об ошибке, но и предлагал
помощь в виде предложения правильного названия. Это знаковое
сообщение, известное как “did you mean?”, сэкономило программистам часы
поиска опечаток:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// выбрасывает Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Хотя современный PHP не имеет встроенных подсказок “did you mean?”, эту функциональность теперь предоставляет Tracy. Он даже может автоматически исправлять такие ошибки.
Properties с контролируемым доступом
Значительным нововведением, которое SmartObject принес в PHP, были properties с контролируемым доступом. Эта концепция, распространенная в таких языках как C# или Python, позволила разработчикам элегантно контролировать доступ к данным объекта и обеспечивать их целостность. Properties являются мощным инструментом объектно-ориентированного программирования. Они работают как переменные, но фактически представлены методами (getters и setters). Это позволяет валидировать входные данные или генерировать значения в момент чтения.
Для использования properties необходимо:
- Добавить классу аннотацию вида
@property <type> $xyz
- Создать getter с именем
getXyz()
илиisXyz()
, setter с именемsetXyz()
- Обеспечить, чтобы getter и setter были public или protected. Они опциональны – могут существовать как read-only или write-only properties
Рассмотрим практический пример на классе Circle, где мы используем
properties для обеспечения того, чтобы радиус всегда был неотрицательным
числом. Заменим public $radius
на property:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // не public!
// getter для property $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter для property $radius
protected function setRadius(float $radius): void
{
// санитизируем значение перед сохранением
$this->radius = max(0.0, $radius);
}
// getter для 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()
Начиная с PHP 8.4, той же функциональности можно достичь с помощью property hooks, которые предлагают более элегантный и краткий синтаксис:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Extension methods
Nette\Object
принес в PHP еще одну интересную концепцию,
вдохновленную современными языками программирования – extension methods. Эта
функция, заимствованная из C#, позволяла разработчикам элегантно
расширять существующие классы новыми методами без необходимости их
модификации или наследования. Например, вы могли добавить в форму
метод addDateTime()
, который добавляет пользовательский DateTimePicker:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Extension methods оказались непрактичными, потому что редакторы не подсказывали их имена, а наоборот, сообщали, что метод не существует. Поэтому их поддержка была прекращена. Сегодня для расширения функциональности классов чаще используется композиция или наследование.
Получение имени класса
Для получения имени класса SmartObject предлагал простой метод:
$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];
Method getters
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, можно использовать так называемый first-class callable syntax, который развивает эту концепцию еще дальше:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
События
SmartObject предлагает упрощенный синтаксис для работы с событиями. События позволяют объектам информировать другие части приложения об изменениях своего состояния:
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:
`
php Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);