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()
lubisXyz()
, setter o nazwiesetXyz()
- 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);