View code
Validating Value Objects using Enums in PHP
In this tutorial, we talked about Value Objects. One of the benefits of value objects is that they can be self-validated, this is good because, in the case of a currency, a currency should be aware of the supported ISO codes.
This time, I want to expand a bit more on the validation part and make use of PHP enums to build even better code. In the previous tutorial, we ended up with the following code:
class Currency{ public function __construct(private readonly string $isoCode) { if (!in_array($isoCode, ['EUR', 'USD'])) { throw new InvalidArgumentException('Invalid ISO code'); } } public function isoCode(): string { return $this->isoCode; } public function equals(Currency $currency): bool { return $this->isoCode() === $currency->isoCode(); } public static function make(string $isoCode): Currency { return new self($isoCode); }}
That is a simple Value Object class that represents a currency. This time, we will refactor that class and make it easier to maintain and extend in the future.
The problem
To be honest, this class is totally fine and there is nothing wrong with it, however, we are developers and as developers, we tend to create problems when they don't exist 😅.
Suppose we want to support another currency, GBP, what will the change be like? Simple, we will update the if statement to include GBP.
if (!in_array($isoCode, ['EUR', 'USD', 'GBP'])) { throw new InvalidArgumentException('Invalid ISO code');}
Cool, done!
…
No, wait! I don’t like that. Those are ISO codes that represent currencies, we sure must need them somewhere else in our application and we know the best place to add them.
Say hi to IsoCodeEnum
enum IsoCodeEnum: string{ case EUR = 'EUR'; case USD = 'USD'; case GBP = 'GBP';}
By using an enum to represent ISO codes we help with encapsulation, not only that, the IsoCodeEnum might be used somewhere else in the application, helping with code reusability.
With the introduction of IsoCodeEnum, we have the following change:
class Currency{ public function __construct(private readonly IsoCodeEnum $isoCode) { } public function isoCode(): IsoCodeEnum { return $this->isoCode; } public function equals(Currency $currency): bool { return $this->isoCode() === $currency->isoCode(); } public static function make(IsoCodeEnum $isoCode): Currency { return new self($isoCode); }}
As you can see, the validation inside the constructor has been removed. A Currency value object requires an IsoCodeEnum from now on, as a result, the validation is now integrated into the language itself. It is impossible to pass an invalid IsoCodeEnum as an argument.
Even though the responsibility of validating an ISO code is passed down to PHP because of the enum, Currency is still self-validating the ISO code, that is because the enum is a dependency of the Currency.
Using the refactored Currency Value Object
$currencyInEur = new Currency(IsoCodeEnum::EUR);echo $currencyInEur->isoCode()->value; // EUR$currencyInUsd = new Currency(IsoCodeEnum::USD);echo $currencyInUsd->isoCode()->value; // USD
Thanks to IsoCodeEnum, the developer cannot misspell the ISO code anymore, something that could have been possible before.
Because of IsoCodeEnum we cannot replicate the case where the dev would enter an invalid ISO code as we had before with the:
new Currency('GBP'); // will throw an InvalidArgumentException