SmartObject

SmartObject przez lata usprawniał zachowanie obiektów w PHP. Od wersji PHP 8.4 wszystkie jego funkcje są już częścią samego PHP, tym samym kończąc swoją historyczną misję jako pionier nowoczesnego podejścia obiektowego w PHP.

Instalacja:

composer require nette/utils

SmartObject powstał w 2007 roku jako rewolucyjne rozwiązanie niedoskonałości ówczesnego modelu obiektowego PHP. W czasie, gdy PHP borykało się z wieloma problemami w projektowaniu obiektowym, wprowadził znaczące usprawnienia i uproszczenia w pracy programistów. Stał się legendarną częścią frameworka Nette. Oferował funkcjonalności, które PHP zyskało dopiero wiele lat później – od walidacji dostępu do właściwości obiektów po zaawansowaną obsługę błędów. Wraz z nadejściem PHP 8.4 zakończył swoją historyczną misję, ponieważ wszystkie jego funkcje stały się natywną częścią języka. Wyprzedził rozwój PHP o imponujące 17 lat.

SmartObject przeszedł ciekawą ewolucję techniczną. Początkowo był zaimplementowany jako klasa Nette\Object, po której inne klasy dziedziczyły potrzebną funkcjonalność. Znacząca zmiana nastąpiła wraz z PHP 5.4, które wprowadziło obsługę trait. Umożliwiło to transformację w trait Nette\SmartObject, co przyniosło większą elastyczność – programiści mogli wykorzystywać funkcjonalność nawet w klasach, które już dziedziczyły po innej klasie. Podczas gdy oryginalna klasa Nette\Object przestała istnieć wraz z PHP 7.2 (które zabroniło nazywania klas słowem ‘Object’), trait Nette\SmartObject żyje nadal.

Przyjrzyjmy się funkcjom, które kiedyś oferował Nette\Object, a później Nette\SmartObject. Każda z tych funkcji stanowiła w swoim czasie znaczący krok naprzód w programowaniu obiektowym w PHP.

Spójne stany błędów

Jednym z najbardziej palących problemów wczesnego PHP było niespójne zachowanie podczas pracy z obiektami. Nette\Object wprowadził porządek i przewidywalność do tego chaosu. Zobaczmy, jak wyglądało oryginalne zachowanie PHP:

echo $obj->undeclared;    // E_NOTICE, później E_WARNING
$obj->undeclared = 1;     // przechodzi bez ostrzeżenia
$obj->unknownMethod();    // Fatal error (nieprzechwytywany przez try/catch)

Fatal error powodował zakończenie aplikacji bez możliwości reakcji. Ciche zapisywanie do nieistniejących członków bez ostrzeżenia mogło prowadzić do poważnych błędów, które trudno było wykryć. Nette\Object przechwytywał wszystkie te przypadki i rzucał wyjątek MemberAccessException, co pozwalało programistom reagować na błędy i je obsługiwać:

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

Od PHP 7.0 język nie powoduje już nieprzechwytywanych błędów krytycznych, a od PHP 8.2 dostęp do niezadeklarowanych członków jest traktowany jako błąd.

Podpowiedź “Did you mean?”

Nette\Object wprowadził bardzo przydatną funkcję: inteligentne podpowiedzi przy literówkach. Gdy programista popełnił błąd w nazwie metody lub zmiennej, nie tylko zgłaszał błąd, ale także oferował pomoc w postaci sugestii prawidłowej nazwy. Ta charakterystyczna wiadomość, znana jako “did you mean?”, zaoszczędziła programistom godziny szukania literówek:

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

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

Dzisiejsze PHP nie ma wprawdzie żadnej formy „did you mean?", ale ten dodatek potrafi uzupełniać Tracy. A nawet automatycznie poprawiać takie błędy.

Properties z kontrolowanym dostępem

Znaczącą innowacją, którą SmartObject wprowadził do PHP, były properties z kontrolowanym dostępem. Ta koncepcja, powszechna w językach takich jak C# czy Python, pozwoliła programistom elegancko kontrolować dostęp do danych obiektu i zapewnić ich spójność. Properties są potężnym narzędziem programowania obiektowego. Działają jak zmienne, ale w rzeczywistości są reprezentowane przez metody (gettery i settery). Umożliwia to walidację danych wejściowych lub generowanie wartości w momencie odczytu.

Aby używać properties, musisz:

  • Dodać do klasy adnotację w formacie @property <type> $xyz
  • Utworzyć getter o nazwie getXyz() lub isXyz(), setter o nazwie setXyz()
  • Zapewnić, aby getter i setter były public lub protected. Są opcjonalne – mogą więc istnieć jako read-only lub write-only property

Zobaczmy praktyczny przykład na klasie Circle, gdzie użyjemy properties, aby zapewnić, że promień będzie zawsze nieujemny. Zastąpimy oryginalne public $radius przez property:

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

	private float $radius = 0.0; // nie jest public!

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

	// setter dla property $radius
	protected function setRadius(float $radius): void
	{
		// sanityzacja wartości przed zapisem
		$this->radius = max(0.0, $radius);
	}

	// getter dla property $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // w rzeczywistości wywołuje setRadius(10)
echo $circle->radius;  // wywołuje getRadius()
echo $circle->visible; // wywołuje isVisible()

Od PHP 8.4 można osiągnąć tę samą funkcjonalność za pomocą property hooks, które oferują znacznie elegantszą i zwięzłą składnię:

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

Extension methods

Nette\Object wprowadził do PHP kolejną interesującą koncepcję zainspirowaną nowoczesnymi językami programowania – extension methods. Ta funkcja, zapożyczona z C#, pozwoliła programistom elegancko rozszerzać istniejące klasy o nowe metody bez konieczności ich modyfikacji lub dziedziczenia. Na przykład można było dodać do formularza metodę addDateTime(), która dodaje własny DateTimePicker:

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

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

Extension methods okazały się niepraktyczne, ponieważ ich nazwy nie były podpowiadane przez edytory, przeciwnie – zgłaszały, że metoda nie istnieje. Dlatego ich wsparcie zostało zakończone. Obecnie częściej stosuje się kompozycję lub dziedziczenie do rozszerzania funkcjonalności klas.

Pobieranie nazwy klasy

Do pobrania nazwy klasy SmartObject oferował prostą metodę:

$class = $obj->getClass(); // za pomocą Nette\Object
$class = $obj::class;      // od PHP 8.0

Dostęp do reflection i adnotacji

Nette\Object oferował dostęp do reflection i adnotacji za pomocą metod getReflection() i getAnnotation(). To podejście znacząco uprościło pracę z metainformacjami klas:

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

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // zwraca 'John Doe'

Od PHP 8.0 można uzyskiwać dostęp do metainformacji za pomocą atrybutów, które oferują jeszcze większe możliwości i lepszą kontrolę typów:

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

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

Method gettery

Nette\Object oferował elegancki sposób przekazywania metod, jakby były 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 wykorzystać tzw. first-class callable syntax, która rozwija ten koncept jeszcze dalej:

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

Wydarzenia

SmartObject oferuje uproszczoną składnię do pracy z wydarzeniami. Wydarzenia pozwalają obiektom informować inne części aplikacji o zmianach swojego stanu:

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 następującej pętli:

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

Ze względu na przejrzystość zalecamy unikanie magicznej metody $this->onChange(). Praktycznym zamiennikiem jest funkcja Nette\Utils\Arrays::invoke:

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