feature #39507 [Uid] Add UidFactory to create Ulid and Uuid from timestamps and randomness/nodes (fancyweb)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[Uid] Add UidFactory to create Ulid and Uuid from timestamps and randomness/nodes

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Ref https://github.com/symfony/symfony/pull/36097

When you migrate an existing resource identifier to an uid, you might want to choose the timestamp so that it is coherent with the creation date of the existing resource. (eg: I have a row in a table with id=1, created_at=2018-12-11 19:00:00, I would like to use that timestamp to create the resource Ulid).

I guess it can also be useful to choose the randomness of the Ulid or the node of the Uuid.

From what I understood, v3 and v5 don't need those features, this is why there are not in the factory.

See https://github.com/symfony/symfony/pull/39507#pullrequestreview-584904889 for more details.

Commits
-------

88a99ddbdf [Uid] Add UuidFactory to create Ulid and Uuid from timestamps, namespaces and nodes
This commit is contained in:
Nicolas Grekas 2021-02-11 18:51:13 +01:00
commit 12b9d92b54
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]
);
}
}