SmartObject

SmartObject hat das Verhalten von Objekten in vielerlei Hinsicht verbessert, aber das heutige PHP enthält die meisten dieser Verbesserungen bereits von Haus aus. Es fügt jedoch noch Unterstützung für Property hinzu.

Installation:

composer require nette/utils

Eigenschaften, Getters und Setters

In modernen objektorientierten Sprachen (z. B. C#, Python, Ruby, JavaScript) bezieht sich der Begriff Eigenschaft auf spezielle Mitglieder von Klassen, die wie Variablen aussehen, aber eigentlich durch Methoden repräsentiert werden. Wenn der Wert dieser “Variablen” zugewiesen oder gelesen wird, wird die entsprechende Methode (Getter oder Setter genannt) aufgerufen. Das ist sehr praktisch, denn es gibt uns die volle Kontrolle über den Zugriff auf Variablen. Wir können die Eingabe validieren oder Ergebnisse nur dann erzeugen, wenn die Eigenschaft gelesen wird.

PHP-Eigenschaften werden nicht unterstützt, aber Trait Nette\SmartObject kann sie imitieren. Wie verwendet man es?

  • Fügen Sie der Klasse eine Annotation in der Form @property <type> $xyz
  • Erstellen Sie einen Getter namens getXyz() oder isXyz(), einen Setter namens setXyz()
  • Die Getter und Setter müssen public oder protected sein und sind optional, d.h. es kann eine read-only oder write-only Eigenschaft geben

Wir werden die Eigenschaft für die Klasse Circle verwenden, um sicherzustellen, dass nur nicht-negative Zahlen in die Variable $radius eingegeben werden. Ersetzen Sie public $radius durch property:

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // not public

	// getter for property $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter for property $radius
	protected function setRadius(float $radius): void
	{
		// sanitizing value before saving it
		$this->radius = max(0.0, $radius);
	}

	// getter for property $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // actually calls setRadius(10)
echo $circle->radius;  // calls getRadius()
echo $circle->visible; // calls isVisible()

Eigenschaften sind in erster Linie “syntaktischer Zucker” ((syntactic sugar)), der das Leben des Programmierers durch Vereinfachung des Codes versüßen soll. Wenn Sie sie nicht wollen, müssen Sie sie nicht verwenden.

Ein Blick in die Geschichte

SmartObject verfeinerte das Verhalten von Objekten auf zahlreiche Arten, aber das heutige PHP enthält die meisten dieser Erweiterungen bereits von Haus aus. Der folgende Text ist ein nostalgischer Blick zurück in die Geschichte, der uns daran erinnert, wie sich die Dinge entwickelt haben.

Seit seinen Anfängen litt das Objektmodell von PHP unter einer Vielzahl von ernsthaften Mängeln und Unzulänglichkeiten. Dies führte zur Schaffung der Klasse Nette\Object (im Jahr 2007), die diese Probleme beheben und den Komfort bei der Verwendung von PHP erhöhen sollte. Andere Klassen brauchten nur von dieser Klasse zu erben, um die Vorteile zu nutzen, die sie bot. Als mit PHP 5.4 die Unterstützung für Traits eingeführt wurde, wurde die Klasse Nette\Object durch den Trait Nette\SmartObject ersetzt. Damit entfiel die Notwendigkeit, von einem gemeinsamen Vorfahren zu erben. Außerdem konnte der Trait in Klassen verwendet werden, die bereits von einer anderen Klasse geerbt hatten. Das endgültige Ende von Nette\Object kam mit der Veröffentlichung von PHP 7.2, die es verbot, Klassen den Namen Object zu geben.

Mit der weiteren Entwicklung von PHP wurden das Objektmodell und die Sprachfähigkeiten verbessert. Verschiedene Funktionen der Klasse SmartObject wurden überflüssig. Seit der Veröffentlichung von PHP 8.2 gibt es nur noch eine Funktion, die in PHP nicht direkt unterstützt wird: die Möglichkeit, so genannte Properties zu verwenden.

Welche Funktionen boten Nette\Object und in der Folge Nette\SmartObject? Hier ist ein Überblick. (In den Beispielen wird die Klasse Nette\Object verwendet, aber die meisten Funktionen gelten auch für den Trait Nette\SmartObject ).

Inkonsistente Fehler

PHP hatte ein inkonsistentes Verhalten beim Zugriff auf nicht deklarierte Mitglieder. Der Zustand zum Zeitpunkt von Nette\Object war wie folgt:

echo $obj->undeclared; // E_NOTICE, later E_WARNING
$obj->undeclared = 1;  // passes silently without reporting
$obj->unknownMethod(); // Fatal error (not catchable by try/catch)

Ein schwerwiegender Fehler beendete die Anwendung, ohne dass eine Möglichkeit zur Reaktion bestand. Das stille Schreiben auf nicht existierende Mitglieder ohne Warnung konnte zu schwerwiegenden Fehlern führen, die schwer zu erkennen waren. Nette\Object Alle diese Fälle wurden abgefangen und eine Ausnahme MemberAccessException wurde ausgelöst.

echo $obj->undeclared;   // throw Nette\MemberAccessException
$obj->undeclared = 1;    // throw Nette\MemberAccessException
$obj->unknownMethod();   // throw Nette\MemberAccessException

Seit PHP 7.0 verursacht PHP keine nicht abfangbaren fatalen Fehler mehr, und der Zugriff auf nicht deklarierte Member ist seit PHP 8.2 ein Fehler.

Haben Sie gemeint?

Wenn ein Nette\MemberAccessException -Fehler ausgelöst wurde, etwa aufgrund eines Tippfehlers beim Zugriff auf eine Objektvariable oder beim Aufruf einer Methode, versuchte Nette\Object, in der Fehlermeldung einen Hinweis darauf zu geben, wie der Fehler zu beheben ist, und zwar in Form des ikonischen Zusatzes “Meinten Sie?”.

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// throw Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Das heutige PHP hat zwar keine “Meinten Sie?”-Funktion, aber dieser Satz kann von Tracy zu Fehlern hinzugefügt werden. Es kann solche Fehler sogar automatisch korrigieren.

Erweiterungsmethoden

Inspiriert von Erweiterungsmethoden aus C#. Sie bieten die Möglichkeit, neue Methoden zu bestehenden Klassen hinzuzufügen. Zum Beispiel könnten Sie die Methode addDateTime() zu einem Formular hinzufügen, um Ihren eigenen DateTimePicker hinzuzufügen.

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

Erweiterungsmethoden erwiesen sich als unpraktisch, da ihre Namen von Editoren nicht automatisch vervollständigt wurden, sondern sie meldeten, dass die Methode nicht existierte. Daher wurde ihre Unterstützung eingestellt.

Ermitteln des Klassennamens

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

Zugang zu Reflexion und Anmerkungen

Nette\Object bietet den Zugang zu Reflexion und Kommentaren mit den Methoden getReflection() und getAnnotation():

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // returns 'John Doe'

Seit PHP 8.0 ist es möglich, auf Metainformationen in Form von Attributen zuzugreifen:

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

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

Methoden-Getter

Nette\Object boten eine elegante Möglichkeit, mit Methoden so umzugehen, 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 sogenannte First-Class-Callable-Syntax verwenden:

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

Ereignisse

Nette\Object bietet syntaktischen Zucker zum Auslösen des Ereignisses:

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) entspricht folgendem:

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

Aus Gründen der Übersichtlichkeit wird empfohlen, die magische Methode $this->onChange() zu vermeiden. Ein guter Ersatz ist Nette\Utils\Arrays::invoke:

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