diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md
index 84d3fc55cf199..7afda2ec2d167 100644
--- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md
+++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
* Deprecate `DoctrineTestHelper` and `TestRepositoryFactory`
* [BC BREAK] Remove `UuidV*Generator` classes
+ * Add `UuidGenerator`
5.2.0
-----
diff --git a/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php
index 8953038860fb8..b3923d11c051a 100644
--- a/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php
+++ b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php
@@ -13,12 +13,24 @@
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();
}
}
diff --git a/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php b/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php
new file mode 100644
index 0000000000000..272989a834ab7
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php
@@ -0,0 +1,82 @@
+
+ *
+ * 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;
+ }
+}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php
index c4373554e2b6b..957ac0f60aeb0 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php
@@ -14,7 +14,7 @@
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 @@ public function testUlidCanBeGenerated()
$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()));
+ }
}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php
new file mode 100644
index 0000000000000..bfca276a811ba
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php
@@ -0,0 +1,91 @@
+
+ *
+ * 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;
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index 7dd9ccbdb335d..61edb7de0e37b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -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
-----
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index fbe47b6a0b061..f5e2678e4038f 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -34,6 +34,7 @@
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 @@ public function getConfigTreeBuilder()
$this->addSecretsSection($rootNode);
$this->addNotifierSection($rootNode);
$this->addRateLimiterSection($rootNode);
+ $this->addUidSection($rootNode);
return $treeBuilder;
}
@@ -1891,4 +1893,37 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
->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()
+ ;
+ }
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 12478fadd5d8e..ac528cbc3c29b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -160,6 +160,8 @@
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 @@ public function load(array $configs, ContainerBuilder $container)
$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 @@ public static function registerRateLimiter(ContainerBuilder $container, string $
$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;
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index c92fd3cd3b2b9..05675e19b3180 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -35,6 +35,7 @@
+
@@ -692,4 +693,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php
new file mode 100644
index 0000000000000..840fb97b5f5f5
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php
@@ -0,0 +1,41 @@
+
+ *
+ * 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')
+ ;
+};
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index 3a4af4b800435..73dcb2c4baa28 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -22,6 +22,7 @@
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_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'enabled' => false,
'limiters' => [],
],
+ 'uid' => [
+ 'enabled' => class_exists(UuidFactory::class),
+ 'default_uuid_version' => 6,
+ 'name_based_uuid_version' => 5,
+ 'time_based_uuid_version' => 6,
+ ],
];
}
}
diff --git a/src/Symfony/Component/Uid/BinaryUtil.php b/src/Symfony/Component/Uid/BinaryUtil.php
index f9ef273d48384..131976021560f 100644
--- a/src/Symfony/Component/Uid/BinaryUtil.php
+++ b/src/Symfony/Component/Uid/BinaryUtil.php
@@ -119,7 +119,7 @@ public static function add(string $a, string $b): string
/**
* @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 @@ public static function timeToDateTime(string $time): \DateTimeImmutable
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);
+ }
}
diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md
index 209851585dd5f..1acafc0e32770 100644
--- a/src/Symfony/Component/Uid/CHANGELOG.md
+++ b/src/Symfony/Component/Uid/CHANGELOG.md
@@ -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
-----
diff --git a/src/Symfony/Component/Uid/Factory/NameBasedUuidFactory.php b/src/Symfony/Component/Uid/Factory/NameBasedUuidFactory.php
new file mode 100644
index 0000000000000..cbf080bc0b52d
--- /dev/null
+++ b/src/Symfony/Component/Uid/Factory/NameBasedUuidFactory.php
@@ -0,0 +1,47 @@
+
+ *
+ * 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);
+ }
+}
diff --git a/src/Symfony/Component/Uid/Factory/RandomBasedUuidFactory.php b/src/Symfony/Component/Uid/Factory/RandomBasedUuidFactory.php
new file mode 100644
index 0000000000000..83ab61fbe048d
--- /dev/null
+++ b/src/Symfony/Component/Uid/Factory/RandomBasedUuidFactory.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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();
+ }
+}
diff --git a/src/Symfony/Component/Uid/Factory/TimeBasedUuidFactory.php b/src/Symfony/Component/Uid/Factory/TimeBasedUuidFactory.php
new file mode 100644
index 0000000000000..4337dbb303fa7
--- /dev/null
+++ b/src/Symfony/Component/Uid/Factory/TimeBasedUuidFactory.php
@@ -0,0 +1,42 @@
+
+ *
+ * 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));
+ }
+}
diff --git a/src/Symfony/Component/Uid/Factory/UlidFactory.php b/src/Symfony/Component/Uid/Factory/UlidFactory.php
new file mode 100644
index 0000000000000..40cb7837178a9
--- /dev/null
+++ b/src/Symfony/Component/Uid/Factory/UlidFactory.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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));
+ }
+}
diff --git a/src/Symfony/Component/Uid/Factory/UuidFactory.php b/src/Symfony/Component/Uid/Factory/UuidFactory.php
new file mode 100644
index 0000000000000..edf64672dafb8
--- /dev/null
+++ b/src/Symfony/Component/Uid/Factory/UuidFactory.php
@@ -0,0 +1,104 @@
+
+ *
+ * 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);
+ }
+}
diff --git a/src/Symfony/Component/Uid/Tests/Factory/UlidFactoryTest.php b/src/Symfony/Component/Uid/Tests/Factory/UlidFactoryTest.php
new file mode 100644
index 0000000000000..195c2466d72b3
--- /dev/null
+++ b/src/Symfony/Component/Uid/Tests/Factory/UlidFactoryTest.php
@@ -0,0 +1,44 @@
+
+ *
+ * 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'));
+ }
+}
diff --git a/src/Symfony/Component/Uid/Tests/Factory/UuidFactoryTest.php b/src/Symfony/Component/Uid/Tests/Factory/UuidFactoryTest.php
new file mode 100644
index 0000000000000..a6a05fade23ff
--- /dev/null
+++ b/src/Symfony/Component/Uid/Tests/Factory/UuidFactoryTest.php
@@ -0,0 +1,93 @@
+
+ *
+ * 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());
+ }
+}
diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php
index c07362738dd44..69a782d438e90 100644
--- a/src/Symfony/Component/Uid/Ulid.php
+++ b/src/Symfony/Component/Uid/Ulid.php
@@ -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 @@ public function getDateTime(): \DateTimeImmutable
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 @@ private static function generate(): string
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;
diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php
index 1e14e7a49715a..7621de5212146 100644
--- a/src/Symfony/Component/Uid/UuidV1.php
+++ b/src/Symfony/Component/Uid/UuidV1.php
@@ -31,11 +31,27 @@ public function __construct(string $uuid = null)
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;
+ }
}
diff --git a/src/Symfony/Component/Uid/UuidV6.php b/src/Symfony/Component/Uid/UuidV6.php
index f4b6c362c3957..cf231e20f2d11 100644
--- a/src/Symfony/Component/Uid/UuidV6.php
+++ b/src/Symfony/Component/Uid/UuidV6.php
@@ -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 @@ public function __construct(string $uuid = null)
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]
+ );
+ }
}