Add fromDateTime() to create version 1 UUIDs from DateTime instances

Fixes #28
This commit is contained in:
Ben Ramsey
2020-02-03 00:48:41 -06:00
parent e269c16cd4
commit 5fa4eb4f17
13 changed files with 265 additions and 5 deletions
+5
View File
@@ -10,8 +10,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added
* Add `Uuid::fromDateTime()` to create version 1 UUIDs from instances of
`\DateTimeInterface`.
### Changed
* Add `fromDateTime()` method to `UuidFactoryInterface`.
### Deprecated
### Removed
+24
View File
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of the ramsey/uuid library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
declare(strict_types=1);
namespace Ramsey\Uuid\Exception;
use RuntimeException as PhpRuntimeException;
/**
* Thrown to indicate that the source of time encountered an error
*/
class TimeSourceException extends PhpRuntimeException
{
}
+8
View File
@@ -214,6 +214,14 @@ class FeatureSet
return $this->randomGenerator;
}
/**
* Returns the time converter configured for this environment
*/
public function getTimeConverter(): TimeConverterInterface
{
return $this->timeConverter;
}
/**
* Returns the time generator configured for this environment
*/
+15 -3
View File
@@ -17,6 +17,7 @@ namespace Ramsey\Uuid\Generator;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Exception\InvalidArgumentException;
use Ramsey\Uuid\Exception\RandomSourceException;
use Ramsey\Uuid\Exception\TimeSourceException;
use Ramsey\Uuid\Provider\NodeProviderInterface;
use Ramsey\Uuid\Provider\TimeProviderInterface;
use Throwable;
@@ -75,12 +76,23 @@ class DefaultTimeGenerator implements TimeGeneratorInterface
}
}
$time = $this->timeProvider->getTime();
$uuidTime = $this->timeConverter->calculateTime(
$this->timeProvider->getTime()->getSeconds()->toString(),
$this->timeProvider->getTime()->getMicroSeconds()->toString()
$time->getSeconds()->toString(),
$time->getMicroSeconds()->toString()
);
$timeBytes = (string) hex2bin(str_pad($uuidTime->toString(), 16, '0', STR_PAD_LEFT));
$timeHex = str_pad($uuidTime->toString(), 16, '0', STR_PAD_LEFT);
if (strlen($timeHex) !== 16) {
throw new TimeSourceException(sprintf(
'The generated time of \'%s\' is larger than expected',
$timeHex
));
}
$timeBytes = (string) hex2bin($timeHex);
return $timeBytes[4] . $timeBytes[5] . $timeBytes[6] . $timeBytes[7]
. $timeBytes[2] . $timeBytes[3]
+22
View File
@@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Ramsey\Uuid;
use DateTimeInterface;
use Ramsey\Uuid\Codec\CodecInterface;
use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\TimeConverterInterface;
@@ -368,6 +369,27 @@ class Uuid implements UuidInterface
return self::getFactory()->fromString($uuid);
}
/**
* Creates a UUID from a DateTimeInterface instance
*
* @param DateTimeInterface $dateTime The date and time
* @param Hexadecimal|null $node A 48-bit number representing the hardware
* address
* @param int|null $clockSeq A 14-bit number used to help avoid duplicates
* that could arise when the clock is set backwards in time or if the
* node ID changes
*
* @return UuidInterface A UuidInterface instance that represents a
* version 1 UUID created from a DateTimeInterface instance
*/
public static function fromDateTime(
DateTimeInterface $dateTime,
?Hexadecimal $node = null,
?int $clockSeq = null
): UuidInterface {
return self::getFactory()->fromDateTime($dateTime, $node, $clockSeq);
}
/**
* Creates a UUID from a 128-bit integer string
*
+33
View File
@@ -14,15 +14,20 @@ declare(strict_types=1);
namespace Ramsey\Uuid;
use DateTimeInterface;
use Ramsey\Uuid\Builder\UuidBuilderInterface;
use Ramsey\Uuid\Codec\CodecInterface;
use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
use Ramsey\Uuid\Generator\DefaultTimeGenerator;
use Ramsey\Uuid\Generator\RandomGeneratorInterface;
use Ramsey\Uuid\Generator\TimeGeneratorInterface;
use Ramsey\Uuid\Provider\NodeProviderInterface;
use Ramsey\Uuid\Provider\Time\FixedTimeProvider;
use Ramsey\Uuid\Type\Hexadecimal;
use Ramsey\Uuid\Type\IntegerValue;
use Ramsey\Uuid\Type\Time;
use Ramsey\Uuid\Validator\ValidatorInterface;
class UuidFactory implements UuidFactoryInterface
@@ -52,6 +57,11 @@ class UuidFactory implements UuidFactoryInterface
*/
private $randomGenerator;
/**
* @var TimeConverterInterface
*/
private $timeConverter;
/**
* @var TimeGeneratorInterface
*/
@@ -79,6 +89,7 @@ class UuidFactory implements UuidFactoryInterface
$this->nodeProvider = $features->getNodeProvider();
$this->numberConverter = $features->getNumberConverter();
$this->randomGenerator = $features->getRandomGenerator();
$this->timeConverter = $features->getTimeConverter();
$this->timeGenerator = $features->getTimeGenerator();
$this->uuidBuilder = $features->getBuilder();
$this->validator = $features->getValidator();
@@ -234,6 +245,28 @@ class UuidFactory implements UuidFactoryInterface
return $this->fromString($hex);
}
public function fromDateTime(
DateTimeInterface $dateTime,
?Hexadecimal $node = null,
?int $clockSeq = null
): UuidInterface {
$timeProvider = new FixedTimeProvider(
new Time($dateTime->getTimestamp(), $dateTime->format('u'))
);
$timeGenerator = new DefaultTimeGenerator(
$this->nodeProvider,
$this->timeConverter,
$timeProvider
);
$nodeHex = $node ? $node->toString() : null;
$bytes = $timeGenerator->generate($nodeHex, $clockSeq);
return $this->uuidFromBytesAndVersion($bytes, 1);
}
/**
* @inheritDoc
*/
+20
View File
@@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Ramsey\Uuid;
use DateTimeInterface;
use Ramsey\Uuid\Type\Hexadecimal;
use Ramsey\Uuid\Type\IntegerValue;
use Ramsey\Uuid\Validator\ValidatorInterface;
@@ -139,4 +140,23 @@ interface UuidFactoryInterface
* @psalm-pure
*/
public function fromInteger(string $integer): UuidInterface;
/**
* Creates a UUID from a DateTimeInterface instance
*
* @param DateTimeInterface $dateTime The date and time
* @param Hexadecimal|null $node A 48-bit number representing the hardware
* address
* @param int|null $clockSeq A 14-bit number used to help avoid duplicates
* that could arise when the clock is set backwards in time or if the
* node ID changes
*
* @return UuidInterface A UuidInterface instance that represents a
* version 1 UUID created from a DateTimeInterface instance
*/
public function fromDateTime(
DateTimeInterface $dateTime,
?Hexadecimal $node = null,
?int $clockSeq = null
): UuidInterface;
}
@@ -54,13 +54,28 @@ class GenericTimeConverterTest extends TestCase
'expected' => '0000000000000000',
],
// This is the last possible time supported by v1 UUIDs:
// This is the last possible time supported by the GenericTimeConverter:
// 60038-03-11 05:36:10.955161
// When a UUID is created from this time, however, the highest 4 bits
// are replaced with the version (1), so we lose fidelity and cannot
// accurately decompose the date from the UUID.
[
'seconds' => '1832455114570',
'microseconds' => '955161',
'expected' => 'fffffffffffffffa',
],
// This is technically the last possible time supported by v1 UUIDs:
// 5236-03-31 21:21:00.684697
// All dates above this will lose fidelity, since the highest 4 bits
// are replaced with the UUID version (1). As a result, we cannot
// accurately decompose the date from UUIDs created from dates
// greater than this one.
[
'seconds' => '103072857660',
'microseconds' => '684697',
'expected' => '0ffffffffffffffa',
],
];
}
+8
View File
@@ -6,6 +6,7 @@ namespace Ramsey\Uuid\Test;
use Mockery;
use Ramsey\Uuid\Builder\FallbackBuilder;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\FeatureSet;
use Ramsey\Uuid\Guid\GuidBuilder;
use Ramsey\Uuid\Validator\ValidatorInterface;
@@ -35,4 +36,11 @@ class FeatureSetTest extends TestCase
$this->assertSame($validator, $featureSet->getValidator());
}
public function testGetTimeConverter(): void
{
$featureSet = new FeatureSet();
$this->assertInstanceOf(TimeConverterInterface::class, $featureSet->getTimeConverter());
}
}
@@ -12,8 +12,11 @@ use PHPUnit\Framework\MockObject\MockObject;
use Ramsey\Uuid\BinaryUtils;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Exception\RandomSourceException;
use Ramsey\Uuid\Exception\TimeSourceException;
use Ramsey\Uuid\FeatureSet;
use Ramsey\Uuid\Generator\DefaultTimeGenerator;
use Ramsey\Uuid\Provider\NodeProviderInterface;
use Ramsey\Uuid\Provider\Time\FixedTimeProvider;
use Ramsey\Uuid\Provider\TimeProviderInterface;
use Ramsey\Uuid\Test\TestCase;
use Ramsey\Uuid\Type\Hexadecimal;
@@ -189,4 +192,22 @@ class DefaultTimeGeneratorTest extends TestCase
$defaultTimeGenerator->generate($this->nodeId);
}
public function testDefaultTimeGeneratorThrowsExceptionForLargeGeneratedValue(): void
{
$timeProvider = new FixedTimeProvider(new Time('1832455114570', '955162'));
$featureSet = new FeatureSet();
$timeGenerator = new DefaultTimeGenerator(
$featureSet->getNodeProvider(),
$featureSet->getTimeConverter(),
$timeProvider
);
$this->expectException(TimeSourceException::class);
$this->expectExceptionMessage(
'The generated time of \'10000000000000004\' is larger than expected'
);
$timeGenerator->generate();
}
}
+79
View File
@@ -4,16 +4,22 @@ declare(strict_types=1);
namespace Ramsey\Uuid\Test;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use Ramsey\Uuid\Builder\UuidBuilderInterface;
use Ramsey\Uuid\Codec\CodecInterface;
use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\FeatureSet;
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
use Ramsey\Uuid\Generator\RandomGeneratorInterface;
use Ramsey\Uuid\Generator\TimeGeneratorInterface;
use Ramsey\Uuid\Provider\NodeProviderInterface;
use Ramsey\Uuid\Rfc4122\UuidV1;
use Ramsey\Uuid\Type\Hexadecimal;
use Ramsey\Uuid\UuidFactory;
use Ramsey\Uuid\Validator\ValidatorInterface;
@@ -55,6 +61,7 @@ class UuidFactoryTest extends TestCase
$codec = Mockery::mock(CodecInterface::class);
$nodeProvider = Mockery::mock(NodeProviderInterface::class);
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
$timeConverter = Mockery::mock(TimeConverterInterface::class);
$timeGenerator = Mockery::mock(TimeGeneratorInterface::class);
$dceSecurityGenerator = Mockery::mock(DceSecurityGeneratorInterface::class);
$numberConverter = Mockery::mock(NumberConverterInterface::class);
@@ -65,6 +72,7 @@ class UuidFactoryTest extends TestCase
'getCodec' => $codec,
'getNodeProvider' => $nodeProvider,
'getRandomGenerator' => $randomGenerator,
'getTimeConverter' => $timeConverter,
'getTimeGenerator' => $timeGenerator,
'getDceSecurityGenerator' => $dceSecurityGenerator,
'getNumberConverter' => $numberConverter,
@@ -122,4 +130,75 @@ class UuidFactoryTest extends TestCase
$uuidFactory->setUuidBuilder($uuidBuilder);
$this->assertEquals($uuidBuilder, $uuidFactory->getUuidBuilder());
}
/**
* @dataProvider provideDateTime
*/
public function testFromDateTime(
DateTimeInterface $dateTime,
?Hexadecimal $node,
?int $clockSeq,
string $expectedUuidFormat,
string $expectedTime
): void {
$factory = new UuidFactory();
/** @var UuidV1 $uuid */
$uuid = $factory->fromDateTime($dateTime, $node, $clockSeq);
$this->assertInstanceOf(UuidV1::class, $uuid);
$this->assertStringMatchesFormat($expectedUuidFormat, $uuid->toString());
$this->assertSame($expectedTime, $uuid->getDateTime()->format('U.u'));
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification
*/
public function provideDateTime(): array
{
return [
[
new DateTimeImmutable('2012-07-04 02:14:34.491000'),
null,
null,
'ff6f8cb0-c57d-11e1-%s',
'1341368074.491000',
],
[
new DateTimeImmutable('1582-10-16 16:34:04'),
new Hexadecimal('0800200c9a66'),
15137,
'0901e600-0154-1000-%cb21-0800200c9a66',
'-12219146756.000000',
],
[
new DateTime('5236-03-31 21:20:59.999999'),
new Hexadecimal('00007ffffffe'),
1641,
'ff9785f6-ffff-1fff-%c669-00007ffffffe',
'103072857659.999999',
],
[
new DateTime('1582-10-15 00:00:00'),
new Hexadecimal('00007ffffffe'),
1641,
'00000000-0000-1000-%c669-00007ffffffe',
'-12219292800.000000',
],
[
new DateTimeImmutable('@103072857660.684697'),
new Hexadecimal('0'),
0,
'fffffffa-ffff-1fff-%c000-000000000000',
'103072857660.684697',
],
[
new DateTimeImmutable('5236-03-31 21:21:00.684697'),
null,
null,
'fffffffa-ffff-1fff-%s',
'103072857660.684697',
],
];
}
}
+12
View File
@@ -33,6 +33,7 @@ use Ramsey\Uuid\Rfc4122\UuidV2;
use Ramsey\Uuid\Rfc4122\UuidV3;
use Ramsey\Uuid\Rfc4122\UuidV4;
use Ramsey\Uuid\Rfc4122\UuidV5;
use Ramsey\Uuid\Type\Hexadecimal;
use Ramsey\Uuid\Type\Time;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;
@@ -961,6 +962,17 @@ class UuidTest extends TestCase
$this->assertTrue($uuid->equals($fromIntegerUuid));
}
public function testFromDateTime(): void
{
/** @var UuidV1 $uuid */
$uuid = Uuid::fromString('ff6f8cb0-c57d-11e1-8b21-0800200c9a66');
$dateTime = $uuid->getDateTime();
$fromDateTimeUuid = Uuid::fromDateTime($dateTime, new Hexadecimal('0800200c9a66'), 2849);
$this->assertTrue($uuid->equals($fromDateTimeUuid));
}
/**
* This test ensures that Ramsey\Uuid passes the same test cases
* as the Python UUID library.
+2 -1
View File
@@ -70,7 +70,8 @@
</TypeDoesNotContainType>
</file>
<file src="src/Uuid.php">
<ImpureMethodCall occurrences="4">
<ImpureMethodCall occurrences="5">
<code>getFactory</code>
<code>getFactory</code>
<code>getFactory</code>
<code>getFactory</code>