Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Ulid
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
7 / 7
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 from
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 equals
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromBase32
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 toBase32
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
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 Phexium\Domain\Id;
11
12use InvalidArgumentException;
13use LogicException;
14use Override;
15use Ramsey\Uuid\Uuid;
16use Ramsey\Uuid\UuidInterface;
17use Stringable;
18use Tuupola\Base32;
19
20final readonly class Ulid implements Stringable, IdInterface
21{
22    private function __construct(private UuidInterface $uuid) {}
23
24    #[Override]
25    public function __toString(): string
26    {
27        return $this->toBase32();
28    }
29
30    #[Override]
31    public static function from(int|string $id): self
32    {
33        if (is_int($id)) {
34            throw new LogicException('Ulid cannot be created from an integer. Use string ULIDs instead.');
35        }
36
37        if (preg_match('|^['.Base32::CROCKFORD.'ILO]{26}$|', $id)) {
38            return self::fromBase32($id);
39        }
40
41        if (!Uuid::isValid($id)) {
42            throw new InvalidArgumentException(sprintf('Invalid ULID: %s', $id));
43        }
44
45        return new self(Uuid::fromString($id));
46    }
47
48    #[Override]
49    public function getValue(): string
50    {
51        return $this->toBase32();
52    }
53
54    #[Override]
55    public function equals(IdInterface $other): bool
56    {
57        return $this->getValue() === $other->getValue();
58    }
59
60    private static function fromBase32(string $ulid): self
61    {
62        $crockford = new Base32([
63            'characters' => Base32::CROCKFORD,
64            'crockford' => true,
65        ]);
66
67        $paddedUlid = str_pad($ulid, 32, '0', STR_PAD_LEFT);
68        $bytes = $crockford->decode($paddedUlid);
69        $uuidBytes = substr($bytes, 4);
70        $uuid = Uuid::fromBytes($uuidBytes);
71
72        return new self($uuid);
73    }
74
75    private function toBase32(): string
76    {
77        $crockford = new Base32([
78            'characters' => Base32::CROCKFORD,
79        ]);
80
81        $bytes = str_pad($this->uuid->getBytes(), 20, "\x00", STR_PAD_LEFT);
82        $encoded = $crockford->encode($bytes);
83
84        return substr($encoded, 6);
85    }
86}