SmartObject
SmartObject много лет улучшал поведение объектов в PHP. Начиная с PHP 8.4 все его возможности стали нативной частью самого PHP, чем и была завершена его историческая миссия первопроходца современного объектно-ориентированного подхода в PHP.
Установка:
composer require nette/utils
SmartObject появился в 2007 году как революционное решение недостатков тогдашней объектной модели PHP. В эпоху, когда PHP страдал от множества проблем с объектно-ориентированным проектированием, он принёс существенные улучшения и упростил работу разработчиков. Он стал легендарной составляющей Nette Framework. SmartObject предлагал функциональность, которую PHP получил лишь много лет спустя: от контроля доступа к свойствам объектов до продуманного синтаксического сахара. С выходом PHP 8.4 он выполнил свою историческую миссию, потому что большая часть его возможностей стала нативной частью языка. Он опередил развитие PHP на впечатляющие 17 лет.
Технически SmartObject прошёл интересную эволюцию. Изначально он был
реализован как класс Nette\Object, от которого другие классы
наследовали нужную функциональность. Значительная перемена пришла с
PHP 5.4, где появилась поддержка трейтов. Это позволило превратить его в
трейт Nette\SmartObject и дало больше гибкости: разработчики могли
использовать эту функциональность даже в классах, которые уже
наследуют от другого класса. Исходный класс Nette\Object перестал
существовать с выходом PHP 7.2 (который запретил называть классы словом
Object), а трейт 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)
Фатальная ошибка завершала приложение без всякой возможности
отреагировать. Молчаливая запись в несуществующие члены без
предупреждения могла привести к серьёзным ошибкам, которые трудно
обнаружить. Nette\Object перехватывал все эти случаи и выбрасывал
MemberAccessException, позволяя программистам реагировать на такие
ошибки и обрабатывать их:
echo $obj->undeclared; // выбрасывает Nette\MemberAccessException
$obj->undeclared = 1; // выбрасывает Nette\MemberAccessException
$obj->unknownMethod(); // выбрасывает Nette\MemberAccessException
Начиная с PHP 7.0 язык больше не вызывает неперехватываемых фатальных ошибок, а начиная с 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. И она даже умеет автоматически исправлять такие ошибки.
Свойства с контролируемым доступом
Значительным новшеством, которое SmartObject принёс в PHP, стали свойства с контролируемым доступом. Эта концепция, привычная в языках вроде C# или Python, позволяла разработчикам изящно контролировать доступ к данным объекта и обеспечивать их согласованность. Свойства – мощный инструмент объектно-ориентированного программирования. Они работают как переменные, но на самом деле представлены методами (геттерами и сеттерами). Это позволяет проверять входные данные или порождать значение в момент чтения.
Чтобы использовать свойства, нужно было:
- Добавить к классу аннотацию
@property <type> $xyz - Создать геттер с именем
getXyz()илиisXyz()и сеттер с именемsetXyz() - Обеспечить, чтобы геттер и сеттер были public или protected. Они были необязательны, поэтому могли существовать свойства только для чтения или только для записи
Посмотрим на практический пример с классом Circle, где мы с
помощью свойств обеспечим, чтобы радиус всегда был неотрицательным.
Заменим public $radius свойством:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // не 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()
Начиная с PHP 8.4 того же можно добиться с помощью хуков свойств, которые предлагают гораздо более изящный и краткий синтаксис:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Методы расширения
Nette\Object принёс в PHP ещё одну интересную концепцию,
вдохновлённую современными языками программирования, – методы
расширения. Эта возможность, заимствованная из C#, позволяла
разработчикам изящно расширять существующие классы новыми методами,
не изменяя их и не наследуя от них. Например, вы могли добавить форме
метод addDateTime(), который добавляет собственный DateTimePicker:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Методы расширения оказались непрактичными, потому что редакторы кода их не подсказывали, а вместо этого сообщали, что метод не существует. Поэтому их поддержка была прекращена. Сегодня для расширения функциональности классов чаще используют композицию или наследование.
Получение имени класса
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];
Геттеры методов
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, который развивает эту идею ещё дальше:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
События
SmartObject предлагает упрощённый синтаксис для работы с событиями. События позволяют объектам сообщать другим частям приложения об изменениях своего состояния:
class Circle
{
use Nette\SmartObject;
public array $onChange = [];
private float $radius = 0.0;
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);