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, което донесе поддръжка на трейтове (traits). Това позволи трансформацията във формата на трейт 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, позволи на разработчиците елегантно да контролират достъпа до данните на обекта и да гарантират тяхната консистенция. Свойствата са мощен инструмент на обектно-ориентираното програмиране. Те функционират като променливи, но всъщност се представят от методи (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;
	}
}

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

Разширителните методи се оказаха непрактични, тъй като техните имена не се подсказваха от редакторите, а напротив, съобщаваха, че методът не съществува. Затова поддръжката им беше прекратена. Днес е по-често срещано използването на композиция или наследяване за разширяване на функционалността на класовете.

Установяване на името на класа

За установяване на името на класа 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:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);