Introducing a value object

Within the European Union, we use IBANs to uniquely identify banks. An IBAN is, so to speak, the unique identifier of a bank; in the context of databases, we would perhaps speak of a primary key.

Many developers tend to regard an IBAN as an entity, because it has an identity. Eric Evans is often quoted here, who wrote in the blue book: "an entity is an object with a unique identity". This is correct from a technical point of view, but - if consistently misunderstood - would mean that ultimately everything that is loaded from a database would be an entity. Perhaps this is the reason why object-relational mappers (ORMs) are considered indispensable by many.

When in doubt, it is a value object and not an entity.

So let's build a value object to represent an IBAN. This object is a very good place to have all validations (or plausibility checks) easily findable in one place in the code. After all, what could be more obvious than looking in the IBAN class to see which rules apply to IBANs?

First of all, we need a constructor to which we pass an "IBAN candidate". There we call a ensure method that either throws an exception if something is wrong with the IBAN, or remains silent forever (return value void) if it has nothing to complain about:

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

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

Since we don't want to get into the real IBAN fiddly stuff in this example (which countries, what format do they have, ...), we'll just lazily write a bit of code to check whether it's a German IBAN. So much patriotism is a must, I'm from Bavaria after all.

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

The bad news is that this would of course never be enough in reality; the good news, however, is that further checks are simply appended as new if blocks at the bottom, ideally whenever we have previously written a failing test.

Finally, we give our object the ability to convert the IBAN back into a string:

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

I could also have used __toString() here, but I don't like it so much because it is actually intended for debug output, for example if we want to serialize the object in an exception message or a log entry.

Since not all objects are based on strings, a method asString() is more of a type conversion for me. Similarly, depending on the object, I could have methods like asInt(), asFloat() or asArray() instead.

Let's go ahead and build a valid IBAN. Well, whatever our example program lets pass as valid:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

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

That works:

valid-iban.php:6:
string(5) "DE..."
Output of valid-iban.php
Execute valid-iban.php

If, on the other hand, we pass an invalid string ...

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

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

... we are penalized with an exception:

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
Output of invalid-iban-will-fail.php
Execute invalid-iban-will-fail.php

Now it's time to talk about the term "validate".