Skip to content

Commit 92a66fb

Browse files
authored
Merge pull request #100 from mambax7/feature/ulid
2 parents e315bc4 + ec44dbf commit 92a66fb

File tree

2 files changed

+795
-0
lines changed

2 files changed

+795
-0
lines changed

src/Ulid.php

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?php
2+
/*
3+
You may not change or alter any portion of this comment or credits
4+
of supporting developers from this source code or any supporting source code
5+
which is considered copyrighted (c) material of the original comment or credit authors.
6+
7+
This program is distributed in the hope that it will be useful,
8+
but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10+
*/
11+
12+
namespace Xmf;
13+
14+
/**
15+
* Generate ULID
16+
*
17+
* @category Xmf\Ulid
18+
* @package Xmf
19+
* @author Michael Beck <mambax7@gmail.com>
20+
* @copyright 2023 XOOPS Project (https://xoops.org)
21+
* @license GNU GPL 2 or later (https://www.gnu.org/licenses/gpl-2.0.html)
22+
*/
23+
class Ulid
24+
{
25+
const ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
26+
const ENCODING_LENGTH = 32;
27+
28+
/**
29+
* Generate a new ULID.
30+
*
31+
* @return string The generated ULID.
32+
*/
33+
public static function generate(bool $upperCase = true): string
34+
{
35+
$time = self::microtimeToUlidTime(\microtime(true));
36+
$timeChars = self::encodeTime($time);
37+
$randChars = self::encodeRandomness();
38+
$ulid = $timeChars . $randChars;
39+
40+
$ulid = $upperCase ? \strtoupper($ulid) : \strtolower($ulid);
41+
42+
return $ulid;
43+
}
44+
45+
/**
46+
* @param int $time
47+
*
48+
* @return string
49+
*/
50+
public static function encodeTime(int $time): string
51+
{
52+
$encodingCharsArray = str_split(self::ENCODING_CHARS);
53+
$timeChars = '';
54+
for ($i = 0; $i < 10; $i++) {
55+
$mod = \floor($time % self::ENCODING_LENGTH);
56+
$timeChars = $encodingCharsArray[$mod] . $timeChars;
57+
$time = (int)(($time - $mod) / self::ENCODING_LENGTH);
58+
}
59+
return $timeChars;
60+
}
61+
62+
public static function encodeRandomness(): string
63+
{
64+
$encodingCharsArray = str_split(self::ENCODING_CHARS);
65+
$randomBytes = \random_bytes(10); // 80 bits
66+
// Check if the random bytes were generated successfully.
67+
if (false === $randomBytes) {
68+
throw new \RuntimeException('Failed to generate random bytes');
69+
}
70+
71+
$randChars = '';
72+
for ($i = 0; $i < 16; $i++) {
73+
$randValue = \ord($randomBytes[$i % 10]);
74+
if (0 === $i % 2) {
75+
$randValue >>= 3; // take the upper 5 bits
76+
} else {
77+
$randValue &= 31; // take the lower 5 bits
78+
}
79+
$randChars .= $encodingCharsArray[$randValue];
80+
}
81+
return $randChars;
82+
}
83+
84+
/**
85+
* @param string $ulid
86+
*
87+
* @return array
88+
*/
89+
public static function decode(string $ulid): array
90+
{
91+
if (!self::isValid($ulid)) {
92+
throw new \InvalidArgumentException('Invalid ULID string');
93+
}
94+
95+
$time = self::decodeTime($ulid);
96+
$rand = self::decodeRandomness($ulid);
97+
98+
return [
99+
'time' => $time,
100+
'rand' => $rand,
101+
];
102+
}
103+
104+
/**
105+
* @param string $ulid
106+
*
107+
* @return int
108+
*/
109+
public static function decodeTime(string $ulid): int
110+
{
111+
// $encodingCharsArray = str_split(self::ENCODING_CHARS);
112+
113+
// Check if the ULID string is valid.
114+
if (!self::isValid($ulid)) {
115+
throw new \InvalidArgumentException('Invalid ULID string');
116+
}
117+
118+
$time = 0;
119+
for ($i = 0; $i < 10; $i++) {
120+
$char = $ulid[$i];
121+
$value = \strpos(self::ENCODING_CHARS, $char);
122+
$exponent = 9 - $i;
123+
$time += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
124+
}
125+
126+
return $time;
127+
}
128+
129+
/**
130+
* @param string $ulid
131+
*
132+
* @return int
133+
*/
134+
public static function decodeRandomness(string $ulid): int
135+
{
136+
if (26 !== strlen($ulid)) {
137+
throw new \InvalidArgumentException('Invalid ULID length'); // Changed line
138+
}
139+
140+
$rand = 0;
141+
for ($i = 10; $i < 26; $i++) {
142+
$char = $ulid[$i];
143+
$value = \strpos(self::ENCODING_CHARS, $char);
144+
145+
// Check if the random value is within the valid range.
146+
if ($value < 0 || $value >= self::ENCODING_LENGTH) {
147+
throw new \InvalidArgumentException('Invalid ULID random value');
148+
}
149+
$exponent = 15 - $i;
150+
$rand += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
151+
}
152+
153+
return $rand;
154+
}
155+
156+
/**
157+
* @param string $ulid
158+
*
159+
* @return bool
160+
*/
161+
public static function isValid(string $ulid): bool
162+
{
163+
// Check the length of the ULID string before throwing an exception.
164+
if (26 !== strlen($ulid)) {
165+
return false;
166+
}
167+
168+
// Throw an exception if the ULID is invalid.
169+
try {
170+
self::decodeRandomness($ulid);
171+
} catch (\InvalidArgumentException $e) {
172+
return false;
173+
}
174+
175+
return true;
176+
}
177+
178+
/**
179+
* @param float $microtime
180+
*
181+
* @return int
182+
*/
183+
public static function microtimeToUlidTime(float $microtime): int
184+
{
185+
$timestamp = $microtime * 1000000;
186+
$unixEpoch = 946684800000000; // Microseconds since the Unix epoch.
187+
188+
return (int)($timestamp - $unixEpoch);
189+
}
190+
}
191+
192+
193+

0 commit comments

Comments
 (0)