SmartObject
SmartObject verbesserte jahrelang das Verhalten von Objekten in PHP. Seit PHP 8.4 sind alle seine Funktionen nativ in PHP selbst integriert, womit er seine historische Mission als Wegbereiter des modernen objektorientierten Ansatzes in PHP erfüllt hat.
Installation:
composer require nette/utils
SmartObject entstand 2007 als revolutionäre Lösung für die Mängel des damaligen PHP-Objektmodells. In einer Zeit, als PHP unter zahlreichen Problemen mit objektorientiertem Design litt, brachte er bedeutende Verbesserungen und vereinfachte die Arbeit der Entwickler. Er wurde zu einem legendären Bestandteil des Nette Frameworks. Er bot Funktionalitäten, die PHP erst Jahre später erhielt – von der Validierung des Eigenschaftszugriffs bis hin zur ausgefeilten Fehlerbehandlung. Mit der Einführung von PHP 8.4 hat er seine historische Mission erfüllt, da alle seine Funktionen zu nativen Bestandteilen der Sprache wurden. SmartObject war der PHP-Entwicklung um bemerkenswerte 17 Jahre voraus.
SmartObject durchlief eine interessante technische Entwicklung. Ursprünglich wurde er als Klasse Nette\Object
implementiert, von der andere Klassen die benötigte Funktionalität erbten. Eine wesentliche Änderung kam mit PHP 5.4, das
Trait-Unterstützung einführte. Dies ermöglichte die Umwandlung in den Nette\SmartObject
Trait, was größere
Flexibilität brachte – Entwickler konnten die Funktionalität auch in Klassen nutzen, die bereits von einer anderen Klasse
erbten. Während die ursprüngliche Nette\Object
-Klasse mit PHP 7.2 verschwand (das die Benennung von Klassen mit
dem Wort ‘Object’ verbot), lebt der Nette\SmartObject
Trait weiter.
Schauen wir uns die Funktionen an, die Nette\Object
und später Nette\SmartObject
anboten. Jede
dieser Funktionen stellte zu ihrer Zeit einen bedeutenden Fortschritt in der objektorientierten Programmierung in PHP dar.
Konsistente Fehlerzustände
Eines der größten Probleme des frühen PHP war das inkonsistente Verhalten bei der Arbeit mit Objekten.
Nette\Object
brachte Ordnung und Vorhersehbarkeit in dieses Chaos. Betrachten wir das ursprüngliche
PHP-Verhalten:
echo $obj->undeclared; // E_NOTICE, später E_WARNING
$obj->undeclared = 1; // läuft still durch ohne Warnung
$obj->unknownMethod(); // Fatal error (nicht abfangbar mit try/catch)
Fatal Error beendete die Anwendung ohne Möglichkeit zu reagieren. Stilles Schreiben in nicht existierende Mitglieder ohne
Warnung konnte zu schwerwiegenden Fehlern führen, die schwer zu erkennen waren. Nette\Object
fing all diese Fälle
ab und warf eine MemberAccessException
, was Programmierern ermöglichte, auf Fehler zu reagieren:
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 Fatal Errors mehr, und seit PHP 8.2 wird der Zugriff auf nicht deklarierte Mitglieder als Fehler betrachtet.
“Did you mean?” Hilfe
Nette\Object
kam mit einer sehr praktischen Funktion: intelligenten Vorschlägen bei Tippfehlern. Wenn ein
Entwickler einen Fehler im Namen einer Methode oder Variable machte, wurde nicht nur der Fehler gemeldet, sondern auch eine
Hilfestellung in Form eines Vorschlags für den richtigen Namen angeboten. Diese ikonische Meldung, bekannt als “did you
mean?”, ersparte Programmierern stundenlange 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()?"
Während PHP selbst keine Form von “did you mean?” hat, wird diese Funktion jetzt von Tracy bereitgestellt. Es kann solche Fehler sogar automatisch korrigieren.
Properties mit kontrolliertem Zugriff
Eine bedeutende Innovation, die SmartObject in PHP einführte, waren Properties mit kontrolliertem Zugriff. Dieses Konzept, das in Sprachen wie C# oder Python üblich ist, ermöglichte Entwicklern eine elegante Kontrolle über den Zugriff auf Objektdaten und deren Konsistenz. Properties sind ein mächtiges Werkzeug der objektorientierten Programmierung. Sie funktionieren wie Variablen, werden aber tatsächlich durch Methoden (Getter und Setter) repräsentiert. Dies ermöglicht die Validierung von Eingaben oder die Generierung von Werten zum Zeitpunkt des Lesens.
Für die Verwendung von Properties müssen Sie:
- Der Klasse die Annotation
@property <type> $xyz
hinzufügen - Einen Getter mit Namen
getXyz()
oderisXyz()
, einen Setter mit NamensetXyz()
erstellen - Sicherstellen, dass Getter und Setter public oder protected sind. Sie sind optional – können also als read-only oder write-only Properties existieren
Schauen wir uns ein praktisches Beispiel an der Circle-Klasse an, wo wir Properties verwenden, um sicherzustellen, dass der
Radius immer nicht-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 Property $radius
protected function getRadius(): float
{
return $this->radius;
}
// Setter für Property $radius
protected function setRadius(float $radius): void
{
// Wert vor dem Speichern bereinigen
$this->radius = max(0.0, $radius);
}
// Getter für Property $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // ruft tatsächlich setRadius(10) auf
echo $circle->radius; // ruft getRadius() auf
echo $circle->visible; // ruft isVisible() auf
Seit PHP 8.4 kann die gleiche Funktionalität mit Property Hooks erreicht werden, die eine elegantere und kürzere Syntax bieten:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Extension Methods
Nette\Object
brachte ein weiteres interessantes Konzept nach PHP, inspiriert von modernen Programmiersprachen –
Extension Methods. Diese Funktion, die von C# übernommen wurde, ermöglichte Entwicklern, bestehende Klassen elegant um neue
Methoden zu erweitern, ohne sie zu modifizieren oder von ihnen zu erben. Zum Beispiel konnten Sie einem Formular eine
addDateTime()
-Methode hinzufügen, die einen benutzerdefinierten DateTimePicker hinzufügt:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Extension Methods erwiesen sich als unpraktisch, da Editoren ihre Namen nicht vorschlugen und stattdessen meldeten, dass die Methode nicht existiert. Daher wurde ihre Unterstützung eingestellt. Heute ist es üblicher, Komposition oder Vererbung zu verwenden, um Klassenfunktionalität zu erweitern.
Klassennamen ermitteln
SmartObject bot eine einfache Methode zum Ermitteln des Klassennamens:
$class = $obj->getClass(); // mit Nette\Object
$class = $obj::class; // seit PHP 8.0
Zugriff auf Reflection und Annotationen
Nette\Object
bot Zugriff auf Reflection und Annotationen durch die Methoden getReflection()
und
getAnnotation()
. Dieser Ansatz vereinfachte die Arbeit mit Klassen-Metainformationen 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 ist es möglich, auf Metainformationen durch Attribute zuzugreifen, die noch mehr Möglichkeiten und bessere Typenprüfung bieten:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Method Getters
Nette\Object
bot eine elegante Möglichkeit, Methoden wie Variablen zu übergeben:
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 führt:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Events
SmartObject bietet eine vereinfachte Syntax für die Arbeit mit Events. Events ermöglichen es Objekten, andere Teile der Anwendung über Änderungen ihres Zustands zu informieren:
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
Der Code $this->onChange($this, $radius)
ist äquivalent zu folgender 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);