diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 84d3fc55cf..7afda2ec2d 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 8953038860..b3923d11c0 100644 --- a/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php +++ b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php @@ -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(); } } diff --git a/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php b/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php new file mode 100644 index 0000000000..272989a834 --- /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 c4373554e2..957ac0f60a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php @@ -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())); + } } 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 0000000000..bfca276a81 --- /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 7dd9ccbdb3..61edb7de0e 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 fbe47b6a0b..f5e2678e40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -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() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 12478fadd5..ac528cbc3c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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; 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 c92fd3cd3b..05675e19b3 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 0000000000..840fb97b5f --- /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 3a4af4b800..73dcb2c4ba 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\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, + ], ]; } } diff --git a/src/Symfony/Component/Uid/BinaryUtil.php b/src/Symfony/Component/Uid/BinaryUtil.php index f9ef273d48..1319760215 100644 --- a/src/Symfony/Component/Uid/BinaryUtil.php +++ b/src/Symfony/Component/Uid/BinaryUtil.php @@ -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); + } } diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index 209851585d..1acafc0e32 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 0000000000..cbf080bc0b --- /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 0000000000..83ab61fbe0 --- /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 0000000000..4337dbb303 --- /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 0000000000..40cb783717 --- /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 0000000000..edf64672da --- /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 0000000000..195c2466d7 --- /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 0000000000..a6a05fade2 --- /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 c07362738d..69a782d438 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 @@ 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; diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php index 1e14e7a497..7621de5212 100644 --- a/src/Symfony/Component/Uid/UuidV1.php +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -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; + } } diff --git a/src/Symfony/Component/Uid/UuidV6.php b/src/Symfony/Component/Uid/UuidV6.php index f4b6c362c3..cf231e20f2 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 @@ 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] + ); + } }