• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nicebooks-com / isbn / 21119739852

18 Jan 2026 10:32PM UTC coverage: 95.775% (-3.2%) from 98.985%
21119739852

push

github

BenMorel
Update README

204 of 213 relevant lines covered (95.77%)

36.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.09
/src/Isbn.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Nicebooks\Isbn;
6

7
use Nicebooks\Isbn\Exception\IsbnException;
8
use Nicebooks\Isbn\Internal\RangeService;
9
use Override;
10
use Stringable;
11

12
/**
13
 * Represents an immutable ISBN number.
14
 *
15
 * Instances of this class always have valid digits, length, and check digit. This basic validation does not depend on
16
 * the version of the ISBN International data file used by this version of nicebooks/isbn.
17
 *
18
 * Full validation, which does depend on the data file version, can be performed with `isValid()`.
19
 */
20
abstract readonly class Isbn implements Stringable
21
{
22
    protected string $isbn;
23

24
    private ?RegistrationGroup $registrationGroup;
25

26
    /**
27
     * @var list<string>|null
28
     */
29
    private ?array $parts;
30

31
    /**
32
     * @param string $isbn The unformatted ISBN number, validated.
33
     */
34
    private function __construct(string $isbn)
35
    {
36
        $this->isbn = $isbn;
69✔
37
        $rangeInfo = RangeService::getRangeInfo($isbn);
69✔
38

39
        $this->registrationGroup = $rangeInfo?->registrationGroup;
69✔
40
        $this->parts = $rangeInfo?->parts;
69✔
41
    }
42

43
    /**
44
     * Proxy method to create Isbn10 instances from within the Isbn13 class.
45
     */
46
    final protected function newIsbn10(string $isbn): Isbn10
47
    {
48
        return new Isbn10($isbn);
10✔
49
    }
50

51
    /**
52
     * Proxy method to create Isbn13 instances from within the Isbn10 class.
53
     */
54
    final protected function newIsbn13(string $isbn): Isbn13
55
    {
56
        return new Isbn13($isbn);
18✔
57
    }
58

59
    /**
60
     * @throws Exception\InvalidIsbnException If the ISBN is not valid.
61
     * @throws Exception\IsbnNotConvertibleException If called on the Isbn10 class, and an ISBN-13 is passed.
62
     */
63
    public static function of(string $isbn): Isbn
64
    {
65
        if (preg_match(Internal\Regexp::ASCII, $isbn) === 0) {
77✔
66
            throw Exception\InvalidIsbnException::forIsbn($isbn);
1✔
67
        }
68

69
        $isbn = preg_replace(Internal\Regexp::NON_ALNUM, '', $isbn);
76✔
70
        assert($isbn !== null);
71

72
        if (preg_match(Internal\Regexp::ISBN13, $isbn) === 1) {
76✔
73
            if (!Internal\CheckDigit::validateCheckDigit13($isbn)) {
45✔
74
                throw Exception\InvalidIsbnException::forIsbn($isbn);
2✔
75
            }
76

77
            return new Isbn13($isbn);
43✔
78
        }
79

80
        $isbn = strtoupper($isbn);
39✔
81

82
        if (preg_match(Internal\Regexp::ISBN10, $isbn) === 1) {
39✔
83
            if (!Internal\CheckDigit::validateCheckDigit10($isbn)) {
35✔
84
                throw Exception\InvalidIsbnException::forIsbn($isbn);
1✔
85
            }
86

87
            return new Isbn10($isbn);
34✔
88
        }
89

90
        throw Exception\InvalidIsbnException::forIsbn($isbn);
4✔
91
    }
92

93
    abstract public function is10(): bool;
94

95
    abstract public function is13(): bool;
96

97
    abstract public function isConvertibleTo10(): bool;
98

99
    /**
100
     * Returns a copy of this Isbn, converted to ISBN-10.
101
     *
102
     * @throws Exception\IsbnNotConvertibleException If this is an ISBN-13 not starting with 978.
103
     */
104
    abstract public function to10(): Isbn10;
105

106
    /**
107
     * Returns a copy of this Isbn, converted to ISBN-13.
108
     */
109
    abstract public function to13(): Isbn13;
110

111
    /**
112
     * Returns whether this ISBN belongs to a known group.
113
     *
114
     * This method only validates the group. For a full group and range validation, use isValid().
115
     *
116
     * When this method returns true, the following methods do not throw an exception:
117
     *
118
     * - getGroupIdentifier()
119
     * - getGroupName()
120
     *
121
     * @deprecated use hasValidRegistrationGroup() instead.
122
     */
123
    final public function isValidGroup(): bool
124
    {
125
        return $this->registrationGroup !== null;
6✔
126
    }
127

128
    /**
129
     * Returns whether this ISBN is in a recognized group and range.
130
     *
131
     * @deprecated Use isValid() instead.
132
     */
133
    final public function isValidRange(): bool
134
    {
135
        return $this->parts !== null;
6✔
136
    }
137

138
    /**
139
     * Returns whether this ISBN belongs to a known group.
140
     *
141
     * This method only validates the group. For a full group and range validation, use isValid().
142
     *
143
     * When this method returns true, the following methods do not throw an exception:
144
     *
145
     * - getRegistrationGroup()
146
     * - getGroupIdentifier()
147
     * - getGroupName()
148
     */
149
    final public function hasValidRegistrationGroup(): bool
150
    {
151
        return $this->registrationGroup !== null;
×
152
    }
153

154
    /**
155
     * Returns whether this ISBN belongs to a known registration group and range.
156
     *
157
     * If this method returns true, we are able to split the ISBN into parts and format it with hyphens.
158
     * If it returns false, the ISBN number is not formattable; it means that either the ISBN number is invalid, or that
159
     * this version of the library is compiled against an outdated data file from ISBN International.
160
     *
161
     * Note that this method returning true only means that the ISBN number is potentially valid, but does not indicate
162
     * in any way whether the ISBN number has been assigned to a book yet.
163
     *
164
     * This is the highest level of validation that can be performed by looking at the ISBN number alone.
165
     *
166
     * When this method returns true, toFormattedString() returns a hyphenated result, and the following methods do not
167
     * throw an exception:
168
     *
169
     * - getRegistrationGroup()
170
     * - getGroupIdentifier()
171
     * - getGroupName()
172
     * - getPublisherIdentifier()
173
     * - getTitleIdentifier()
174
     * - getParts()
175
     */
176
    final public function isValid(): bool
177
    {
178
        return $this->parts !== null;
×
179
    }
180

181
    /**
182
     * @throws IsbnException If this ISBN is not in a recognized group.
183
     */
184
    final public function getRegistrationGroup(): RegistrationGroup
185
    {
186
        if ($this->registrationGroup === null) {
×
187
            throw IsbnException::unknownGroup($this->isbn);
×
188
        }
189

190
        return $this->registrationGroup;
×
191
    }
192

193
    /**
194
     * Returns the group identifier which identifies a country, geographic region, or language area.
195
     *
196
     * Example for ISBN-10: "1-338-87893-X" => "1"
197
     * Example for ISBN-13: "978-1-338-87893-6" => "978-1"
198
     *
199
     * @throws IsbnException If this ISBN is not in a recognized group.
200
     *
201
     * @deprecated Use getRegistrationGroup()->prefix, ->identifier, and ->toString() instead.
202
     */
203
    final public function getGroupIdentifier(): string
204
    {
205
        if ($this->registrationGroup === null) {
25✔
206
            throw IsbnException::unknownGroup($this->isbn);
2✔
207
        }
208

209
        return $this->is10() ? $this->registrationGroup->identifier : $this->registrationGroup->toString();
23✔
210
    }
211

212
    /**
213
     * Returns the English name of the country, geographic region, or language area that matches the group identifier.
214
     *
215
     * Examples: "English Language", "French language", "Japan", "Spain".
216
     *
217
     * @throws IsbnException If this ISBN is not in a recognized group.
218
     *
219
     * @deprecated Use getRegistrationGroup()->name instead.
220
     */
221
    final public function getGroupName(): string
222
    {
223
        if ($this->registrationGroup === null) {
25✔
224
            throw IsbnException::unknownGroup($this->isbn);
2✔
225
        }
226

227
        return $this->registrationGroup->name;
23✔
228
    }
229

230
    /**
231
     * Returns the publisher identifier.
232
     *
233
     * The publisher identifier identifies a particular publisher within a group.
234
     *
235
     * Example for ISBN-10: "1-338-87893-X" => "338"
236
     * Example for ISBN-13: "978-1-338-87893-6" => "338"
237
     *
238
     * @throws IsbnException If this ISBN is not in a recognized group or range.
239
     */
240
    final public function getPublisherIdentifier(): string
241
    {
242
        if ($this->registrationGroup === null) {
27✔
243
            throw IsbnException::unknownGroup($this->isbn);
2✔
244
        }
245

246
        if ($this->parts === null) {
25✔
247
            throw IsbnException::unknownRange($this->isbn);
2✔
248
        }
249

250
        return $this->parts[$this->is13() ? 2 : 1];
23✔
251
    }
252

253
    /**
254
     * Returns the title identifier.
255
     *
256
     * The title identifier identifies a particular title or edition of a title.
257
     *
258
     * Example for ISBN-10: "1-338-87893-X" => "87893"
259
     * Example for ISBN-13: "978-1-338-87893-6" => "87893"
260
     *
261
     * @throws IsbnException If this ISBN is not in a recognized group or range.
262
     */
263
    final public function getTitleIdentifier(): string
264
    {
265
        if ($this->registrationGroup === null) {
27✔
266
            throw IsbnException::unknownGroup($this->isbn);
2✔
267
        }
268

269
        if ($this->parts === null) {
25✔
270
            throw IsbnException::unknownRange($this->isbn);
2✔
271
        }
272

273
        return $this->parts[$this->is13() ? 3 : 2];
23✔
274
    }
275

276
    /**
277
     * Returns the check digit.
278
     *
279
     * Example for ISBN-10: "1-338-87893-X" => "X"
280
     * Example for ISBN-13: "978-1-338-87893-6" => "6"
281
     *
282
     * The check digit is the single digit at the end of the ISBN which validates the ISBN.
283
     */
284
    final public function getCheckDigit(): string
285
    {
286
        return $this->isbn[-1];
23✔
287
    }
288

289
    /**
290
     * Returns the parts that constitute this ISBN number, as an array of strings.
291
     *
292
     * ISBN-10 have 4 parts, ISBN-13 have 5 parts.
293
     *
294
     * Example for ISBN-10: "1-338-87893-X" => ["1", "338", "87893", "X"]
295
     * Example for ISBN-13: "978-1-338-87893-6" => ["978", "1", "338", "87893", "6"]
296
     *
297
     * @return list<string>
298
     *
299
     * @throws IsbnException If this ISBN is not in a recognized group or range.
300
     */
301
    final public function getParts(): array
302
    {
303
        if ($this->registrationGroup === null) {
27✔
304
            throw IsbnException::unknownGroup($this->isbn);
2✔
305
        }
306

307
        if ($this->parts === null) {
25✔
308
            throw IsbnException::unknownRange($this->isbn);
2✔
309
        }
310

311
        return $this->parts;
23✔
312
    }
313

314
    /**
315
     * Returns the formatted (hyphenated) ISBN number.
316
     *
317
     * If the ISBN number is not in a recognized range, it is returned unformatted.
318
     *
319
     * @deprecated Use toFormattedString() instead.
320
     */
321
    final public function format(): string
322
    {
323
        return $this->toFormattedString();
25✔
324
    }
325

326
    /**
327
     * Checks if this ISBN is equal to another ISBN.
328
     *
329
     * An ISBN-10 is considered equal to its corresponding ISBN-13.
330
     * For example, `Isbn::of('978-0-399-16534-4')->isEqualTo(Isbn::of("0-399-16534-7"))` returns true.
331
     */
332
    final public function isEqualTo(Isbn $otherIsbn): bool
333
    {
334
        return $this->to13()->isbn === $otherIsbn->to13()->isbn;
8✔
335
    }
336

337
    /**
338
     * Returns the unformatted ISBN number.
339
     */
340
    final public function toString(): string
341
    {
342
        return $this->isbn;
×
343
    }
344

345
    /**
346
     * Returns the formatted (hyphenated) ISBN number.
347
     *
348
     * If the ISBN number is not in a recognized range, it is returned unformatted.
349
     *
350
     * Example for ISBN-10: "1-338-87893-X"
351
     * Example for ISBN-13: "978-1-338-87893-6"
352
     */
353
    final public function toFormattedString(): string
354
    {
355
        if ($this->parts === null) {
25✔
356
            return $this->isbn;
2✔
357
        }
358

359
        return implode('-', $this->parts);
23✔
360
    }
361

362
    #[Override]
363
    final public function __toString(): string
364
    {
365
        return $this->isbn;
22✔
366
    }
367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc