SmartObject
SmartObject годами улучшал поведение объектов в PHP. С версии PHP 8.4 все его функции уже стали частью самого PHP, тем самым завершив свою историческую миссию быть пионером современного объектного подхода в PHP.
Установка:
composer require nette/utils
SmartObject возник в 2007 году как революционное решение недостатков тогдашней объектной модели PHP. В то время, когда PHP страдал от множества проблем с объектным дизайном, он принес значительное улучшение и упрощение работы для разработчиков. Он стал легендарной частью фреймворка Nette. Он предлагал функциональность, которую 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)
Fatal error завершал приложение без возможности как-либо отреагировать.
Тихая запись в несуществующие члены без предупреждения могла привести
к серьезным ошибкам, которые было трудно обнаружить. 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. Они необязательны – могут существовать как read-only или write-only свойства
Покажем практический пример на классе Circle, где мы используем
свойства для обеспечения того, чтобы радиус всегда был
неотрицательным числом. Заменим исходное public $radius
на
свойство:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // не public!
// getter для свойства $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter для свойства $radius
protected function setRadius(float $radius): void
{
// санируем значение перед сохранением
$this->radius = max(0.0, $radius);
}
// getter для свойства $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;
}
}
Методы расширения
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 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:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);