
Ein Value Object in PHP ist ein Typ, der Daten kapselt und sich allein über seine Eigenschaften unterscheidet. Anders als eine Entität besitzt es keinen eindeutigen Identifikator. Zwei Value Objects mit denselben Eigenschaftswerten müssen daher als gleich gelten.
Gute Kandidaten für Value Objects sind:
- ein Preis
- eine Telefonnummer
- eine Adresse
- ein Commit-Hash
- ein Entitäts-Identifikator
- und so weiter.
Beim Entwurf eines Value Object in PHP achtest du auf seine drei wichtigsten Eigenschaften: Unveränderlichkeit, strukturelle Gleichheit und Selbstvalidierung.
Beispiel für ein Value Object in PHP
final class Price
{
const EU = 'EU';
const CAD = 'CAD';
/** @var float */
private $amount;
/** @var string */
private $currency;
public function __construct(float $amount, string $currency = 'EU')
{
if ($amount < 0) {
throw new InvalidArgumentException("Le montant doit être une valeur positive: {$amount}.");
}
if (!in_array($currency, $this->getAvailableCurrencies())) {
throw new InvalidArgumentException("La devise doit être valide: {$currency}.");
}
$this->amount = $amount;
$this->currency = $currency;
}
private function getAvailableCurrencies(): array
{
return [self::EU, self::CAD];
}
public function getAmount(): float
{
return $this->amount;
}
public function getCurrency(): string
{
return $this->currency;
}
}Unveränderlichkeit
Sobald du ein Value Object in PHP instanziiert hast, muss es für den Rest der Anwendungslaufzeit unverändert bleiben. Willst du seinen Wert ändern, ersetzt du das Objekt vollständig.
Veränderliche Value Objects sind vertretbar, solange sie vollständig in einem lokalen Scope bleiben, mit einer einzigen Referenz auf das Objekt. Überall sonst handelst du dir Ärger ein.
Zurück zum vorherigen Beispiel: So aktualisierst du den Betrag eines Preis-Typs.
final class Price
{
// ...
private function hasSameCurrency(Price $price): bool
{
return $this->currency === $price->currency;
}
public function sum(Price $price): self
{
if (!$this->hasSameCurrency($price)) {
throw InvalidArgumentException(
"Vous ne pouvez additionner que les valeurs ayant la même devise: {$this->currency} !== {$price->currency}."
);
}
return new self($this->amount + $price->amount, $this->currency);
}
}Strukturelle Gleichheit
Value Objects in PHP haben keinen Identifikator. Anders gesagt: Zwei Value Objects mit denselben internen Werten müssen als gleich gelten. Da PHP den Gleichheitsoperator nicht überladen kann, implementierst du den Vergleich selbst.
Eine eigene Methode erledigt das:
final class Price
{
// ...
public function isEqualsTo(Price $price): bool
{
return $this->amount === $price->amount &&
$this->currency === $price->currency;
}
}Selbstvalidierung
Ein Value Object validiert sich bei seiner Erstellung. Ist eine seiner Eigenschaften ungültig, wird eine Exception geworfen. Zusammen mit der Unveränderlichkeit heißt das: Einmal erzeugt, ist das Objekt für immer gültig.
Zurück zum Typ Preis: Ein negativer Betrag ergibt in dieser Domäne keinen Sinn.
final class Price
{
// ...
public function __construct(float $amount, string $currency = 'USD')
{
if ($amount < 0) {
throw new InvalidArgumentException("Le montant doit être une valeur positive: {$amount}.");
}
if (!in_array($currency, $this->getAvailableCurrencies())) {
throw new InvalidArgumentException("La devise doit être valide: {$currency}.");
}
$this->amount = $amount;
$this->currency = $currency;
}
}Fazit
Value Objects helfen dir, sauberen Code zu schreiben. Statt so zu schreiben:
public function addPhoneNumber(string $phone) : void {}schreibst du so:
public function addPhoneNumber(string $phone) : void {}Das liest sich leichter und lässt sich leichter durchdenken, und du musst nicht mehr herausfinden, welches Telefonformat gerade gilt. Da ihre Attribute sie definieren und du sie über verschiedene Entitäten hinweg teilen kannst, lassen sie sich beliebig lange cachen.
Außerdem reduzieren sie Dopplungen. Statt mehrerer Felder für Betrag und Währung nimmst du eine einzige Preis-Klasse.
Wie alles im Leben lassen sich Value Objects in PHP natürlich auch übertreiben. Stell dir vor, du wandelst haufenweise Value Objects um, um sie in der Datenbank abzulegen, und wandelst sie beim Lesen wieder zurück: Performance-Probleme sind vorprogrammiert. Und eine Codebasis voller Value Objects wird schwerfällig.
Nimm sie für ein Feld oder eine Gruppe von Feldern deiner Domäne, die eine Validierung brauchen, oder um eine Mehrdeutigkeit aufzulösen – Telefonformate sind der klassische Fall.


