SmartObject i StaticClass

SmartObject dodaje do klas PHP wsparcie dla property. StaticClass służy do oznaczania klas statycznych.

Instalacja:

composer require nette/utils

Właściwości, gettery i settery

W nowoczesnych językach obiektowych (np. C#, Python, Ruby, JavaScript) termin property odnosi się do specjalnych członków klas, które wyglądają jak zmienne, ale w rzeczywistości są reprezentowane przez metody. Kiedy wartość tej “zmiennej” jest przypisywana lub odczytywana, wywoływana jest odpowiednia metoda (zwana getterem lub setterem). Jest to bardzo przydatna rzecz, daje nam pełną kontrolę nad dostępem do zmiennych. Możemy zatwierdzić dane wejściowe lub wygenerować wyniki tylko wtedy, gdy właściwość zostanie odczytana.

Właściwości PHP nie są obsługiwane, ale traita Nette\SmartObject może je imitować. Jak to zrobić?

  • Dodaj adnotację do klasy w postaci @property <type> $xyz
  • Utwórz getter o nazwie getXyz() lub isXyz(), setter o nazwie setXyz()
  • Getter i setter muszą być public lub protected i są opcjonalne, więc może być właściwość read-only lub write-only.

Wykorzystamy własność dla klasy Circle, aby zapewnić, że do zmiennej $radius wstawiane są tylko liczby nieujemne. Zamień public $radius na własność:

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

	private float $radius = 0.0; // není publiczne!

	// getter pro właściwość $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter pro właściwość $radius
	protected function setRadius(float $radius): void
	{
		// hodnotu před uložením sanitizujeme
		$this->radius = max(0.0, $radius);
	}

	// getter pro właściwość $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // ve skutečnosti volá setRadius(10)
echo $circle->radius;  // volá getRadius()
echo $circle->visible; // volá isVisible()

Właściwości to przede wszystkim cukier syntaktyczny, który ma uczynić życie programisty słodszym poprzez uproszczenie kodu. Jeśli ich nie chcesz, nie musisz z nich korzystać.

Klasy statyczne

Klasy statyczne, czyli takie, które nie są przeznaczone do instancjonowania, mogą być oznaczone cechą Nette\StaticClass:

class Strings
{
	use Nette\StaticClass;
}

Podczas próby utworzenia instancji rzucany jest wyjątek Error wskazujący, że klasa jest statyczna.

Spojrzenie na historię

SmartObject kiedyś poprawiał i naprawiał zachowanie klasy na wiele sposobów, ale ewolucja PHP sprawiła, że większość oryginalnych funkcji stała się zbędna. Poniżej przedstawiamy więc spojrzenie na historię tego, jak sprawy się rozwijały.

Model obiektowy PHP od początku cierpiał na szereg poważnych wad i nieefektywności. Z tego powodu powstała klasa Nette\Object (w 2007 roku), która starała się im zaradzić i poprawić doświadczenia związane z używaniem PHP. Wystarczyło, aby inne klasy dziedziczyły po niej i czerpały korzyści, które przynosiła. Kiedy w PHP 5.4 pojawiła się obsługa cech, klasa Nette\Object została zastąpiona przez Nette\SmartObject. Tym samym nie było już konieczności dziedziczenia po wspólnym przodku. Dodatkowo trait można było wykorzystać w klasach, które już dziedziczyły po innej klasie. Ostateczny koniec Nette\Object nastąpił wraz z wydaniem PHP 7.2, który zabronił klasom nadawania nazw Object.

Wraz z rozwojem PHP udoskonalano model obiektowy i możliwości języka. Poszczególne funkcje klasy SmartObject stały się zbędne. Od wydania PHP 8.2 jedyną cechą, która pozostała, a która nie jest jeszcze bezpośrednio wspierana w PHP, jest możliwość korzystania z tzw. właściwości.

Jakie funkcje oferowały kiedyś Nette\Object i Nette\Object? Oto przegląd. (W przykładach użyto klasy Nette\Object, ale większość właściwości dotyczy również cechy Nette\SmartObject).

Niespójne błędy

PHP zachowywał się niespójnie podczas dostępu do niezadeklarowanych członków. Stan w momencie wejścia na stronę Nette\Object był następujący:

echo $obj->undeclared; // E_NOTICE, później E_WARNING
$obj->undeclared = 1; // przechodzi cicho bez zgłaszania
$obj->unknownMethod(); // Błąd fatalny (nie do wychwycenia przez try/catch)

Fatal error zakończył działanie aplikacji bez możliwości reakcji. Ciche pisanie do nieistniejących członków bez ostrzeżenia mogło prowadzić do poważnych błędów trudnych do wykrycia. Nette\Object Wszystkie te przypadki zostały złapane i wyjątek rzucony przez MemberAccessException.

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

Od PHP 7.0, PHP nie powoduje już nieśledzonych błędów fatalnych, a dostęp do niezadeklarowanych członków jest błędem od PHP 8.2.

Miałeś na myśli?

Jeśli został rzucony błąd Nette\MemberAccessException, być może z powodu literówki przy dostępie do zmiennej obiektu lub wywołaniu metody, Nette\Object próbował w komunikacie o błędzie dać podpowiedź, jak naprawić błąd, w postaci ikonicznego dodatku “czy miałeś na myśli?”.

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

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

Dzisiejszy PZP może nie ma żadnej formy “czy miałeś na myśli?”, ale Tracy może dodać ten dodatek do błędów. I może nawet sam naprawić takie błędy.

Metody rozszerzania

Zainspirowany metodami rozszerzającymi z języka C#. Dawały one możliwość dodawania nowych metod do istniejących klas. Na przykład możesz dodać metodę addDateTime() do formularza, aby dodać własny DateTimePicker.

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

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

Metody rozszerzania okazały się niepraktyczne, ponieważ ich nazwy nie sugerowały redaktorów, zamiast tego informowały, że dana metoda nie istnieje. Dlatego też zaprzestano ich wspierania.

Uzyskanie nazwy klasy:

$class = $obj->getClass(); // używając Nette.
$class = $obj::class; // od PHP 8.0

Dostęp do refleksji i adnotacji

Nette\Object zaoferował dostęp do refleksji i adnotacji za pomocą metod getReflection() i getAnnotation():

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // vrátí 'John Doe

Od PHP 8.0 możliwy jest dostęp do metainformacji w postaci atrybutów:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Metoda gettery

Nette\Object oferował elegancki sposób przekazywania metod tak, jakby były one zmiennymi:

class Foo extends Nette\Object
{
	public function adder($a, $b)
	{
		return $a + $b;
	}
}

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

Od PHP 8.1 można używać tzw. składni wywoływalnej pierwszej klasy:https://www.php.net/…lable_syntax:

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

Wydarzenia

Nette\Object zaoferował cukier syntaktyczny do wywołania zdarzenia:

class Circle extends Nette\Object
{
	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius
	}
}

Kod $this->onChange($this, $radius) jest równoważny z następującym:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Dla jasności, zalecamy unikanie metody magicznej $this->onChange(). Praktycznym substytutem jest funkcja Nette\Utils\Arrays::invoke:

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