Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
ISBN
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
6 / 6
9
100.00% covered (success)
100.00%
1 / 1
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFormattedValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 normalize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExceptionClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateRules
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 isValidISBN13
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10namespace AppDemo\Library\Domain;
11
12use AppDemo\Library\Domain\Exception\InvalidIsbnException;
13use Assert\Assert;
14use Override;
15use Phexium\Domain\AbstractStringValueObject;
16use Phexium\Domain\ValueObjectInterface;
17
18final readonly class ISBN extends AbstractStringValueObject implements ValueObjectInterface
19{
20    #[Override]
21    public function __toString(): string
22    {
23        return $this->getFormattedValue();
24    }
25
26    public function getFormattedValue(): string
27    {
28        $value = $this->getValue();
29
30        return substr($value, 0, 3).'-'.substr($value, 3, 3).'-'.substr($value, 6, 6).'-'.substr($value, 12);
31    }
32
33    #[Override]
34    protected function normalize(string $value): string
35    {
36        return preg_replace('/\D/', '', $value);
37    }
38
39    #[Override]
40    protected static function getExceptionClass(): callable
41    {
42        return InvalidIsbnException::whenValidationFailed(...);
43    }
44
45    #[Override]
46    protected function validateRules(string $value): void
47    {
48        Assert::that($value)
49            ->notEmpty('ISBN cannot be empty')
50            ->string('ISBN must be a string')
51            ->regex('/\d{13}/', 'ISBN must be exactly 13 digits')
52        ;
53
54        if (!$this->isValidISBN13($value)) {
55            throw InvalidIsbnException::whenChecksumIsInvalid();
56        }
57    }
58
59    private function isValidISBN13(string $isbn): bool
60    {
61        $checksum = 0;
62
63        for ($i = 0; $i < 12; ++$i) {
64            $digit = $isbn[$i];
65            $checksum += ($i % 2 === 0) ? $digit : $digit * 3;
66        }
67
68        $checksumDigit = (10 - ($checksum % 10)) % 10;
69
70        return $checksumDigit === (int) $isbn[12];
71    }
72}