Lines 95.45% 21 / 22
Functions and Methods 85.71% 6 / 7
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
Currency 95.45% 21 / 22 85.71% 6 / 7 12 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 fromCode 100.00% 10 / 10 100.00% 1 / 1 4
 tryFromCode 100.00% 3 / 3 100.00% 1 / 1 2
 minorUnits 100.00% 1 / 1 100.00% 1 / 1 1
 equals 100.00% 1 / 1 100.00% 1 / 1 1
 __serialize 100.00% 1 / 1 100.00% 1 / 1 1
 __unserialize 80.00% 4 / 5 0.00% 0 / 1 2.03
1<?php
2
3declare(strict_types=1);
4
5namespace Tomaj\BankMailsParser\ValueObject;
6
7use InvalidArgumentException;
8
9/**
10 * ISO 4217 currency.
11 *
12 * Alphabetic and numeric codes from {@see Iso4217} are accepted, including the
13 * withdrawn lev (BGN, 975) and kuna (HRK, 191) so older mail can be parsed again.
14 * `978` and `036` are stored as `EUR` and `AUD`. A code that is not in that list throws.
15 * Compare values with {@see equals()}. Unserialize does not preserve object identity.
16 */
17final class Currency
18{
19    /** @var array<string, self> */
20    private static array $cache = [];
21
22    private function __construct(
23        public readonly string $value,
24    ) {}
25
26    public static function fromCode(string $code): self
27    {
28        $normalized = strtoupper(trim($code));
29        if (preg_match('/^\d{1,3}$/', $normalized) === 1) {
30            $padded = str_pad($normalized, 3, '0', STR_PAD_LEFT);
31            $alpha = array_search($padded, Iso4217::NUMERIC, true);
32            if (!is_string($alpha)) {
33                throw new InvalidArgumentException(sprintf('Unsupported currency code "%s".', $code));
34            }
35            $normalized = $alpha;
36        }
37
38        if (!isset(Iso4217::SCALE[$normalized])) {
39            throw new InvalidArgumentException(sprintf('Unsupported currency code "%s".', $code));
40        }
41
42        return self::$cache[$normalized] ??= new self($normalized);
43    }
44
45    public static function tryFromCode(string $code): ?self
46    {
47        try {
48            return self::fromCode($code);
49        } catch (InvalidArgumentException) {
50            return null;
51        }
52    }
53
54    public function minorUnits(): int
55    {
56        return Iso4217::SCALE[$this->value];
57    }
58
59    public function equals(self $other): bool
60    {
61        return $this->value === $other->value;
62    }
63
64    /**
65     * @return array{value: string}
66     */
67    public function __serialize(): array
68    {
69        return ['value' => $this->value];
70    }
71
72    /**
73     * @param array{value?: string} $data
74     */
75    public function __unserialize(array $data): void
76    {
77        $value = $data['value'] ?? '';
78        if (!isset(Iso4217::SCALE[$value])) {
79            throw new InvalidArgumentException('Invalid currency payload.');
80        }
81        $this->value = $value;
82        self::$cache[$value] ??= $this;
83    }
84}