Ein Wertobjekt einführen

Innerhalb der Europäischen Union verwenden wir IBANs, um Banken eindeutig zu identifizieren. Eine IBAN ist sozusagen der unique identifier einer Bank, im Zusammenhang mit Datenbanken würden wir vielleicht von einem Primary Key sprechen.

Viele Entwickler neigen dazu, eine IBAN als eine Entität anzusehen, weil sie hat ja eine Identität. Gerne wird hier Eric Evans zitiert, der im blauen Buch sinngemäß schrieb: "eine Entität ist ein Objekt mit einer eindeutigen Identität". Das ist aus technischer Sicht schon richtig, würde aber - konsequent missverstanden - bedeuten, dass letztlich alles, was aus einer Datenbank geladen wird, eine Entität wäre. Vielleicht ist das der Grund, warum objektrelationale Mapper (ORMs) von vielen als unverzichtbar angesehen werden.

Im Zweifel ist es ein Wertobjekt und keine Entität.

Bauen wir uns also ein Wertobjekt, um eine IBAN zu repräsentieren. Dieses Objekt ist ein sehr guter Ort, um alle Validierungen (oder Plausibilitätsprüfungen) an einer Stelle im Code gut auffindbar zu haben. Was liegt schließlich näher, als in die Klasse IBAN zu gucken, um nachzusehen, welche Regeln für IBANs gelten?

Erst mal brauchen wir einen Konstruktor, dem wir einen "IBAN-Kandidaten" übergeben. Dort rufen wir eine ensure-Methode auf, die entweder eine Exception wirft, wenn etwas mit der IBAN nicht passt, oder für immer schweigt (Rückgabewert void), wenn sie nichts zu meckern hat:

public function __construct(string $iban)
{
    $this->ensureIsValid($iban);

    $this->iban = $iban;
}
IBAN.php

Da wir uns in diesem Beispiel nicht dem echten IBAN-Frickelkram (welche Länder, welches Format haben die, ...) aufhalten wollen, schreiben wir einfach ganz faul nur ein bißchen Code zur Prüfung, ob es sich um eine deutsche IBAN handelt. So viel Patriotismus muss ein, ich komme schließlich aus Bayern.

private function ensureIsValid(string $iban): void
{
    if (!str_starts_with($iban, 'DE')) {
        throw new IBANException('Unsupported IBAN');
    }
}
IBAN.php

Die schlechte Nachricht ist, dass das in der Realität natürlich nie ausreichen würde; die gute Nachricht aber ist, dass weitere Prüfungen ganz einfach als neue if-Blöcke unten angehängt werden, idealerweise immer dann, wenn wir zuvor einen fehlschlagenden Test geschrieben haben.

Zu guter Letzt geben wir unserem Objekt noch die Möglichkeit, die IBAN wieder zurück in einen String zu konvertieren:

public function asString(): string
{
    return $this->iban;
}
IBAN.php

Ich hätte hier auch __toString() verwenden können, das mag ich allerdings nicht so gerne, weil es eigentlich für eher Debug-Ausgaben gedacht ist, beispielsweise wenn wir das Objekt in einer Exception-Message oder einem Logeintrag serialisieren wollen.

Da nicht alle Objekte auf Strings basieren, ist eine Methode asString() für mich eher eine Typumwandlung. Genauso könnte ich je nach Objekt stattdessen Methoden wir asInt(), asFloat() oder asArray() haben.

Legen wir los und bauen uns eine gültige IBAN. Naja, was halt unser Beispielprogramm so alles als gültig durchgehen lässt:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$iban = new IBAN('DE...');
var_dump($iban->asString());
valid-iban.php

Das funktioniert:

valid-iban.php:6:
string(5) "DE..."
Ausgabe von valid-iban.php

Wenn wir dagegen einen ungültigen String übergeben ...

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$iban = new IBAN('invalid-iban');
invalid-iban-will-fail.php

... werden wir mit einer Exception bestraft:

PHP Fatal error:  Uncaught IBANException: Unsupported IBAN in IBAN.php:22
Stack trace:
#0 IBAN.php(9): IBAN->ensureIsValid()
#1 invalid-iban-will-fail.php(5): IBAN->__construct()
#2 {main}
  thrown in IBAN.php on line 22
Ausgabe von invalid-iban-will-fail.php

Jetzt ist es an der Zeit, über den Begriff "validieren" zu reden.