mirror of
https://github.com/ramsey/uuid.git
synced 2026-06-14 15:56:48 +03:00
fix: ensure monotonicity for version 7 UUIDs
This commit is contained in:
+2
-2
@@ -90,8 +90,8 @@
|
||||
"phpcbf": "phpcbf -vpw --cache=build/cache/phpcs.cache",
|
||||
"phpcs": "phpcs --cache=build/cache/phpcs.cache",
|
||||
"phpstan": [
|
||||
"phpstan analyse --no-progress",
|
||||
"phpstan analyse -c phpstan-tests.neon --no-progress"
|
||||
"phpstan analyse --no-progress --memory-limit=1G",
|
||||
"phpstan analyse -c phpstan-tests.neon --no-progress --memory-limit=1G"
|
||||
],
|
||||
"phpunit": "phpunit --verbose --colors=always",
|
||||
"phpunit-coverage": "phpunit --verbose --colors=always --coverage-html build/coverage",
|
||||
|
||||
+3
-11
@@ -23,7 +23,6 @@ use Ramsey\Uuid\Converter\Number\GenericNumberConverter;
|
||||
use Ramsey\Uuid\Converter\NumberConverterInterface;
|
||||
use Ramsey\Uuid\Converter\Time\GenericTimeConverter;
|
||||
use Ramsey\Uuid\Converter\Time\PhpTimeConverter;
|
||||
use Ramsey\Uuid\Converter\Time\UnixTimeConverter;
|
||||
use Ramsey\Uuid\Converter\TimeConverterInterface;
|
||||
use Ramsey\Uuid\Generator\DceSecurityGenerator;
|
||||
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
|
||||
@@ -105,7 +104,7 @@ class FeatureSet
|
||||
$this->validator = new GenericValidator();
|
||||
|
||||
assert($this->timeProvider !== null);
|
||||
$this->unixTimeGenerator = $this->buildUnixTimeGenerator($this->timeProvider);
|
||||
$this->unixTimeGenerator = $this->buildUnixTimeGenerator();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,17 +338,10 @@ class FeatureSet
|
||||
|
||||
/**
|
||||
* Returns a Unix Epoch time generator configured for this environment
|
||||
*
|
||||
* @param TimeProviderInterface $timeProvider The time provider to use with
|
||||
* the time generator
|
||||
*/
|
||||
private function buildUnixTimeGenerator(TimeProviderInterface $timeProvider): TimeGeneratorInterface
|
||||
private function buildUnixTimeGenerator(): TimeGeneratorInterface
|
||||
{
|
||||
return new UnixTimeGenerator(
|
||||
new UnixTimeConverter(new BrickMathCalculator()),
|
||||
$timeProvider,
|
||||
$this->randomGenerator,
|
||||
);
|
||||
return new UnixTimeGenerator($this->randomGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,39 +14,156 @@ declare(strict_types=1);
|
||||
|
||||
namespace Ramsey\Uuid\Generator;
|
||||
|
||||
use Ramsey\Uuid\Converter\TimeConverterInterface;
|
||||
use Ramsey\Uuid\Provider\TimeProviderInterface;
|
||||
use Brick\Math\BigInteger;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Ramsey\Uuid\Type\Hexadecimal;
|
||||
|
||||
use function hex2bin;
|
||||
use function hash;
|
||||
use function pack;
|
||||
use function str_pad;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function substr_replace;
|
||||
use function unpack;
|
||||
|
||||
use const PHP_INT_SIZE;
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* UnixTimeGenerator generates bytes that combine a 48-bit timestamp in
|
||||
* milliseconds since the Unix Epoch with 80 random bits
|
||||
*
|
||||
* Code and concepts within this class are borrowed from the symfony/uid package
|
||||
* and are used under the terms of the MIT license distributed with symfony/uid.
|
||||
*
|
||||
* symfony/uid is copyright (c) Fabien Potencier.
|
||||
*
|
||||
* @link https://symfony.com/components/Uid Symfony Uid component
|
||||
* @link https://github.com/symfony/uid/blob/4f9f537e57261519808a7ce1d941490736522bbc/UuidV7.php Symfony UuidV7 class
|
||||
* @link https://github.com/symfony/uid/blob/6.2/LICENSE MIT License
|
||||
*/
|
||||
class UnixTimeGenerator implements TimeGeneratorInterface
|
||||
{
|
||||
private static string $time = '';
|
||||
private static ?string $seed = null;
|
||||
private static int $seedIndex = 0;
|
||||
|
||||
/** @var int[] */
|
||||
private static array $rand = [];
|
||||
|
||||
/** @var int[] */
|
||||
private static array $seedParts;
|
||||
|
||||
public function __construct(
|
||||
private TimeConverterInterface $timeConverter,
|
||||
private TimeProviderInterface $timeProvider,
|
||||
private RandomGeneratorInterface $randomGenerator
|
||||
private RandomGeneratorInterface $randomGenerator,
|
||||
private int $intSize = PHP_INT_SIZE
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Hexadecimal|int|string|null $node Unused in this generator
|
||||
* @param int|null $clockSeq Unused in this generator
|
||||
* @param DateTimeInterface $dateTime A date-time instance to use when
|
||||
* generating bytes
|
||||
*
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function generate($node = null, ?int $clockSeq = null): string
|
||||
public function generate($node = null, ?int $clockSeq = null, ?DateTimeInterface $dateTime = null): string
|
||||
{
|
||||
// Generate 10 random bytes to append to the string returned, since our
|
||||
// time bytes will consist of 6 bytes.
|
||||
$random = $this->randomGenerator->generate(10);
|
||||
$time = ($dateTime ?? new DateTimeImmutable('now'))->format('Uv');
|
||||
|
||||
$time = $this->timeProvider->getTime();
|
||||
$unixTimeHex = $this->timeConverter->calculateTime(
|
||||
$time->getSeconds()->toString(),
|
||||
$time->getMicroseconds()->toString(),
|
||||
);
|
||||
if ($time > self::$time || ($dateTime !== null && $time !== self::$time)) {
|
||||
$this->randomize($time);
|
||||
} else {
|
||||
$time = $this->increment();
|
||||
}
|
||||
|
||||
return hex2bin($unixTimeHex->toString()) . $random;
|
||||
if ($this->intSize >= 8) {
|
||||
$time = substr(pack('J', (int) $time), -6);
|
||||
} else {
|
||||
$time = str_pad(BigInteger::of($time)->toBytes(false), 6, "\x00", STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/** @var non-empty-string */
|
||||
return $time . pack('n*', self::$rand[1], self::$rand[2], self::$rand[3], self::$rand[4], self::$rand[5]);
|
||||
}
|
||||
|
||||
private function randomize(string $time): void
|
||||
{
|
||||
if (self::$seed === null) {
|
||||
$seed = $this->randomGenerator->generate(16);
|
||||
self::$seed = $seed;
|
||||
} else {
|
||||
$seed = $this->randomGenerator->generate(10);
|
||||
}
|
||||
|
||||
/** @var int[] $rand */
|
||||
$rand = unpack('n*', $seed);
|
||||
$rand[1] &= 0x03ff;
|
||||
|
||||
self::$rand = $rand;
|
||||
self::$time = $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Special thanks to Nicolas Grekas for sharing the following information:
|
||||
*
|
||||
* Within the same ms, we increment the rand part by a random 24-bit number.
|
||||
*
|
||||
* Instead of getting this number from random_bytes(), which is slow, we get
|
||||
* it by sha512-hashing self::$seed. This produces 64 bytes of entropy,
|
||||
* which we need to split in a list of 24-bit numbers. unpack() first splits
|
||||
* them into 16 x 32-bit numbers; we take the first byte of each of these
|
||||
* numbers to get 5 extra 24-bit numbers. Then, we consume those numbers
|
||||
* one-by-one and run this logic every 21 iterations.
|
||||
*
|
||||
* self::$rand holds the random part of the UUID, split into 5 x 16-bit
|
||||
* numbers for x86 portability. We increment this random part by the next
|
||||
* 24-bit number in the self::$seedParts list and decrement
|
||||
* self::$seedIndex.
|
||||
*
|
||||
* @link https://twitter.com/nicolasgrekas/status/1583356938825261061 Tweet from Nicolas Grekas
|
||||
*/
|
||||
private function increment(): string
|
||||
{
|
||||
if (self::$seedIndex === 0 && self::$seed !== null) {
|
||||
self::$seed = hash('sha512', self::$seed, true);
|
||||
|
||||
/** @var int[] $s */
|
||||
$s = unpack('l*', self::$seed);
|
||||
$s[] = ($s[1] >> 8 & 0xff0000) | ($s[2] >> 16 & 0xff00) | ($s[3] >> 24 & 0xff);
|
||||
$s[] = ($s[4] >> 8 & 0xff0000) | ($s[5] >> 16 & 0xff00) | ($s[6] >> 24 & 0xff);
|
||||
$s[] = ($s[7] >> 8 & 0xff0000) | ($s[8] >> 16 & 0xff00) | ($s[9] >> 24 & 0xff);
|
||||
$s[] = ($s[10] >> 8 & 0xff0000) | ($s[11] >> 16 & 0xff00) | ($s[12] >> 24 & 0xff);
|
||||
$s[] = ($s[13] >> 8 & 0xff0000) | ($s[14] >> 16 & 0xff00) | ($s[15] >> 24 & 0xff);
|
||||
|
||||
self::$seedParts = $s;
|
||||
self::$seedIndex = 21;
|
||||
}
|
||||
|
||||
self::$rand[5] = 0xffff & $carry = self::$rand[5] + (self::$seedParts[self::$seedIndex--] & 0xffffff);
|
||||
self::$rand[4] = 0xffff & $carry = self::$rand[4] + ($carry >> 16);
|
||||
self::$rand[3] = 0xffff & $carry = self::$rand[3] + ($carry >> 16);
|
||||
self::$rand[2] = 0xffff & $carry = self::$rand[2] + ($carry >> 16);
|
||||
self::$rand[1] += $carry >> 16;
|
||||
|
||||
if (0xfc00 & self::$rand[1]) {
|
||||
$time = self::$time;
|
||||
$mtime = (int) substr($time, -9);
|
||||
|
||||
if ($this->intSize >= 8 || strlen($time) < 10) {
|
||||
$time = (string) ((int) $time + 1);
|
||||
} elseif ($mtime === 999999999) {
|
||||
$time = (1 + (int) substr($time, 0, -9)) . '000000000';
|
||||
} else {
|
||||
$mtime++;
|
||||
$time = substr_replace($time, str_pad((string) $mtime, 9, '0', STR_PAD_LEFT), -9);
|
||||
}
|
||||
|
||||
$this->randomize($time);
|
||||
}
|
||||
|
||||
return self::$time;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ final class Time implements TypeInterface
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->seconds->toString() . '.' . $this->microseconds->toString();
|
||||
return $this->seconds->toString() . '.' . sprintf('%06s', $this->microseconds->toString());
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
|
||||
+2
-17
@@ -18,7 +18,6 @@ use DateTimeInterface;
|
||||
use Ramsey\Uuid\Builder\UuidBuilderInterface;
|
||||
use Ramsey\Uuid\Codec\CodecInterface;
|
||||
use Ramsey\Uuid\Converter\NumberConverterInterface;
|
||||
use Ramsey\Uuid\Converter\Time\UnixTimeConverter;
|
||||
use Ramsey\Uuid\Converter\TimeConverterInterface;
|
||||
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
|
||||
use Ramsey\Uuid\Generator\DefaultTimeGenerator;
|
||||
@@ -27,7 +26,6 @@ use Ramsey\Uuid\Generator\RandomGeneratorInterface;
|
||||
use Ramsey\Uuid\Generator\TimeGeneratorInterface;
|
||||
use Ramsey\Uuid\Generator\UnixTimeGenerator;
|
||||
use Ramsey\Uuid\Lazy\LazyUuidFromString;
|
||||
use Ramsey\Uuid\Math\BrickMathCalculator;
|
||||
use Ramsey\Uuid\Provider\NodeProviderInterface;
|
||||
use Ramsey\Uuid\Provider\Time\FixedTimeProvider;
|
||||
use Ramsey\Uuid\Type\Hexadecimal;
|
||||
@@ -396,21 +394,8 @@ class UuidFactory implements UuidFactoryInterface
|
||||
*/
|
||||
public function uuid7(?DateTimeInterface $dateTime = null): UuidInterface
|
||||
{
|
||||
if ($dateTime !== null) {
|
||||
$timeProvider = new FixedTimeProvider(
|
||||
new Time($dateTime->format('U'), $dateTime->format('u'))
|
||||
);
|
||||
|
||||
$timeGenerator = new UnixTimeGenerator(
|
||||
new UnixTimeConverter(new BrickMathCalculator()),
|
||||
$timeProvider,
|
||||
$this->randomGenerator,
|
||||
);
|
||||
|
||||
$bytes = $timeGenerator->generate();
|
||||
} else {
|
||||
$bytes = $this->unixTimeGenerator->generate();
|
||||
}
|
||||
assert($this->unixTimeGenerator instanceof UnixTimeGenerator);
|
||||
$bytes = $this->unixTimeGenerator->generate(null, null, $dateTime);
|
||||
|
||||
return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_UNIX_TIME);
|
||||
}
|
||||
|
||||
@@ -4,36 +4,201 @@ declare(strict_types=1);
|
||||
|
||||
namespace Ramsey\Uuid\Test\Generator;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Ramsey\Uuid\Converter\Time\UnixTimeConverter;
|
||||
use Ramsey\Uuid\Generator\RandomBytesGenerator;
|
||||
use Ramsey\Uuid\Generator\RandomGeneratorInterface;
|
||||
use Ramsey\Uuid\Generator\UnixTimeGenerator;
|
||||
use Ramsey\Uuid\Math\BrickMathCalculator;
|
||||
use Ramsey\Uuid\Provider\TimeProviderInterface;
|
||||
use Ramsey\Uuid\Test\TestCase;
|
||||
use Ramsey\Uuid\Type\Time;
|
||||
|
||||
class UnixTimeGeneratorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerate(): void
|
||||
{
|
||||
$unixTimeConverter = new UnixTimeConverter(new BrickMathCalculator());
|
||||
|
||||
/** @var TimeProviderInterface&MockInterface $timeProvider */
|
||||
$timeProvider = Mockery::mock(TimeProviderInterface::class, [
|
||||
'getTime' => new Time('1578612359', '521023'),
|
||||
]);
|
||||
$dateTime = new DateTimeImmutable('@1578612359.521023');
|
||||
$expectedBytes = "\x01\x6f\x8c\xa1\x01\x61\x03\x00\xff\x00\xff\x00\xff\x00\xff\x00";
|
||||
|
||||
/** @var RandomGeneratorInterface&MockInterface $randomGenerator */
|
||||
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
|
||||
$randomGenerator->expects()->generate(10)->andReturns("\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00");
|
||||
|
||||
$unixTimeGenerator = new UnixTimeGenerator($unixTimeConverter, $timeProvider, $randomGenerator);
|
||||
|
||||
$this->assertSame(
|
||||
"\x01\x6f\x8c\xa1\x01\x61\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00",
|
||||
$unixTimeGenerator->generate(),
|
||||
$randomGenerator->expects()->generate(16)->andReturns(
|
||||
"\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00",
|
||||
);
|
||||
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator);
|
||||
|
||||
$bytes = $unixTimeGenerator->generate(null, null, $dateTime);
|
||||
|
||||
$this->assertSame($expectedBytes, $bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResults(): void
|
||||
{
|
||||
$randomGenerator = new RandomBytesGenerator();
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate();
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsWithSameDate(): void
|
||||
{
|
||||
$dateTime = new DateTimeImmutable('now');
|
||||
$randomGenerator = new RandomBytesGenerator();
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate(null, null, $dateTime);
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsFor32BitPath(): void
|
||||
{
|
||||
$randomGenerator = new RandomBytesGenerator();
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator, 4);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate();
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsWithSameDateFor32BitPath(): void
|
||||
{
|
||||
$dateTime = new DateTimeImmutable('now');
|
||||
$randomGenerator = new RandomBytesGenerator();
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator, 4);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate(null, null, $dateTime);
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsStartingWithAllBitsSet(): void
|
||||
{
|
||||
/** @var RandomGeneratorInterface&MockInterface $randomGenerator */
|
||||
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
|
||||
$randomGenerator->expects()->generate(16)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
$randomGenerator->expects()->generate(10)->times(24)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate();
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsStartingWithAllBitsSetWithSameDate(): void
|
||||
{
|
||||
$dateTime = new DateTimeImmutable('now');
|
||||
|
||||
/** @var RandomGeneratorInterface&MockInterface $randomGenerator */
|
||||
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
|
||||
$randomGenerator->expects()->generate(16)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
$randomGenerator->expects()->generate(10)->times(24)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate(null, null, $dateTime);
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsStartingWithAllBitsSetFor32BitPath(): void
|
||||
{
|
||||
/** @var RandomGeneratorInterface&MockInterface $randomGenerator */
|
||||
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
|
||||
$randomGenerator->expects()->generate(16)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
$randomGenerator->expects()->generate(10)->times(24)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator, 4);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate();
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess since values are stored statically on the class
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGenerateProducesMonotonicResultsStartingWithAllBitsSetWithSameDateFor32BitPath(): void
|
||||
{
|
||||
$dateTime = new DateTimeImmutable('now');
|
||||
|
||||
/** @var RandomGeneratorInterface&MockInterface $randomGenerator */
|
||||
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
|
||||
$randomGenerator->expects()->generate(16)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
$randomGenerator->expects()->generate(10)->times(24)->andReturns(
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
);
|
||||
|
||||
$unixTimeGenerator = new UnixTimeGenerator($randomGenerator, 4);
|
||||
|
||||
$previous = '';
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$bytes = $unixTimeGenerator->generate(null, null, $dateTime);
|
||||
$this->assertTrue($bytes > $previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ class TimeTest extends TestCase
|
||||
|
||||
if ($microseconds !== null) {
|
||||
$params[] = $microseconds;
|
||||
$timeString .= ".{$microseconds}";
|
||||
$timeString .= sprintf('.%06s', (string) $microseconds);
|
||||
} else {
|
||||
$timeString .= '.0';
|
||||
$timeString .= '.000000';
|
||||
}
|
||||
|
||||
$time = new Time(...$params);
|
||||
|
||||
@@ -768,6 +768,65 @@ class UuidTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testUuid7SettingTheClockBackwards(): void
|
||||
{
|
||||
$dates = [
|
||||
new DateTimeImmutable('now'),
|
||||
new DateTimeImmutable('last year'),
|
||||
new DateTimeImmutable('1979-01-01 00:00:00.000000'),
|
||||
];
|
||||
|
||||
foreach ($dates as $dateTime) {
|
||||
$previous = Uuid::uuid7($dateTime);
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$uuid = Uuid::uuid7($dateTime);
|
||||
$this->assertGreaterThan(0, $uuid->compareTo($previous));
|
||||
$this->assertSame($dateTime->format('Y-m-d H:i'), $uuid->getDateTime()->format('Y-m-d H:i'));
|
||||
$previous = $uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testUuid7WithMinimumDateTime(): void
|
||||
{
|
||||
$dateTime = new DateTimeImmutable('1979-01-01 00:00:00.000000');
|
||||
|
||||
$uuid = Uuid::uuid7($dateTime);
|
||||
$this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime());
|
||||
$this->assertSame(2, $uuid->getVariant());
|
||||
$this->assertSame(7, $uuid->getVersion());
|
||||
$this->assertSame(
|
||||
'1979-01-01T00:00:00.000+00:00',
|
||||
$uuid->getDateTime()->format(DateTimeInterface::RFC3339_EXTENDED),
|
||||
);
|
||||
}
|
||||
|
||||
public function testUuid7EachUuidIsMonotonicallyIncreasing(): void
|
||||
{
|
||||
$previous = Uuid::uuid7();
|
||||
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$uuid = Uuid::uuid7();
|
||||
$now = gmdate('Y-m-d H:i');
|
||||
$this->assertGreaterThan(0, $uuid->compareTo($previous));
|
||||
$this->assertSame($now, $uuid->getDateTime()->format('Y-m-d H:i'));
|
||||
$previous = $uuid;
|
||||
}
|
||||
}
|
||||
|
||||
public function testUuid7EachUuidFromSameDateTimeIsMonotonicallyIncreasing(): void
|
||||
{
|
||||
$dateTime = new DateTimeImmutable();
|
||||
$previous = Uuid::uuid7($dateTime);
|
||||
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$uuid = Uuid::uuid7($dateTime);
|
||||
$this->assertGreaterThan(0, $uuid->compareTo($previous));
|
||||
$this->assertSame($dateTime->format('Y-m-d H:i'), $uuid->getDateTime()->format('Y-m-d H:i'));
|
||||
$previous = $uuid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests known version-3 UUIDs
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user