[Uid] Add UuidFactory to create Ulid and Uuid from timestamps, namespaces and nodes

This commit is contained in:
Thomas Calvet 2020-12-14 23:47:04 +01:00 committed by Nicolas Grekas
parent 37e1823598
commit 88a99ddbdf
23 changed files with 833 additions and 27 deletions

View File

@ -6,6 +6,7 @@ CHANGELOG
* Deprecate `DoctrineTestHelper` and `TestRepositoryFactory`
* [BC BREAK] Remove `UuidV*Generator` classes
* Add `UuidGenerator`
5.2.0
-----

View File

@ -13,12 +13,24 @@ namespace Symfony\Bridge\Doctrine\IdGenerator;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Symfony\Component\Uid\Factory\UlidFactory;
use Symfony\Component\Uid\Ulid;
final class UlidGenerator extends AbstractIdGenerator
{
private $factory;
public function __construct(UlidFactory $factory = null)
{
$this->factory = $factory;
}
public function generate(EntityManager $em, $entity): Ulid
{
if ($this->factory) {
return $this->factory->create();
}
return new Ulid();
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\IdGenerator;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\Uuid;
final class UuidGenerator extends AbstractIdGenerator
{
private $protoFactory;
private $factory;
private $entityGetter;
public function __construct(UuidFactory $factory = null)
{
$this->protoFactory = $this->factory = $factory ?? new UuidFactory();
}
public function generate(EntityManager $em, $entity): Uuid
{
if (null !== $this->entityGetter) {
if (\is_callable([$entity, $this->entityGetter])) {
return $this->factory->create($entity->{$this->entityGetter}());
}
return $this->factory->create($entity->{$this->entityGetter});
}
return $this->factory->create();
}
/**
* @param Uuid|string|null $namespace
*
* @return static
*/
public function nameBased(string $entityGetter, $namespace = null): self
{
$clone = clone $this;
$clone->factory = $clone->protoFactory->nameBased($namespace);
$clone->entityGetter = $entityGetter;
return $clone;
}
/**
* @return static
*/
public function randomBased(): self
{
$clone = clone $this;
$clone->factory = $clone->protoFactory->randomBased();
$clone->entityGetter = null;
return $clone;
}
/**
* @param Uuid|string|null $node
*
* @return static
*/
public function timeBased($node = null): self
{
$clone = clone $this;
$clone->factory = $clone->protoFactory->timeBased($node);
$clone->entityGetter = null;
return $clone;
}
}

View File

@ -14,7 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\IdGenerator;
use Doctrine\ORM\Mapping\Entity;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\Factory\UlidFactory;
use Symfony\Component\Uid\Ulid;
class UlidGeneratorTest extends TestCase
@ -25,8 +25,23 @@ class UlidGeneratorTest extends TestCase
$generator = new UlidGenerator();
$ulid = $generator->generate($em, new Entity());
$this->assertInstanceOf(AbstractUid::class, $ulid);
$this->assertInstanceOf(Ulid::class, $ulid);
$this->assertTrue(Ulid::isValid($ulid));
}
/**
* @requires function \Symfony\Component\Uid\Factory\UlidFactory::create
*/
public function testUlidFactory()
{
$ulid = new Ulid('00000000000000000000000000');
$em = new EntityManager();
$factory = $this->createMock(UlidFactory::class);
$factory->expects($this->any())
->method('create')
->willReturn($ulid);
$generator = new UlidGenerator($factory);
$this->assertSame($ulid, $generator->generate($em, new Entity()));
}
}

View File

@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Tests\IdGenerator;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\NilUuid;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV6;
/**
* @requires function \Symfony\Component\Uid\Factory\UuidFactory::create
*/
class UuidGeneratorTest extends TestCase
{
public function testUuidCanBeGenerated()
{
$em = new EntityManager();
$generator = new UuidGenerator();
$uuid = $generator->generate($em, new Entity());
$this->assertInstanceOf(Uuid::class, $uuid);
}
public function testCustomUuidfactory()
{
$uuid = new NilUuid();
$em = new EntityManager();
$factory = $this->createMock(UuidFactory::class);
$factory->expects($this->any())
->method('create')
->willReturn($uuid);
$generator = new UuidGenerator($factory);
$this->assertSame($uuid, $generator->generate($em, new Entity()));
}
public function testUuidfactory()
{
$em = new EntityManager();
$generator = new UuidGenerator();
$this->assertInstanceOf(UuidV6::class, $generator->generate($em, new Entity()));
$generator = $generator->randomBased();
$this->assertInstanceOf(UuidV4::class, $generator->generate($em, new Entity()));
$generator = $generator->timeBased();
$this->assertInstanceOf(UuidV6::class, $generator->generate($em, new Entity()));
$generator = $generator->nameBased('prop1', Uuid::NAMESPACE_OID);
$this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '3'), $generator->generate($em, new Entity()));
$generator = $generator->nameBased('prop2', Uuid::NAMESPACE_OID);
$this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '2'), $generator->generate($em, new Entity()));
$generator = $generator->nameBased('getProp4', Uuid::NAMESPACE_OID);
$this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '4'), $generator->generate($em, new Entity()));
$factory = new UuidFactory(6, 6, 5, 5, null, Uuid::NAMESPACE_OID);
$generator = new UuidGenerator($factory);
$generator = $generator->nameBased('prop1');
$this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '3'), $generator->generate($em, new Entity()));
}
}
class Entity
{
public $prop1 = 1;
public $prop2 = 2;
public function prop1()
{
return 3;
}
public function getProp4()
{
return 4;
}
}

View File

@ -10,6 +10,7 @@ CHANGELOG
* Added the `dispatcher` option to `debug:event-dispatcher`
* Added the `event_dispatcher.dispatcher` tag
* Added `assertResponseFormatSame()` in `BrowserKitAssertionsTrait`
* Add support for configuring UUID factory services
5.2.0
-----

View File

@ -34,6 +34,7 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Validator\Validation;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Symfony\Component\Workflow\WorkflowEvents;
@ -136,6 +137,7 @@ class Configuration implements ConfigurationInterface
$this->addSecretsSection($rootNode);
$this->addNotifierSection($rootNode);
$this->addRateLimiterSection($rootNode);
$this->addUidSection($rootNode);
return $treeBuilder;
}
@ -1891,4 +1893,37 @@ class Configuration implements ConfigurationInterface
->end()
;
}
private function addUidSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('uid')
->info('Uid configuration')
->{class_exists(UuidFactory::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->addDefaultsIfNotSet()
->children()
->enumNode('default_uuid_version')
->defaultValue(6)
->values([6, 4, 1])
->end()
->enumNode('name_based_uuid_version')
->defaultValue(5)
->values([5, 3])
->end()
->scalarNode('name_based_uuid_namespace')
->cannotBeEmpty()
->end()
->enumNode('time_based_uuid_version')
->defaultValue(6)
->values([6, 1])
->end()
->scalarNode('time_based_uuid_node')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
;
}
}

View File

@ -160,6 +160,8 @@ use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
use Symfony\Component\Translation\PseudoLocalizationTranslator;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\ObjectInitializerInterface;
@ -449,6 +451,14 @@ class FrameworkExtension extends Extension
$loader->load('web_link.php');
}
if ($this->isConfigEnabled($container, $config['uid'])) {
if (!class_exists(UuidFactory::class)) {
throw new LogicException('Uid support cannot be enabled as the Uid component is not installed. Try running "composer require symfony/uid".');
}
$this->registerUidConfiguration($config['uid'], $container, $loader);
}
$this->addAnnotatedClassesToCompile([
'**\\Controller\\',
'**\\Entity\\',
@ -2322,6 +2332,27 @@ class FrameworkExtension extends Extension
$container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter');
}
private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
{
$loader->load('uid.php');
$container->getDefinition('uuid.factory')
->setArguments([
$config['default_uuid_version'],
$config['time_based_uuid_version'],
$config['name_based_uuid_version'],
UuidV4::class,
$config['time_based_uuid_node'] ?? null,
$config['name_based_uuid_namespace'] ?? null,
])
;
if (isset($config['name_based_uuid_namespace'])) {
$container->getDefinition('name_based_uuid.factory')
->setArguments([$config['name_based_uuid_namespace']]);
}
}
private function resolveTrustedHeaders(array $headers): int
{
$trustedHeaders = 0;

View File

@ -35,6 +35,7 @@
<xsd:element name="mailer" type="mailer" minOccurs="0" maxOccurs="1" />
<xsd:element name="http-cache" type="http_cache" minOccurs="0" maxOccurs="1" />
<xsd:element name="rate-limiter" type="rate_limiter" minOccurs="0" maxOccurs="1" />
<xsd:element name="uid" type="uid" minOccurs="0" maxOccurs="1" />
</xsd:choice>
<xsd:attribute name="http-method-override" type="xsd:boolean" />
@ -692,4 +693,35 @@
<xsd:attribute name="interval" type="xsd:string" />
<xsd:attribute name="amount" type="xsd:int" />
</xsd:complexType>
<xsd:complexType name="uid">
<xsd:attribute name="enabled" type="xsd:boolean" />
<xsd:attribute name="default_uuid_version" type="default_uuid_version" />
<xsd:attribute name="name_based_uuid_version" type="name_based_uuid_version" />
<xsd:attribute name="time_based_uuid_version" type="time_based_uuid_version" />
<xsd:attribute name="name_based_uuid_namespace" type="xsd:string" />
<xsd:attribute name="time_based_uuid_node" type="xsd:string" />
</xsd:complexType>
<xsd:simpleType name="default_uuid_version">
<xsd:restriction base="xsd:int">
<xsd:enumeration value="6" />
<xsd:enumeration value="4" />
<xsd:enumeration value="1" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="name_based_uuid_version">
<xsd:restriction base="xsd:int">
<xsd:enumeration value="5" />
<xsd:enumeration value="3" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="time_based_uuid_version">
<xsd:restriction base="xsd:int">
<xsd:enumeration value="6" />
<xsd:enumeration value="1" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\Uid\Factory\NameBasedUuidFactory;
use Symfony\Component\Uid\Factory\RandomBasedUuidFactory;
use Symfony\Component\Uid\Factory\TimeBasedUuidFactory;
use Symfony\Component\Uid\Factory\UlidFactory;
use Symfony\Component\Uid\Factory\UuidFactory;
return static function (ContainerConfigurator $container) {
$container->services()
->set('ulid.factory', UlidFactory::class)
->alias(UlidFactory::class, 'ulid.factory')
->set('uuid.factory', UuidFactory::class)
->alias(UuidFactory::class, 'uuid.factory')
->set('name_based_uuid.factory', NameBasedUuidFactory::class)
->factory([service('uuid.factory'), 'nameBased'])
->args([abstract_arg('Please set the "framework.uid.name_based_uuid_namespace" configuration option to use the "name_based_uuid.factory" service')])
->alias(NameBasedUuidFactory::class, 'name_based_uuid.factory')
->set('random_based_uuid.factory', RandomBasedUuidFactory::class)
->factory([service('uuid.factory'), 'randomBased'])
->alias(RandomBasedUuidFactory::class, 'random_based_uuid.factory')
->set('time_based_uuid.factory', TimeBasedUuidFactory::class)
->factory([service('uuid.factory'), 'timeBased'])
->alias(TimeBasedUuidFactory::class, 'time_based_uuid.factory')
;
};

View File

@ -22,6 +22,7 @@ use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\Uid\Factory\UuidFactory;
class ConfigurationTest extends TestCase
{
@ -563,6 +564,12 @@ class ConfigurationTest extends TestCase
'enabled' => false,
'limiters' => [],
],
'uid' => [
'enabled' => class_exists(UuidFactory::class),
'default_uuid_version' => 6,
'name_based_uuid_version' => 5,
'time_based_uuid_version' => 6,
],
];
}
}

View File

@ -119,7 +119,7 @@ class BinaryUtil
/**
* @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
*/
public static function timeToDateTime(string $time): \DateTimeImmutable
public static function hexToDateTime(string $time): \DateTimeImmutable
{
if (\PHP_INT_SIZE >= 8) {
$time = (string) (hexdec($time) - self::TIME_OFFSET_INT);
@ -142,4 +142,34 @@ class BinaryUtil
return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0));
}
/**
* @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
*/
public static function dateTimeToHex(\DateTimeInterface $time): string
{
if (\PHP_INT_SIZE >= 8) {
if (-self::TIME_OFFSET_INT > $time = (int) $time->format('Uu0')) {
throw new \InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.');
}
return str_pad(dechex(self::TIME_OFFSET_INT + $time), 16, '0', \STR_PAD_LEFT);
}
$time = $time->format('Uu0');
$negative = '-' === $time[0];
if ($negative && self::TIME_OFFSET_INT < $time = substr($time, 1)) {
throw new \InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.');
}
$time = self::fromBase($time, self::BASE10);
$time = str_pad($time, 8, "\0", \STR_PAD_LEFT);
if ($negative) {
$time = self::add($time, self::TIME_OFFSET_COM1) ^ "\xff\xff\xff\xff\xff\xff\xff\xff";
} else {
$time = self::add($time, self::TIME_OFFSET_BIN);
}
return bin2hex($time);
}
}

View File

@ -8,6 +8,7 @@ CHANGELOG
* Add `AbstractUid::fromBinary()`, `AbstractUid::fromBase58()`, `AbstractUid::fromBase32()` and `AbstractUid::fromRfc4122()`
* [BC BREAK] Replace `UuidV1::getTime()`, `UuidV6::getTime()` and `Ulid::getTime()` by `UuidV1::getDateTime()`, `UuidV6::getDateTime()` and `Ulid::getDateTime()`
* Add `Uuid::NAMESPACE_*` constants from RFC4122
* Add `UlidFactory`, `UuidFactory`, `RandomBasedUuidFactory`, `TimeBasedUuidFactory` and `NameBasedUuidFactory`
5.2.0
-----

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Factory;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV3;
use Symfony\Component\Uid\UuidV5;
class NameBasedUuidFactory
{
private $class;
private $namespace;
public function __construct(string $class, Uuid $namespace)
{
$this->class = $class;
$this->namespace = $namespace;
}
/**
* @return UuidV5|UuidV3
*/
public function create(string $name): Uuid
{
switch ($class = $this->class) {
case UuidV5::class: return Uuid::v5($this->namespace, $name);
case UuidV3::class: return Uuid::v3($this->namespace, $name);
}
if (is_subclass_of($class, UuidV5::class)) {
$uuid = Uuid::v5($this->namespace, $name);
} else {
$uuid = Uuid::v3($this->namespace, $name);
}
return new $class($uuid);
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Factory;
use Symfony\Component\Uid\UuidV4;
class RandomBasedUuidFactory
{
private $class;
public function __construct(string $class)
{
$this->class = $class;
}
public function create(): UuidV4
{
$class = $this->class;
return new $class();
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Factory;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV6;
class TimeBasedUuidFactory
{
private $class;
private $node;
public function __construct(string $class, Uuid $node = null)
{
$this->class = $class;
$this->node = $node;
}
/**
* @return UuidV6|UuidV1
*/
public function create(\DateTimeInterface $time = null): Uuid
{
$class = $this->class;
if (null === $time && null === $this->node) {
return new $class();
}
return new $class($class::generate($time, $this->node));
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Factory;
use Symfony\Component\Uid\Ulid;
class UlidFactory
{
public function create(\DateTimeInterface $time = null): Ulid
{
return new Ulid(null === $time ? null : Ulid::generate($time));
}
}

View File

@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Factory;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV5;
use Symfony\Component\Uid\UuidV6;
class UuidFactory
{
private $defaultClass;
private $timeBasedClass;
private $nameBasedClass;
private $randomBasedClass;
private $timeBasedNode;
private $nameBasedNamespace;
/**
* @param string|int $defaultClass
* @param string|int $timeBasedClass
* @param string|int $nameBasedClass
* @param string|int $randomBasedClass
* @param Uuid|string|null $timeBasedNode
* @param Uuid|string|null $nameBasedNamespace
*/
public function __construct($defaultClass = UuidV6::class, $timeBasedClass = UuidV6::class, $nameBasedClass = UuidV5::class, $randomBasedClass = UuidV4::class, $timeBasedNode = null, $nameBasedNamespace = null)
{
if (null !== $timeBasedNode && !$timeBasedNode instanceof Uuid) {
$timeBasedNode = Uuid::fromString($timeBasedNode);
}
if (null !== $nameBasedNamespace && !$nameBasedNamespace instanceof Uuid) {
$nameBasedNamespace = Uuid::fromString($nameBasedNamespace);
}
$this->defaultClass = is_numeric($defaultClass) ? Uuid::class.'V'.$defaultClass : $defaultClass;
$this->timeBasedClass = is_numeric($timeBasedClass) ? Uuid::class.'V'.$timeBasedClass : $timeBasedClass;
$this->nameBasedClass = is_numeric($nameBasedClass) ? Uuid::class.'V'.$nameBasedClass : $nameBasedClass;
$this->randomBasedClass = is_numeric($randomBasedClass) ? Uuid::class.'V'.$randomBasedClass : $randomBasedClass;
$this->timeBasedNode = $timeBasedNode;
$this->nameBasedNamespace = $nameBasedNamespace;
}
/**
* @return UuidV6|UuidV4|UuidV1
*/
public function create(): Uuid
{
$class = $this->defaultClass;
return new $class();
}
public function randomBased(): RandomBasedUuidFactory
{
return new RandomBasedUuidFactory($this->randomBasedClass);
}
/**
* @param Uuid|string|null $node
*/
public function timeBased($node = null): TimeBasedUuidFactory
{
$node ?? $node = $this->timeBasedNode;
if (null === $node) {
$class = $this->timeBasedClass;
$node = $this->timeBasedNode = new $class();
} elseif (!$node instanceof Uuid) {
$node = Uuid::fromString($node);
}
return new TimeBasedUuidFactory($this->timeBasedClass, $node);
}
/**
* @param Uuid|string|null $namespace
*/
public function nameBased($namespace = null): NameBasedUuidFactory
{
$namespace ?? $namespace = $this->nameBasedNamespace;
if (null === $namespace) {
throw new \LogicException(sprintf('A namespace should be defined when using "%s()".', __METHOD__));
}
if (!$namespace instanceof Uuid) {
$namespace = Uuid::fromString($namespace);
}
return new NameBasedUuidFactory($this->nameBasedClass, $namespace);
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Tests\Factory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Factory\UlidFactory;
final class UlidFactoryTest extends TestCase
{
public function testCreate()
{
$ulidFactory = new UlidFactory();
$ulidFactory->create();
$ulid1 = $ulidFactory->create(new \DateTime('@999999.123000'));
$this->assertSame('999999.123000', $ulid1->getDateTime()->format('U.u'));
$ulid2 = $ulidFactory->create(new \DateTime('@999999.123000'));
$this->assertSame('999999.123000', $ulid2->getDateTime()->format('U.u'));
$this->assertFalse($ulid1->equals($ulid2));
$this->assertSame(-1, ($ulid1->compare($ulid2)));
$ulid3 = $ulidFactory->create(new \DateTime('@1234.162524'));
$this->assertSame('1234.162000', $ulid3->getDateTime()->format('U.u'));
}
public function testCreateWithInvalidTimestamp()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The timestamp must be positive.');
(new UlidFactory())->create(new \DateTime('@-1000'));
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid\Tests\Factory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\NilUuid;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV3;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV5;
use Symfony\Component\Uid\UuidV6;
final class UuidFactoryTest extends TestCase
{
public function testCreateNamedDefaultVersion()
{
$this->assertInstanceOf(UuidV5::class, (new UuidFactory())->nameBased('6f80c216-0492-4421-bd82-c10ab929ae84')->create('foo'));
$this->assertInstanceOf(UuidV3::class, (new UuidFactory(6, 6, 3))->nameBased('6f80c216-0492-4421-bd82-c10ab929ae84')->create('foo'));
}
public function testCreateNamed()
{
$uuidFactory = new UuidFactory();
// Test custom namespace
$uuid1 = $uuidFactory->nameBased('6f80c216-0492-4421-bd82-c10ab929ae84')->create('foo');
$this->assertInstanceOf(UuidV5::class, $uuid1);
$this->assertSame('d521ceb7-3e31-5954-b873-92992c697ab9', (string) $uuid1);
// Test default namespace override
$uuid2 = $uuidFactory->nameBased(Uuid::v4())->create('foo');
$this->assertFalse($uuid1->equals($uuid2));
// Test version override
$uuidFactory = new UuidFactory(6, 6, 3, 4, new NilUuid(), '6f80c216-0492-4421-bd82-c10ab929ae84');
$uuid3 = $uuidFactory->nameBased()->create('foo');
$this->assertInstanceOf(UuidV3::class, $uuid3);
}
public function testCreateTimedDefaultVersion()
{
$this->assertInstanceOf(UuidV6::class, (new UuidFactory())->timeBased()->create());
$this->assertInstanceOf(UuidV1::class, (new UuidFactory(6, 1))->timeBased()->create());
}
public function testCreateTimed()
{
$uuidFactory = new UuidFactory(6, 6, 5, 4, '6f80c216-0492-4421-bd82-c10ab929ae84');
// Test custom timestamp
$uuid1 = $uuidFactory->timeBased()->create(new \DateTime('@1611076938.057800'));
$this->assertInstanceOf(UuidV6::class, $uuid1);
$this->assertSame('1611076938.057800', $uuid1->getDateTime()->format('U.u'));
$this->assertSame('c10ab929ae84', $uuid1->getNode());
// Test default node override
$uuid2 = $uuidFactory->timeBased('7c1ede70-3586-48ed-a984-23c8018d9174')->create();
$this->assertInstanceOf(UuidV6::class, $uuid2);
$this->assertSame('23c8018d9174', $uuid2->getNode());
// Test version override
$uuid3 = (new UuidFactory(6, 1))->timeBased()->create();
$this->assertInstanceOf(UuidV1::class, $uuid3);
// Test negative timestamp and round
$uuid4 = $uuidFactory->timeBased()->create(new \DateTime('@-12219292800'));
$this->assertSame('-12219292800.000000', $uuid4->getDateTime()->format('U.u'));
}
public function testInvalidCreateTimed()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The given UUID date cannot be earlier than 1582-10-15.');
(new UuidFactory())->timeBased()->create(new \DateTime('@-12219292800.001000'));
}
public function testCreateRandom()
{
$this->assertInstanceOf(UuidV4::class, (new UuidFactory())->randomBased()->create());
}
}

View File

@ -26,7 +26,7 @@ class Ulid extends AbstractUid
public function __construct(string $ulid = null)
{
if (null === $ulid) {
$this->uid = self::generate();
$this->uid = static::generate();
return;
}
@ -124,10 +124,25 @@ class Ulid extends AbstractUid
return \DateTimeImmutable::createFromFormat('U.u', substr_replace($time, '.', -3, 0));
}
private static function generate(): string
public static function generate(\DateTimeInterface $time = null): string
{
$time = microtime(false);
$time = substr($time, 11).substr($time, 2, 3);
if (null === $time) {
return self::doGenerate();
}
if (0 > $time = substr($time->format('Uu'), 0, -3)) {
throw new \InvalidArgumentException('The timestamp must be positive.');
}
return self::doGenerate($time);
}
private static function doGenerate(string $mtime = null): string
{
if (null === $time = $mtime) {
$time = microtime(false);
$time = substr($time, 11).substr($time, 2, 3);
}
if ($time !== self::$time) {
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
@ -139,9 +154,13 @@ class Ulid extends AbstractUid
self::$rand = array_values($r);
self::$time = $time;
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
usleep(100);
if (null === $mtime) {
usleep(100);
} else {
self::$rand = [0, 0, 0, 0];
}
return self::generate();
return self::doGenerate($mtime);
} else {
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
self::$rand[$i] = 0;

View File

@ -31,11 +31,27 @@ class UuidV1 extends Uuid
public function getDateTime(): \DateTimeImmutable
{
return BinaryUtil::timeToDateTime('0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8));
return BinaryUtil::hexToDateTime('0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8));
}
public function getNode(): string
{
return uuid_mac($this->uid);
}
public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string
{
$uuid = uuid_create(static::TYPE);
if (null !== $time) {
$time = BinaryUtil::dateTimeToHex($time);
$uuid = substr($time, 8).'-'.substr($time, 4, 4).'-1'.substr($time, 1, 3).substr($uuid, 18);
}
if ($node) {
$uuid = substr($uuid, 0, 24).substr($node->uid, 24);
}
return $uuid;
}
}

View File

@ -27,22 +27,7 @@ class UuidV6 extends Uuid
public function __construct(string $uuid = null)
{
if (null === $uuid) {
$uuid = uuid_create(\UUID_TYPE_TIME);
$this->uid = substr($uuid, 15, 3).substr($uuid, 9, 4).$uuid[0].'-'.substr($uuid, 1, 4).'-6'.substr($uuid, 5, 3).substr($uuid, 18, 6);
// uuid_create() returns a stable "node" that can leak the MAC of the host, but
// UUIDv6 prefers a truly random number here, let's XOR both to preserve the entropy
if (null === self::$seed) {
self::$seed = [random_int(0, 0xffffff), random_int(0, 0xffffff)];
}
$node = unpack('N2', hex2bin('00'.substr($uuid, 24, 6)).hex2bin('00'.substr($uuid, 30)));
$this->uid .= sprintf('%06x%06x',
(self::$seed[0] ^ $node[1]) | 0x010000,
self::$seed[1] ^ $node[2]
);
$this->uid = static::generate();
} else {
parent::__construct($uuid);
}
@ -50,11 +35,35 @@ class UuidV6 extends Uuid
public function getDateTime(): \DateTimeImmutable
{
return BinaryUtil::timeToDateTime('0'.substr($this->uid, 0, 8).substr($this->uid, 9, 4).substr($this->uid, 15, 3));
return BinaryUtil::hexToDateTime('0'.substr($this->uid, 0, 8).substr($this->uid, 9, 4).substr($this->uid, 15, 3));
}
public function getNode(): string
{
return substr($this->uid, 24);
}
public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string
{
$uuidV1 = UuidV1::generate($time, $node);
$uuid = substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).$uuidV1[0].'-'.substr($uuidV1, 1, 4).'-6'.substr($uuidV1, 5, 3).substr($uuidV1, 18, 6);
if ($node) {
return $uuid.substr($uuidV1, 24);
}
// uuid_create() returns a stable "node" that can leak the MAC of the host, but
// UUIDv6 prefers a truly random number here, let's XOR both to preserve the entropy
if (null === self::$seed) {
self::$seed = [random_int(0, 0xffffff), random_int(0, 0xffffff)];
}
$node = unpack('N2', hex2bin('00'.substr($uuidV1, 24, 6)).hex2bin('00'.substr($uuidV1, 30)));
return $uuid.sprintf('%06x%06x',
(self::$seed[0] ^ $node[1]) | 0x010000,
self::$seed[1] ^ $node[2]
);
}
}