SmartObject

SmartObject hat über Jahre hinweg das Verhalten von Objekten in PHP verbessert. Seit PHP 8.4 sind alle seine Funktionen bereits Teil 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 entstand 2007 als revolutionäre Lösung für die Mängel des damaligen Objektmodells von PHP. In einer Zeit, als PHP unter zahlreichen Problemen im Objektdesign litt, brachte es erhebliche Verbesserungen und vereinfachte die Arbeit für Entwickler. Es wurde zu einem legendären Bestandteil des Nette Frameworks. Es bot Funktionalität, die PHP erst viele Jahre später erhielt – von der Zugriffskontrolle auf Objekteigenschaften bis hin zu ausgefeiltem syntaktischem Zucker. Mit der Einführung von PHP 8.4 erfüllte es seine historische Mission, da alle seine Funktionen nativer Bestandteil der Sprache wurden. Es war der PHP-Entwicklung um bemerkenswerte 17 Jahre voraus.

Technisch durchlief SmartObject eine interessante Entwicklung. Ursprünglich wurde es als Klasse Nette\Object implementiert, von der andere Klassen die benötigte Funktionalität erbten. Eine grundlegende Änderung kam mit PHP 5.4, das Unterstützung für Traits einführte. Dies ermöglichte die Transformation in die Form des Traits Nette\SmartObject, 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 Klasse Nette\Object mit der Einführung von PHP 7.2 (das die Benennung von Klassen mit dem Wort Object verbot) verschwand, lebt der Trait Nette\SmartObject weiter.

Lassen Sie uns die Eigenschaften durchgehen, die einst Nette\Object und später Nette\SmartObject boten. Jede dieser Funktionen stellte zu ihrer Zeit einen bedeutenden Fortschritt im Bereich der objektorientierten Programmierung in PHP dar.

Konsistente Fehlerzustände

Eines der dringendsten Probleme des frühen PHP war das inkonsistente Verhalten bei der Arbeit mit Objekten. Nette\Object brachte Ordnung und Vorhersehbarkeit in dieses Chaos. Schauen wir uns an, wie das ursprüngliche Verhalten von PHP aussah:

echo $obj->undeclared;    // E_NOTICE, später E_WARNING
$obj->undeclared = 1;     // läuft leise ohne Meldung durch
$obj->unknownMethod();    // Fatal error (nicht abfangbar mit try/catch)

Ein Fatal Error beendete die Anwendung ohne die Möglichkeit, darauf zu reagieren. Das stille Schreiben in nicht existierende Mitglieder ohne Warnung konnte zu schwerwiegenden Fehlern führen, die schwer zu entdecken waren. Nette\Object fing all diese Fälle ab und warf eine MemberAccessException, was Programmierern ermöglichte, auf Fehler zu reagieren und sie zu beheben.

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.

Hilfe “Did you mean?”

Nette\Object kam mit einer sehr angenehmen Funktion: intelligenter Hilfe bei Tippfehlern. Wenn ein Entwickler einen Fehler im Namen einer Methode oder Variablen machte, meldete es nicht nur den Fehler, sondern bot auch eine helfende Hand in Form eines Vorschlags für den richtigen Namen. 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 keine Form von „did you mean?“, aber dieser Zusatz kann von Tracy zu Fehlern hinzugefügt werden. Und solche Fehler sogar selbst 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 es Entwicklern, den Zugriff auf Objektdaten elegant zu kontrollieren und deren Konsistenz sicherzustellen. 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 erst im Moment des Lesens.

Um Properties zu verwenden, müssen Sie:

  • Der Klasse eine Annotation im Format @property <type> $xyz hinzufügen
  • Einen Getter mit dem Namen getXyz() oder isXyz(), einen Setter mit dem Namen setXyz() erstellen
  • Sicherstellen, dass Getter und Setter public oder protected sind. Sie sind optional – sie können also als read-only oder write-only Property existieren

Zeigen wir ein praktisches Beispiel an der Klasse Circle, wo wir Properties verwenden, um sicherzustellen, dass der Radius immer eine nicht negative Zahl ist. Wir ersetzen das ursprüngliche 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
	{
		// wir bereinigen den Wert vor dem Speichern
		$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 dieselbe Funktionalität mit Property Hooks erreicht werden, die eine viel elegantere und prägnantere 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 in PHP ein, das von modernen Programmiersprachen inspiriert war – Extension Methods. Diese Funktion, übernommen aus C#, ermöglichte es Entwicklern, bestehende Klassen elegant um neue Methoden zu erweitern, ohne sie ändern oder von ihnen erben zu müssen. Zum Beispiel konnten Sie einem Formular eine Methode addDateTime() hinzufügen, die einen eigenen 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 ihre Namen von Editoren nicht vorgeschlagen wurden, sondern diese meldeten, dass die Methode nicht existiert. Daher wurde ihre Unterstützung eingestellt. Heutzutage ist es üblicher, Komposition oder Vererbung zur Erweiterung der Funktionalität von Klassen zu verwenden.

Ermitteln des Klassennamens

Zur Ermittlung des Klassennamens bot SmartObject eine einfache Methode:

$class = $obj->getClass(); // mit Nette\Object
$class = $obj::class;      // seit PHP 8.0

Zugriff auf Reflexion und Annotationen

Nette\Object bot Zugriff auf Reflexion und Annotationen über die Methoden getReflection() und getAnnotation(). Dieser Ansatz vereinfachte die Arbeit mit Metainformationen von Klassen 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 in Form von Attributen zuzugreifen, die noch mehr Möglichkeiten und eine bessere Typkontrolle bieten:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Methoden-Getter

Nette\Object bot eine elegante Möglichkeit, Methoden so 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 kann die sogenannte first-class callable syntax verwendet werden, die dieses Konzept noch weiterführt:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Ereignisse

SmartObject bietet eine vereinfachte Syntax für die Arbeit mit Ereignissen. Ereignisse 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 zum folgenden Zyklus:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Aus Gründen der Verständlichkeit empfehlen wir, die magische Methode $this->onChange() zu vermeiden. Eine praktische Alternative ist beispielsweise die Funktion Nette\Utils\Arrays::invoke:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
Version: 4.0