SmartObject
SmartObject przez lata ulepszał zachowanie obiektów w PHP. Od wersji PHP 8.4 wszystkie jego funkcje są już częścią samego PHP, czym zakończył swoją historyczną misję bycia pionierem nowoczesnego podejścia obiektowego w PHP.
Instalacja:
composer require nette/utils
SmartObject powstał w 2007 roku jako rewolucyjne rozwiązanie niedociągnięć ówczesnego modelu obiektowego PHP. W czasach, gdy PHP cierpiało z powodu wielu problemów z projektowaniem obiektowym, przyniósł znaczące ulepszenie i uproszczenie pracy dla deweloperów. Stał się legendarną częścią frameworka Nette. Oferował funkcjonalność, którą PHP uzyskało dopiero wiele lat później – od kontroli dostępu do właściwości obiektów po wyrafinowane cukierki składniowe. 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 niezwykłe 17 lat.
Technicznie SmartObject przeszedł interesujący rozwój. Początkowo był zaimplementowany jako klasa
Nette\Object
, od której inne klasy dziedziczyły potrzebną funkcjonalność. Zasadnicza zmiana przyszła wraz
z PHP 5.4, które przyniosło wsparcie dla traitów. Umożliwiło to transformację do postaci traitu
Nette\SmartObject
, co przyniosło większą elastyczność – deweloperzy mogli wykorzystać funkcjonalność
również w klasach, które już dziedziczyły po innej klasie. Podczas gdy oryginalna klasa Nette\Object
zniknęła
wraz z nadejściem PHP 7.2 (które zakazało nazywania klas słowem Object
), trait Nette\SmartObject
żyje dalej.
Przejdźmy przez właściwości, które kiedyś oferowały Nette\Object
, a później
Nette\SmartObject
. Każda z tych funkcji w swoim czasie stanowiła znaczący krok naprzód w dziedzinie
programowania obiektowego 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ł do tego chaosu porządek i przewidywalność. Spójrzmy, jak wyglądało oryginalne
zachowanie PHP:
echo $obj->undeclared; // E_NOTICE, później E_WARNING
$obj->undeclared = 1; // przejdzie cicho bez zgłoszenia
$obj->unknownMethod(); // Fatal error (nie do przechwycenia za pomocą try/catch)
Fatal error kończył aplikację bez możliwości jakiejkolwiek reakcji. Cichy zapis do nieistniejących składowych bez
ostrzeżenia mógł prowadzić do poważnych błędów, które były trudne do wykrycia. Nette\Object
wszystkie te
przypadki przechwytywał i rzucał wyjątek MemberAccessException
, co pozwalało programistom reagować na błędy
i je rozwiązywać.
echo $obj->undeclared; // rzuci Nette\MemberAccessException
$obj->undeclared = 1; // rzuci Nette\MemberAccessException
$obj->unknownMethod(); // rzuci Nette\MemberAccessException
Od PHP 7.0 język już nie powoduje nieprzechwytywalnych błędów fatalnych, a od PHP 8.2 dostęp do niezadeklarowanych składowych jest uważany za błąd.
Pomoc “Did you mean?”
Nette\Object
przyszedł z bardzo przyjemną funkcją: inteligentną pomocą przy literówkach. Gdy deweloper
popełnił błąd w nazwie metody lub zmiennej, nie tylko zgłaszał błąd, ale także oferował pomocną dłoń w postaci
sugestii poprawnej nazwy. Ten ikoniczny komunikat, znany jako “did you mean?”, oszczędził programistom godziny szukania
literówek:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// rzuci Nette\MemberAccessException
// "Wywołanie niezdefiniowanej metody statycznej Foo::form(), czy chodziło ci o from()?"
Dzisiejsze PHP wprawdzie nie ma żadnej formy „did you mean?”, ale ten dopisek potrafi dodawać do błędów Tracy. A nawet takie błędy potrafi samo naprawiać.
Właściwości z kontrolowanym dostępem
Znaczącą innowacją, którą SmartObject wprowadził do PHP, były właściwości z kontrolowanym dostępem. Ten koncept, powszechny w językach takich jak C# czy Python, umożliwił deweloperom elegancko kontrolować dostęp do danych obiektu i zapewnić ich spójność. Właściwości są potężnym narzędziem programowania obiektowego. Działają jak zmienne, ale w rzeczywistości są reprezentowane przez metody (gettery i settery). To pozwala walidować dane wejściowe lub generować wartości dopiero w momencie odczytu.
Aby używać właściwości, musisz:
- Dodać klasie adnotację w postaci
@property <type> $xyz
- Stworzyć getter o nazwie
getXyz()
lubisXyz()
, setter o nazwiesetXyz()
- Zapewnić, aby getter i setter były public lub protected. Są opcjonalne – mogą więc istnieć jako właściwości read-only lub write-only
Pokażmy praktyczny przykład na klasie Circle, gdzie wykorzystamy właściwości do zapewnienia, że promień będzie zawsze
liczbą nieujemną. Zastąpimy oryginalny public $radius
właściwością:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // nie jest public!
// getter dla właściwości $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter dla właściwości $radius
protected function setRadius(float $radius): void
{
// wartość przed zapisem sanityzujemy
$this->radius = max(0.0, $radius);
}
// getter dla właściwości $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 bardziej elegancką i zwięzłą składnię:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Metody rozszerzeń
Nette\Object
wprowadził do PHP kolejny interesujący koncept inspirowany nowoczesnymi językami
programowania – metody rozszerzeń. Ta funkcja, przejęta z C#, umożliwiła deweloperom elegancko rozszerzać istniejące
klasy o nowe metody bez konieczności ich modyfikowania lub dziedziczenia po nich. Na przykład, można było dodać do
formularza metodę addDateTime()
, która doda własny DateTimePicker:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Metody rozszerzeń okazały się niepraktyczne, ponieważ ich nazwy nie były podpowiadane przez edytory, wręcz przeciwnie, zgłaszały, że metoda nie istnieje. Dlatego ich wsparcie zostało zakończone. Dziś częściej wykorzystuje się kompozycję lub dziedziczenie do rozszerzania funkcjonalności klas.
Pobieranie nazwy klasy
Do pobierania nazwy klasy SmartObject oferował prostą metodę:
$class = $obj->getClass(); // za pomocą Nette\Object
$class = $obj::class; // od PHP 8.0
Dostęp do refleksji i adnotacji
Nette\Object
oferował dostęp do refleksji i adnotacji za pomocą metod getReflection()
i
getAnnotation()
. To podejście znacznie uprościło pracę z metainformacjami klas:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // zwróci 'John Doe'
Od PHP 8.0 możliwe jest uzyskanie dostępu do metainformacji w postaci 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];
Gettery metod
Nette\Object
oferował elegancki sposób, jak przekazywać metody, 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 przenosi ten koncept jeszcze dalej:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Zdarzenia
SmartObject oferuje uproszczoną składnię do pracy ze zdarzeniami. Zdarzenia 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 czytelność zalecamy unikanie magicznej metody $this->onChange()
. Praktycznym zamiennikiem jest
na przykład funkcja Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);