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);