SmartObject
SmartObject hat das Verhalten von PHP-Objekten viele Jahre lang verbessert. Seit PHP 8.4 sind alle seine Fähigkeiten nativer Bestandteil von PHP selbst, womit es seine historische Mission als Pionier des modernen objektorientierten Ansatzes in PHP erfüllt hat.
Installation:
composer require nette/utils
SmartObject kam 2007 als revolutionäre Lösung für die damaligen Schwächen des Objektmodells von PHP. In einer Zeit, in der PHP mit zahlreichen Problemen des objektorientierten Entwurfs kämpfte, brachte es deutliche Verbesserungen und vereinfachte die Arbeit der Entwickler. Es wurde zu einem legendären Bestandteil des Nette Frameworks. SmartObject bot Funktionen, die PHP erst viele Jahre später bekam – von der Zugriffskontrolle für Properties von Objekten bis zu ausgefeiltem Syntaxzucker. Mit der Veröffentlichung von PHP 8.4 hat es seine historische Mission erfüllt, denn die meisten seiner Fähigkeiten wurden zu einem nativen Teil der Sprache. Es war der Entwicklung von PHP um beeindruckende 17 Jahre voraus.
Technisch hat SmartObject eine interessante Entwicklung durchlaufen. Anfangs war es als Klasse Nette\Object
umgesetzt, von der andere Klassen die benötigte Funktionalität erbten. Eine wesentliche Änderung brachte PHP 5.4 mit der
Unterstützung von Traits. Das ermöglichte die Umwandlung in den Trait Nette\SmartObject und brachte mehr
Flexibilität: Entwickler konnten die Funktionalität auch in Klassen nutzen, die bereits von einer anderen Klasse erbten.
Während die ursprüngliche Klasse Nette\Object mit PHP 7.2 verschwand (das die Benennung von Klassen mit dem Wort
Object verbot), lebt der Trait Nette\SmartObject weiter.
Sehen wir uns die Fähigkeiten an, die Nette\Object und später Nette\SmartObject boten. Jede von
ihnen war zu ihrer Zeit ein bedeutender Schritt nach vorn in der objektorientierten Programmierung mit PHP.
Konsistente Fehlerzustände
Eines der drängendsten Probleme des frühen PHP war das inkonsistente Verhalten bei der Arbeit mit Objekten.
Nette\Object brachte Ordnung und Vorhersagbarkeit in dieses Chaos. Sehen wir uns an, wie sich PHP ursprünglich
verhielt:
echo $obj->undeclared; // E_NOTICE, später E_WARNING
$obj->undeclared = 1; // geht stillschweigend durch, ohne Warnung
$obj->unknownMethod(); // Fatal error (nicht mit try/catch abfangbar)
Ein fataler Fehler beendete die Anwendung, ohne jede Möglichkeit zu reagieren. Das stillschweigende Schreiben in nicht
existierende Mitglieder ohne Warnung konnte zu ernsten Fehlern führen, die sich nur schwer aufspüren ließen.
Nette\Object fing alle diese Fälle ab und warf eine MemberAccessException, sodass Programmierer auf
diese Fehler reagieren und sie behandeln konnten:
echo $obj->undeclared; // wirft Nette\MemberAccessException
$obj->undeclared = 1; // wirft Nette\MemberAccessException
$obj->unknownMethod(); // wirft Nette\MemberAccessException
Seit PHP 7.0 verursacht die Sprache keine nicht abfangbaren fatalen Fehler mehr, und seit PHP 8.2 gilt der Zugriff auf nicht deklarierte Mitglieder als Fehler.
Helfer “Did you mean?”
Nette\Object kam mit einer sehr bequemen Fähigkeit: intelligenten Vorschlägen bei Tippfehlern. Vertippte sich
ein Entwickler im Namen einer Methode oder Variablen, meldete es nicht nur den Fehler, sondern bot Hilfe an, indem es den
richtigen Namen vorschlug. Diese ikonische Meldung, bekannt als “did you mean?”, ersparte Programmierern Stunden der Suche
nach Tippfehlern:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// wirft Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Das heutige PHP hat zwar keinerlei Form von “did you mean?”, diesen Zusatz kann Fehlern aber Tracy hinzufügen. Und es kann solche Fehler sogar automatisch beheben.
Properties mit kontrolliertem Zugriff
Eine bedeutende Neuerung, die SmartObject nach PHP brachte, waren Properties mit kontrolliertem Zugriff. Dieses Konzept, das in Sprachen wie C# oder Python üblich ist, erlaubte es Entwicklern, den Zugriff auf die Daten eines Objekts elegant zu steuern und deren Konsistenz sicherzustellen. Properties sind ein mächtiges Werkzeug der objektorientierten Programmierung. Sie verhalten sich wie Variablen, werden in Wirklichkeit aber durch Methoden (Getter und Setter) dargestellt. Das erlaubt es, Eingaben zu validieren oder Werte erst beim Lesen zu erzeugen.
Um Properties zu nutzen, mussten Sie:
- der Klasse die Annotation
@property <type> $xyzhinzufügen - einen Getter namens
getXyz()oderisXyz()und einen Setter namenssetXyz()anlegen - dafür sorgen, dass Getter und Setter public oder protected waren. Sie waren optional, es konnte also auch read-only- oder write-only-Properties geben
Sehen wir uns ein praktisches Beispiel mit der Klasse Circle an, in der wir mit einer Property sicherstellen, dass
der Radius nie negativ ist. Wir ersetzen public $radius durch eine Property:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // nicht public!
// Getter für die Property $radius
protected function getRadius(): float
{
return $this->radius;
}
// Setter für die Property $radius
protected function setRadius(float $radius): void
{
// den Wert vor dem Speichern bereinigen
$this->radius = max(0.0, $radius);
}
// Getter für die Property $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // ruft in Wirklichkeit setRadius(10) auf
echo $circle->radius; // ruft getRadius() auf
echo $circle->visible; // ruft isVisible() auf
Seit PHP 8.4 lässt sich dieselbe Funktionalität mit Property Hooks erreichen, die eine viel elegantere und knappere Syntax bieten:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Extension-Methoden
Nette\Object brachte noch ein weiteres interessantes Konzept nach PHP, inspiriert von modernen
Programmiersprachen: Extension-Methoden. Diese von C# entlehnte Fähigkeit erlaubte es Entwicklern, bestehende Klassen elegant um
neue Methoden zu erweitern, ohne sie zu verändern oder von ihnen zu erben. Sie konnten einem Formular zum Beispiel eine Methode
addDateTime() hinzufügen, die einen eigenen DateTimePicker ergänzt:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Extension-Methoden erwiesen sich als unpraktisch, weil Code-Editoren ihre Namen nicht vorschlugen, sondern stattdessen meldeten, die Methode existiere nicht. Ihre Unterstützung wurde deshalb eingestellt. Heute ist es üblicher, Klassen über Komposition oder Vererbung zu erweitern.
Ermitteln des Klassennamens
SmartObject bot eine einfache Methode, um den Namen der Klasse zu ermitteln:
$class = $obj->getClass(); // mit Nette\Object
$class = $obj::class; // seit PHP 8.0
Zugriff auf Reflection und Annotationen
Nette\Object bot über die Methoden getReflection() und getAnnotation() Zugriff auf
Reflection und Annotationen. Dieser Ansatz vereinfachte die Arbeit mit den Metainformationen einer Klasse erheblich:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // gibt 'John Doe' zurück
Seit PHP 8.0 lässt sich auf Metainformationen über Attribute zugreifen, die noch mehr Möglichkeiten und eine bessere Typprüfung bieten:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Getter für Methoden
Nette\Object bot eine elegante Möglichkeit, Methoden zu übergeben, als wären sie Variablen:
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
Seit PHP 8.1 können Sie die First-Class Callable Syntax verwenden, die dieses Konzept noch weiter treibt:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Events
SmartObject bietet eine vereinfachte Syntax für die Arbeit mit Events. Events erlauben es Objekten, andere Teile der Anwendung über Änderungen ihres Zustands zu informieren:
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;
}
}
Der Code $this->onChange($this, $radius) entspricht der folgenden Schleife:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Der Übersichtlichkeit halber empfehlen wir, die magische Methode $this->onChange() zu vermeiden. Ein
praktischer Ersatz ist die Funktion Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);