← Zurück zu den Artikeln
PHP-Architektur & Design Patterns

Value Objects in PHP: Hören Sie auf, primitivenbesessenen Code zu schreiben

Erfahren Sie, wie Value Objects die Primitivenbesessenheit in PHP beseitigen – mit realen Beispielen von Email- und Money-Klassen, die Typsicherheit, Validierung und Klarheit in Ihren Domänencode bringen.

Verfügbare Sprachen
Value Objects in PHP: Hören Sie auf, primitivenbesessenen Code zu schreiben
Ausgewählter Artikel ↗

Value Objects in PHP: Hören Sie auf, primitivenbesessenen Code zu schreiben

Betrachten Sie fast jede PHP-Codebasis, und Sie werden dasselbe Muster finden: E-Mails als Strings, Geld als Floats, IDs als Ganzzahlen, Telefonnummern als Strings. Dies nennt man Primitive Obsession (Primitivenbesessenheit) — die übermäßige Verwendung primitiver Typen zur Darstellung von Domänenkonzepten. Es ist eines der häufigsten und schädlichsten Anti-Patterns in der modernen PHP-Entwicklung.

Value Objects sind das Gegenmittel. Sie bringen Typsicherheit, Validierung und Klarheit in Ihren Code — und sobald Sie anfangen, sie zu verwenden, werden Sie sich fragen, wie Sie jemals ohne sie leben konnten.

Was ist ein Value Object?

Ein Value Object (VO, Wertobjekt) ist ein kleines, unveränderliches Objekt, das ein Konzept aus Ihrer Domäne repräsentiert. Im Gegensatz zu einer Entity hat ein Value Object keine Identität — es wird vollständig durch seine Werte definiert. Zwei Value Objects mit denselben Werten gelten als gleich.

Klassische Beispiele sind:

  • Email — statt eines rohen Strings
  • Money — statt eines Floats plus Währungsstring
  • UserId — statt einer Ganzzahl
  • PhoneNumber — statt eines Strings
  • DateRange — statt zweier DateTime-Objekte
  • Address — statt eines Arrays von Strings
  • Password — statt eines einfachen Strings

Das Problem mit Primitiven

Betrachten Sie diesen typischen Code:

<?php function registerUser(string $email, float $balance): void { // Ist $email gültig? Wer weiß. // Ist $balance in USD, EUR oder BTC? Keine Ahnung. // Kann $balance negativ sein? Vielleicht. }

Jeder primitive Wert trägt verborgene Annahmen. Das Typsystem sagt Ihnen nichts darüber:

  • Ob der Wert gültig ist
  • In welchem Format er vorliegt
  • Welche Operationen erlaubt sind
  • Welche Einheiten oder Währung er repräsentiert
  • Ob er sicher geändert oder wiederverwendet werden kann

Das Ergebnis? Validierungslogik überall verstreut, Fehler durch nicht übereinstimmende Formate und Code, über den schwer nachzudenken ist.

Ihr erstes Value Object: Email

Ersetzen wir einen primitiven String durch ein ordentliches Value Object:

<?php declare(strict_types=1); final class Email { private function __construct( private readonly string $value, ) { } public static function fromString(string $value): self { $normalized = mb_strtolower(trim($value)); if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( sprintf('"%s" is not a valid email address.', $value) ); } return new self($normalized); } public function toString(): string { return $this->value; } public function equals(self $other): bool { return $this->value === $other->value; } public function __toString(): string { return $this->value; } }

Jetzt schützt Sie das Typsystem. Wenn Sie ein Email-Objekt haben, wissen Sie, dass es gültig ist. Keine verstreuten Validierungsprüfungen mehr.

Verwendung des Value Objects

<?php final class User { public function __construct( public readonly UserId $id, public readonly Email $email, public readonly string $name, ) { } } // Jetzt erzwingt der Konstruktor die Gültigkeit: $user = new User( id: UserId::fromInt(42), email: Email::fromString('John@Example.com'), name: 'John Doe', ); echo $user->email->toString(); // "john@example.com" (normalisiert)

Ein komplexeres Beispiel: Money

Geld ist der klassische Fall, in dem Primitive katastrophal versagen. Floats verlieren Präzision, und das Mischen von Währungen ist ein stiller Fehler, der nur darauf wartet, zu passieren.

<?php declare(strict_types=1); final class Money { private function __construct( private readonly int $amountInCents, private readonly string $currency, ) { if ($amountInCents < 0) { throw new InvalidArgumentException('Amount cannot be negative.'); } if (!preg_match('/^[A-Z]{3}$/', $currency)) { throw new InvalidArgumentException('Currency must be a 3-letter ISO code.'); } } public static function of(int $amountInCents, string $currency): self { return new self($amountInCents, $currency); } public static function fromFloat(float $amount, string $currency): self { return new self((int) round($amount * 100), $currency); } public function add(self $other): self { $this->assertSameCurrency($other); return new self( $this->amountInCents + $other->amountInCents, $this->currency, ); } public function subtract(self $other): self { $this->assertSameCurrency($other); return new self( $this->amountInCents - $other->amountInCents, $this->currency, ); } public function isGreaterThan(self $other): bool { $this->assertSameCurrency($other); return $this->amountInCents > $other->amountInCents; } public function amount(): int { return $this->amountInCents; } public function currency(): string { return $this->currency; } public function format(): string { return number_format($this->amountInCents / 100, 2) . ' ' . $this->currency; } public function equals(self $other): bool { return $this->amountInCents === $other->amountInCents && $this->currency === $other->currency; } private function assertSameCurrency(self $other): void { if ($this->currency !== $other->currency) { throw new InvalidArgumentException( sprintf('Cannot operate on %s and %s.', $this->currency, $other->currency) ); } } }

Schauen Sie nun, wie sauber der aufrufende Code wird:

<?php $price = Money::fromFloat(19.99, 'USD'); $tax = Money::fromFloat(1.60, 'USD'); $total = $price->add($tax); echo $total->format(); // "21.59 USD" // Dies wirft eine Ausnahme, anstatt Daten stillschweigend zu beschädigen: $price->add(Money::of(500, 'EUR'));

Schlüsselmerkmale eines guten Value Objects

  • Unveränderlich — einmal erstellt, ändert es sich nie. Operationen geben neue Instanzen zurück.
  • Selbstvalidierend — eine Instanz kann nicht in einem ungültigen Zustand existieren.
  • Keine Identität — Gleichheit basiert auf Werten, nicht auf einer Datenbank-ID.
  • Frei von Nebenwirkungen — keine Datenbankaufrufe, kein Logging, kein globaler Zustand.
  • Klein — repräsentiert ein Konzept, nicht ein ganzes Aggregat.
  • Ersetzbar — das gesamte Objekt kann ausgetauscht werden, nicht Feld für Feld mutiert.

Value Object vs. DTO vs. Entity

Es ist wichtig, diese drei Muster zu unterscheiden:

  • Value Object — unveränderlich, durch Werte definiert, enthält Verhalten im Zusammenhang mit diesen Werten (wie add() bei Money).
  • DTO — unveränderlich oder veränderlich, transportiert Daten zwischen Schichten, enthält keine Geschäftslogik.
  • Entity — hat eine eindeutige Identität, die über die Zeit bestehen bleibt, auch wenn sich ihre Attribute ändern.

Wo Value Objects glänzen

  • Domänenmodelle — Darstellung von E-Mails, Geld, IDs, Daten, Adressen.
  • Eingabevalidierung — das Erstellen eines VO aus Benutzereingaben garantiert Gültigkeit überall stromabwärts.
  • Geschäftsregeln — Kapselung von Logik wie Währungsabgleich oder Datumsbereichsprüfungen.
  • Tests — einfachere, schnellere und fokussiertere Tests ohne Datenbankabhängigkeiten.
  • Refactoring — wenn sich Anforderungen ändern, ändern Sie das VO an einer Stelle.

Häufige Fallstricke, die es zu vermeiden gilt

  • Setter hinzufügen — dies bricht die Unveränderlichkeit. Wenn Sie einen anderen Wert benötigen, erstellen Sie eine neue Instanz.
  • Sie zu groß machen — ein VO sollte ein Konzept repräsentieren, nicht eine ganze Aggregatwurzel.
  • Persistenzlogik hinzufügen — halten Sie Datenbankbelange in Repositories, nicht in VOs.
  • Gleichheit ignorieren — implementieren Sie immer eine equals()-Methode.
  • Generische Ausnahmen werfen — definieren Sie domänenspezifische Ausnahmen für eine klarere Fehlerbehandlung.

Ein praktischer Refactoring-Pfad

Sie müssen nicht Ihre gesamte Anwendung über Nacht neu schreiben. Beginnen Sie hier:

  1. Identifizieren Sie ein primitives Feld, das Fehler oder Verwirrung verursacht (E-Mails, Geld, IDs).
  2. Erstellen Sie ein Value Object dafür mit Validierung und nützlichen Methoden.
  3. Ersetzen Sie das Primitiv in einer Klasse oder einem Modul.
  4. Lassen Sie den Compiler und Ihre Tests den Rest leiten.
  5. Wiederholen Sie dies mit dem nächsten offensichtlichen Kandidaten.

Fazit

Value Objects sind eines der wirkungsvollsten Refactorings, die Sie in einer PHP-Codebasis vornehmen können. Sie beseitigen Primitive Obsession, zentralisieren die Validierung, machen ungültige Zustände unmöglich darstellbar und verwandeln Ihren Code in einen selbst dokumentierenden Ausdruck Ihrer Domäne. Das Typsystem wird zum Verbündeten statt zur Formalität.

Beginnen Sie mit einem einzigen Value Object — vielleicht Email oder Money. Sobald Sie sehen, wie viel Klarheit es in Ihren Code bringt, werden Sie nie wieder rohe Strings herumreichen.

Hören Sie auf, primitivenbesessenen Code zu schreiben. Lassen Sie Ihre Typen für Ihre Domäne sprechen.

Technologien & Themen

Artikel-Tags

Keine Artikel entsprechen diesen Filtern.

Haben Sie ein Projekt oder eine Idee zu besprechen?

Lassen Sie uns sprechen ↗