SmartObject
SmartObject години наред подобряваше поведението на обектите в PHP. От версия PHP 8.4 всички негови функции вече са част от самия PHP, с което завърши своята историческа мисия да бъде пионер на модерния обектен подход в PHP.
Инсталация:
composer require nette/utils
SmartObject възниква през 2007 г. като революционно решение на недостатъците на тогавашния обектен модел на PHP. Във време, когато PHP страдаше от редица проблеми с обектния дизайн, той донесе значително подобрение и опростяване на работата за разработчиците. Стана легендарна част от Nette Framework. Предлагаше функционалност, която PHP придоби едва много години по-късно – от контрол на достъпа до свойствата на обектите до сложни синтактични захари. С появата на PHP 8.4 завърши своята историческа мисия, тъй като всички негови функции станаха нативна част от езика. Изпревари развитието на 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)
Фаталната грешка прекратяваше приложението без възможност за
реакция. Тихото записване в несъществуващи членове без предупреждение
можеше да доведе до сериозни грешки, които бяха трудни за откриване.
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, позволи на разработчиците елегантно да контролират достъпа до данните на обекта и да гарантират тяхната консистенция. Свойствата са мощен инструмент на обектно-ориентираното програмиране. Те функционират като променливи, но всъщност се представят от методи (getters и setters). Това позволява валидиране на входовете или генериране на стойности в момента на четене.
За да използвате свойства, трябва:
- Да добавите анотация към класа във формата
@property <type> $xyz
- Да създадете getter с име
getXyz()
илиisXyz()
, setter с имеsetXyz()
- Да се уверите, че getter-ът и setter-ът са 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 е възможно да се използва т.нар. синтаксис за извикване от първи клас, който издига тази концепция още по-далеч:
$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);