SmartObject

SmartObject поправяше поведението на обектите по много начини, но днешният PHP вече включва повечето от тези подобрения. Въпреки това той все още добавя поддръжка за собственост.

Настройка:

composer require nette/utils

Свойства, getteri и setteri

В съвременните обектно-ориентирани езици (напр. C#, Python, Ruby, JavaScript) терминът собственост се отнася до специални членове на класа, които изглеждат като променливи, но всъщност са представени като методи. Когато се присвоява или чете стойността на такава “променлива”, се извиква съответният метод (наречен getter или setter). Това е много удобно, тъй като ни дава пълен контрол върху достъпа до променливите. Можем да проверяваме входни данни или да генерираме резултати само когато свойството е прочетено.

PHP свойствата не се поддържат, но чертите Nette\SmartObject могат да ги симулират. Как да го използвате?

  • Добавяне на анотация към класа под формата на @property <type> $xyz
  • Създайте getter с име getXyz() или isXyz(), setter с име setXyz()
  • Getter и setter трябва да са публични или защитени и незадължителни, така че може да има свойство само за четене или само за писане.

Ще използваме свойството за класа Circle, за да гарантираме, че в променливата $radius се поставят само неотрицателни числа. Заменете public $radius с имота:

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

	private float $radius = 0.0; // не е публичен

	// 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()

Свойствата са преди всичко “синтактична захар” и са предназначени да направят живота на програмиста по-сладък, като опростят кода. Ако нямате нужда от тях, не е нужно да ги използвате.

Поглед към историята

SmartObject усъвършенстваше поведението на обектите по множество начини, но днес PHP вече включва повечето от тези подобрения на роден език. Следващият текст е носталгичен поглед назад към историята, който ни напомня как са се развивали нещата.

От самото си създаване обектният модел на PHP страдаше от множество сериозни недостатъци и пропуски. Това доведе до създаването на класа Nette\Object (през 2007 г.), който имаше за цел да поправи тези проблеми и да повиши удобството при използването на PHP. Всичко, което беше необходимо, беше други класове да наследят от него и да получат предимствата, които той предлагаше. Когато PHP 5.4 въведе поддръжка на черти, класът Nette\Object беше заменен с чертата Nette\SmartObject. Това премахна необходимостта от наследяване от общ предшественик. Освен това чертата можеше да се използва в класове, които вече наследяваха от друг клас. Окончателният край на Nette\Object настъпи с излизането на PHP 7.2, който забрани на класовете да се наричат Object.

С развитието на PHP обектният модел и възможностите на езика се усъвършенстваха. Различните функции на класа SmartObject станаха излишни. След излизането на PHP 8.2 остана само една функция, която не се поддържа директно в PHP: възможността да се използват така наречените свойства.

Какви функции предлагаха Nette\Object и, като следствие, Nette\SmartObject? Ето един преглед. (В примерите е използван класът Nette\Object, но повечето функции се отнасят и за признака Nette\SmartObject ).

Непоследователни грешки

PHP имаше непоследователно поведение при достъп до недекларирани членове. Към момента на публикуване на Nette\Object статусът е следният:

echo $obj->undeclared; // E_NOTICE, по-късно E_WARNING
$obj->undeclared = 1; // преминава безшумно, без да съобщава
$obj->unknownMethod(); // Фатална грешка (която не може да бъде уловена чрез try/catch)

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

echo $obj->undeclared;   // throw Nette\MemberAccessException
$obj->undeclared = 1;    // throw Nette\MemberAccessException
$obj->unknownMethod();   // throw Nette\MemberAccessException

От PHP 7.0 PHP вече не причинява фатални грешки, които не могат да бъдат прихванати, а достъпът до необявени членове е грешка от PHP 8.2 насам.

Какво имаш предвид?

Ако се появи грешка в Nette\MemberAccessException, вероятно поради печатна грешка при достъп до променлива на обект или извикване на метод, Nette\Object се опитва да подскаже в съобщението за грешка как да се поправи грешката под формата на емблематичното допълнение “Искахте ли да кажете?”.

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// throw Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Въпреки че днешният PHP не разполага с функция “Искахте ли да кажете?”, тази фраза може да бъде добавена към грешките от Tracy. Тя дори може да коригира такива грешки автоматично.

Методи за разширение

Вдъхновено от методите за разширение от C#. Те ви позволяват да добавяте нови методи към съществуващи класове. Например можете да добавите метод addDateTime() към формуляр, за да добавите свой собствен DateTimePicker.

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

Методите за разширение се оказаха непрактични, тъй като имената им не се попълваха автоматично от редакторите, а вместо това те съобщаваха, че методът не съществува. Поради това тяхната подкрепа беше преустановена.

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

$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'

От версия 8.0 на PHP вече е възможен достъп до метаинформация под формата на атрибути:

#[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

От версия 8.1 на PHP можете да използвате така наречения синтаксис на извикване на метод от първи клас:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Събития

Nette\Object предлага синтактична захар за задействане на събитие:

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