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() oder isXyz(), einen Setter mit Namen setXyz() 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);
Version: 4.0