An object for each state

What if we represented each state with its own object? Incidentally, this is the idea behind the state pattern. And it is well compatible with the immutability we need for value objects. So, let's get started.

We rename our value object IBAN to UnvalidatedIBAN because we have not yet checked whether the corresponding bank account even exists. You may notice that we are using the term "unvalidated" here in its technical sense: an account number for which we do not know whether the account exists. This no longer really has anything to do with the "validating data" that we like to talk about in everyday life.

Are you confused by all these nonsense words? Good, because this is the first step on the way to understanding it properly!

In addition, we have created ValidatedIBAN, which we use to represent an existing bank account:

<?php declare(strict_types=1);

class ValidatedIBAN
{
    private UnvalidatedIBAN $iban;

    public function __construct(UnvalidatedIBAN $iban)
    {
        $this->iban = $iban;
    }

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

Here we do not need to check anything (syntactically) in the constructor, because our UnvalidatedIBAN has already done this. Repeating the same guard clause here would be unwanted code duplication.

So how do we get from an unvalidated IBAN to a validated IBAN? Quite simply: our UnvalidatedIBAN object generates the ValidatedIBAN as the result of the actual validation, which we have deliberately omitted here for the time being:

public function validate(): ValidatedIBAN
{
    return new ValidatedIBAN($this);
}
UnvalidatedIBAN.php

Depending on the situation, we can now declare one of the two classes UnvalidatedIBAN or ValidatedIBAN as the type. This has the great advantage that we don't mess anything up and can transfer money to Nirvana, for example, because our transfer method only accepts a ValidatedIBAN. The fact that I as a developer could also "falsify" this naturally remains a residual problem. But this should at least be noticeable in a code review.

However, there may be cases in which it is not important whether we work with a validated or non-validated IBAN. This is often the case when we want to display data. In this case, we need to create a commonality between the two classes. For this we would use a common interface IBAN.

We haven't paid any attention to how the actual validation of the IBAN (in the sense of: does the account exist) works. This is not a mistake, because this is a business process that takes place outside of our value objects. We will receive the result of this process at some point from an external source, for example via an event or a command. We then call the validate() method in the UnvalidatedIBAN object for processing.

To summarize: in contrast to "validation of data", we are dealing here with validation as a business process. Therefore, the logic is not found in the value object itself.

In domain-driven design, we would note here that a software developer understands "validate IBAN" to mean something completely different than, for example, an employee in the finance department who is responsible for bank transfers. I would describe the first as "technical validation" and the second as "business validation". The latter always takes place outside our value objects.

An example of "technical validation" is sending a confirmation email to a user. We all know it: "click on this link to confirm your e-mail address". This is a process that takes place outside our software, which is asynchronous, and the result of which arrives back in our software in the form of a GET request.

The European VAT ID is another example of technical vs. functional validation: I can decide relatively easily in the code whether a VAT ID looks valid by programming the rules of the individual countries for it. These rules are very well documented.

In order to decide whether VAT must be shown on an invoice or not(reverse charge procedure), we are obliged to explicitly check VAT IDs. The European Commission has its own online service for this purpose. After successful validation, this service returns a code that we should remember for verification purposes, for example by requiring it as the second constructor parameter of ValidatedVatId.

In this way, the result of the technical validation is represented as additional information in our application. Incidentally, we could do the same with bank transfers by transferring the cent with a code in the subject line, which the customer then has to enter back into our application. We can then assume that only the person who has access to the bank account (or the account statements) can see this code from the subject of the transfer.

Incidentally, we could have a lot of fun with temporality in relation to VAT IDs. What about the validity of a British VAT ID, for example?