|
| 1 | +<?php |
| 2 | + |
| 3 | +/* |
| 4 | + * This file is part of the Symfony package. |
| 5 | + * |
| 6 | + * (c) Fabien Potencier <fabien@symfony.com> |
| 7 | + * |
| 8 | + * For the full copyright and license information, please view the LICENSE |
| 9 | + * file that was distributed with this source code. |
| 10 | + */ |
| 11 | + |
| 12 | +namespace Symfony\Component\Uid; |
| 13 | + |
| 14 | +/** |
| 15 | + * @see https://github.com/ulid/spec |
| 16 | + * |
| 17 | + * @experimental in 5.1 |
| 18 | + * |
| 19 | + * @author Nicolas Grekas <p@tchwork.com> |
| 20 | + */ |
| 21 | +class Ulid implements \JsonSerializable |
| 22 | +{ |
| 23 | + private static $time = -1; |
| 24 | + private static $rand = []; |
| 25 | + |
| 26 | + private $ulid; |
| 27 | + |
| 28 | + public function __construct(string $ulid = null) |
| 29 | + { |
| 30 | + if (null === $ulid) { |
| 31 | + $this->ulid = self::generate(); |
| 32 | + |
| 33 | + return; |
| 34 | + } |
| 35 | + |
| 36 | + if (!self::isValid($ulid)) { |
| 37 | + throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid)); |
| 38 | + } |
| 39 | + |
| 40 | + $this->ulid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ'); |
| 41 | + } |
| 42 | + |
| 43 | + public static function isValid(string $ulid): bool |
| 44 | + { |
| 45 | + if (26 !== \strlen($ulid)) { |
| 46 | + return false; |
| 47 | + } |
| 48 | + |
| 49 | + if (26 !== strspn($ulid, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { |
| 50 | + return false; |
| 51 | + } |
| 52 | + |
| 53 | + return $ulid[0] <= '7'; |
| 54 | + } |
| 55 | + |
| 56 | + public static function fromBinary(string $ulid): self |
| 57 | + { |
| 58 | + if (16 !== \strlen($ulid)) { |
| 59 | + throw new \InvalidArgumentException('Invalid binary ULID.'); |
| 60 | + } |
| 61 | + |
| 62 | + $ulid = bin2hex($ulid); |
| 63 | + $ulid = sprintf('%02s%04s%04s%04s%04s%04s%04s', |
| 64 | + base_convert(substr($ulid, 0, 2), 16, 32), |
| 65 | + base_convert(substr($ulid, 2, 5), 16, 32), |
| 66 | + base_convert(substr($ulid, 7, 5), 16, 32), |
| 67 | + base_convert(substr($ulid, 12, 5), 16, 32), |
| 68 | + base_convert(substr($ulid, 17, 5), 16, 32), |
| 69 | + base_convert(substr($ulid, 22, 5), 16, 32), |
| 70 | + base_convert(substr($ulid, 27, 5), 16, 32) |
| 71 | + ); |
| 72 | + |
| 73 | + return new self(strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ')); |
| 74 | + } |
| 75 | + |
| 76 | + public function toBinary() |
| 77 | + { |
| 78 | + $ulid = strtr($this->ulid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'); |
| 79 | + |
| 80 | + $ulid = sprintf('%02s%05s%05s%05
1CF5
s%05s%05s%05s', |
| 81 | + base_convert(substr($ulid, 0, 2), 32, 16), |
| 82 | + base_convert(substr($ulid, 2, 4), 32, 16), |
| 83 | + base_convert(substr($ulid, 6, 4), 32, 16), |
| 84 | + base_convert(substr($ulid, 10, 4), 32, 16), |
| 85 | + base_convert(substr($ulid, 14, 4), 32, 16), |
| 86 | + base_convert(substr($ulid, 18, 4), 32, 16), |
| 87 | + base_convert(substr($ulid, 22, 4), 32, 16) |
| 88 | + ); |
| 89 | + |
| 90 | + return hex2bin($ulid); |
| 91 | + } |
| 92 | + |
| 93 | + /** |
| 94 | + * Returns whether the argument is of class Ulid and contains the same value as the current instance. |
| 95 | + */ |
| 96 | + public function equals($other): bool |
| 97 | + { |
| 98 | + if (!$other instanceof self) { |
| 99 | + return false; |
| 100 | + } |
| 101 | + |
| 102 | + return $this->ulid === $other->ulid; |
| 103 | + } |
| 104 | + |
| 105 | + public function compare(self $other): int |
| 106 | + { |
| 107 | + return $this->ulid <=> $other->ulid; |
| 108 | + } |
| 109 | + |
| 110 | + public function getTime(): float |
| 111 | + { |
| 112 | + $time = strtr(substr($this->ulid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'); |
| 113 | + |
| 114 | + if (\PHP_INT_SIZE >= 8) { |
| 115 | + return hexdec(base_convert($time, 32, 16)) / 1000; |
| 116 | + } |
| 117 | + |
| 118 | + $time = sprintf('%02s%05s%05s', |
| 119 | + base_convert(substr($time, 0, 2), 32, 16), |
| 120 | + base_convert(substr($time, 2, 4), 32, 16), |
| 121 | + base_convert(substr($time, 6, 4), 32, 16) |
| 122 | + ); |
| 123 | + |
| 124 | + return InternalUtil::toDecimal(hex2bin($time)) / 1000; |
| 125 | + } |
| 126 | + |
| 127 | + public function __toString(): string |
| 128 | + { |
| 129 | + return $this->ulid; |
| 130 | + } |
| 131 | + |
| 132 | + public function jsonSerialize(): string |
| 133 | + { |
| 134 | + return $this->ulid; |
| 135 | + } |
| 136 | + |
| 137 | + private static function generate(): string |
| 138 | + { |
| 139 | + $time = microtime(false); |
| 140 | + $time = substr($time, 11).substr($time, 2, 3); |
| 141 | + |
| 142 | + if ($time !== self::$time) { |
| 143 | + $r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10)); |
| 144 | + $r['r1'] |= ($r['r'] <<= 4) & 0xF0000; |
| 145 | + $r['r2'] |= ($r['r'] <<= 4) & 0xF0000; |
| 146 | + $r['r3'] |= ($r['r'] <<= 4) & 0xF0000; |
| 147 | + $r['r4'] |= ($r['r'] <<= 4) & 0xF0000; |
| 148 | + unset($r['r']); |
| 149 | + self::$rand = array_values($r); |
| 150 | + self::$time = $time; |
| 151 | + } elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) { |
| 152 | + usleep(100); |
| 153 | + |
| 154 | + return self::generate(); |
| 155 | + } else { |
| 156 | + for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) { |
| 157 | + self::$rand[$i] = 0; |
| 158 | + } |
| 159 | + |
| 160 | + ++self::$rand[$i]; |
| 161 | + } |
| 162 | + |
| 163 | + if (\PHP_INT_SIZE >= 8) { |
| 164 | + $time = base_convert($time, 10, 32); |
| 165 | + } else { |
| 166 | + $time = bin2hex(InternalUtil::toBinary($time)); |
| 167 | + $time = sprintf('%s%04s%04s', |
| 168 | + base_convert(substr($time, 0, 2), 16, 32), |
| 169 | + base_convert(substr($time, 2, 5), 16, 32), |
| 170 | + base_convert(substr($time, 7, 5), 16, 32) |
| 171 | + ); |
| 172 | + } |
| 173 | + |
| 174 | + return strtr(sprintf('%010s%04s%04s%04s%04s', |
| 175 | + $time, |
| 176 | + base_convert(self::$rand[0], 10, 32), |
| 177 | + base_convert(self::$rand[1], 10, 32), |
| 178 | + base_convert(self::$rand[2], 10, 32), |
| 179 | + base_convert(self::$rand[3], 10, 32) |
| 180 | + ), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); |
| 181 | + } |
| 182 | +} |
0 commit comments