SmartObject
SmartObject przez wiele lat ulepszał zachowanie obiektów w PHP. Od PHP 8.4 wszystkie jego możliwości stały się natywną częścią samego PHP, przez co dopełnił swoją historyczną misję pioniera nowoczesnego podejścia obiektowego w PHP.
Instalacja:
composer require nette/utils
SmartObject pojawił się w 2007 roku jako rewolucyjne rozwiązanie niedostatków ówczesnego modelu obiektowego PHP. W czasach, gdy PHP borykało się z licznymi problemami projektowania obiektowego, przyniósł znaczące ulepszenia i uprościł pracę programistom. Stał się legendarnym elementem Nette Framework. SmartObject oferował funkcjonalność, którą PHP zyskało dopiero wiele lat później – od kontroli dostępu do właściwości obiektów po wyrafinowany lukier składniowy. Wraz z wydaniem PHP 8.4 dopełnił swoją historyczną misję, bo większość jego możliwości stała się natywną częścią języka. Wyprzedził rozwój PHP o imponujące 17 lat.
Technicznie SmartObject przeszedł ciekawą ewolucję. Początkowo był zaimplementowany jako klasa Nette\Object,
po której inne klasy dziedziczyły potrzebną funkcjonalność. Istotna zmiana przyszła z PHP 5.4, które wprowadziło
obsługę traitów. Umożliwiło to przekształcenie w trait Nette\SmartObject, co dało większą elastyczność:
programiści mogli używać tej funkcjonalności również w klasach, które już po czymś dziedziczyły. Pierwotna klasa
Nette\Object przestała istnieć wraz z PHP 7.2 (które zabroniło nazywania klas słowem Object), ale
trait Nette\SmartObject żyje dalej.
Przyjrzyjmy się możliwościom, które oferowały Nette\Object, a później Nette\SmartObject.
Każda z nich 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 przy pracy z obiektami.
Nette\Object wniósł do tego chaosu porządek i przewidywalność. Zobaczmy, jak PHP zachowywało się
pierwotnie:
echo $obj->undeclared; // E_NOTICE, później E_WARNING
$obj->undeclared = 1; // przechodzi po cichu bez ostrzeżenia
$obj->unknownMethod(); // Fatal error (nie do złapania przez try/catch)
Błąd fatalny kończył aplikację bez jakiejkolwiek możliwości reakcji. Ciche zapisywanie do nieistniejących składowych
bez ostrzeżenia mogło prowadzić do poważnych, trudnych do wykrycia błędów. Nette\Object wyłapywał wszystkie
te przypadki i zgłaszał MemberAccessException, dzięki czemu programiści mogli na te błędy reagować i je
obsługiwać:
echo $obj->undeclared; // zgłasza Nette\MemberAccessException
$obj->undeclared = 1; // zgłasza Nette\MemberAccessException
$obj->unknownMethod(); // zgłasza Nette\MemberAccessException
Od PHP 7.0 język nie powoduje już błędów fatalnych nie do złapania, a od PHP 8.2 dostęp do niezadeklarowanych składowych uznawany jest za błąd.
Pomocnik „did you mean?“
Nette\Object przyszedł z bardzo wygodną funkcją: inteligentnymi podpowiedziami przy literówkach. Gdy
programista pomylił się w nazwie metody albo zmiennej, nie tylko zgłaszał błąd, ale też oferował pomoc, podpowiadając
poprawną nazwę. Ten kultowy komunikat, znany jako „did you mean?“, oszczędził programistom godziny polowania na
literówki:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// zgłasza Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Współczesne PHP nie ma żadnej formy „did you mean?“, ale taki dopisek może dodawać do błędów Tracy. A nawet potrafi takie błędy automatycznie naprawiać.
Właściwości z kontrolowanym dostępem
Istotną nowością, którą SmartObject wniósł do PHP, były właściwości 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ść. Właściwości to potężne narzędzie programowania obiektowego. Zachowują się jak zmienne, ale w rzeczywistości reprezentowane są przez metody (gettery i settery). Pozwala to walidować dane wejściowe albo generować wartość w chwili odczytu.
Aby użyć właściwości, trzeba było:
- dodać do klasy adnotację
@property <typ> $xyz - utworzyć getter o nazwie
getXyz()alboisXyz()i setter o nazwiesetXyz() - zadbać, aby getter i setter były public albo protected. Były opcjonalne, mogły więc istnieć właściwości tylko do odczytu albo tylko do zapisu
Spójrzmy na praktyczny przykład z klasą Circle, w której użyjemy właściwości, aby zapewnić, że promień
jest zawsze nieujemny. Zastąpimy public $radius właściwością:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // nie public!
// getter właściwości $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter właściwości $radius
protected function setRadius(float $radius): void
{
// przed zapisem oczyszczamy wartość
$this->radius = max(0.0, $radius);
}
// getter właściwości $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // w rzeczywistości wywoła setRadius(10)
echo $circle->radius; // wywoła getRadius()
echo $circle->visible; // wywoła isVisible()
Od PHP 8.4 tę samą funkcjonalność można uzyskać za pomocą property hooks, które oferują o wiele elegantszą i zwięźlejszą składnię:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Metody rozszerzające
Nette\Object wniósł do PHP jeszcze jedną ciekawą koncepcję zainspirowaną nowoczesnymi językami
programowania: metody rozszerzające. Ta funkcja, zapożyczona z C#, pozwalała programistom elegancko rozszerzać istniejące
klasy o nowe metody, bez modyfikowania ich ani dziedziczenia po nich. Mogłeś na przykład 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');
Metody rozszerzające okazały się niepraktyczne, bo edytory kodu nie podpowiadały ich nazw, tylko zgłaszały, że metoda nie istnieje. Dlatego ich wsparcie zostało zakończone. Dziś do rozszerzania funkcjonalności klas częściej używa się kompozycji albo dziedziczenia.
Pobranie nazwy klasy
SmartObject oferował prostą metodę do pobrania nazwy klasy:
$class = $obj->getClass(); // z użyciem Nette\Object
$class = $obj::class; // od PHP 8.0
Dostęp do refleksji i adnotacji
Nette\Object udostępniał dostęp do refleksji i adnotacji przez metody getReflection() i
getAnnotation(). Podejście to znacząco upraszczało pracę z metainformacjami klasy:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // zwraca 'John Doe'
Od PHP 8.0 do metainformacji można sięgać przez atrybuty, które oferują jeszcze więcej 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 przekazywania metod tak, 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żesz użyć first-class callable syntax, która idzie z tą koncepcją jeszcze dalej:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Zdarzenia
SmartObject oferuje uproszczoną składnię pracy ze zdarzeniami. Zdarzenia pozwalają obiektom informować inne części aplikacji o zmianach swojego stanu:
class Circle
{
use Nette\SmartObject;
public array $onChange = [];
private float $radius = 0.0;
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);
}
Dla przejrzystości zalecamy unikanie magicznej metody $this->onChange(). Praktycznym zamiennikiem jest funkcja
Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);