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. Това позволи трансформацията в 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)

Fatal error прекратяваше приложението без възможност за реакция. Тихото записване в несъществуващи членове без предупреждение можеше да доведе до сериозни грешки, които бяха трудни за откриване. Nette\Object улавяше всички тези случаи и хвърляше изключение MemberAccessException, което позволяваше на програмистите да реагират и да обработват тези грешки:

echo $obj->undeclared;   // хвърля Nette\MemberAccessException
$obj->undeclared = 1;    // хвърля Nette\MemberAccessException
$obj->unknownMethod();   // хвърля Nette\MemberAccessException

От PHP 7.0 езикът вече не причинява неуловими fatal error, а от 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. Той може дори да поправя автоматично такива грешки.

Properties с контролиран достъп

Значителна иновация, която SmartObject донесе в PHP, бяха properties с контролиран достъп. Този концепт, обичаен в езици като C# или Python, позволи на разработчиците елегантно да контролират достъпа до данните на обекта и да осигуряват тяхната консистентност. Properties са мощен инструмент на обектно-ориентираното програмиране. Те функционират като променливи, но всъщност са представени чрез методи (getters и setters). Това позволява валидация на входа или генериране на стойности в момента на четене.

За да използвате properties, трябва да:

  • Добавите анотация @property <type> $xyz към класа
  • Създадете getter с име getXyz() или isXyz(), setter с име setXyz()
  • Осигурите getter и setter да са public или protected. Те са опционални – следователно могат да съществуват като read-only или write-only properties

Нека разгледаме практически пример с класа Circle, където ще използваме properties, за да гарантираме, че радиусът винаги е неотрицателно число. Ще заменим public $radius с property:

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // не е public!

	// getter за property $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter за property $radius
	protected function setRadius(float $radius): void
	{
		// санитизираме стойността преди записване
		$this->radius = max(0.0, $radius);
	}

	// getter за property $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');

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

Получаване на името на класа

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