From 88a99ddbdf69c48578b26c20486b6769c0c4b2bb Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Mon, 14 Dec 2020 23:47:04 +0100 Subject: [PATCH] [Uid] Add UuidFactory to create Ulid and Uuid from timestamps, namespaces and nodes --- src/Symfony/Bridge/Doctrine/CHANGELOG.md | 1 + .../Doctrine/IdGenerator/UlidGenerator.php | 12 ++ .../Doctrine/IdGenerator/UuidGenerator.php | 82 ++++++++++++++ .../Tests/IdGenerator/UlidGeneratorTest.php | 19 +++- .../Tests/IdGenerator/UuidGeneratorTest.php | 91 +++++++++++++++ .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 35 ++++++ .../FrameworkExtension.php | 31 ++++++ .../Resources/config/schema/symfony-1.0.xsd | 32 ++++++ .../FrameworkBundle/Resources/config/uid.php | 41 +++++++ .../DependencyInjection/ConfigurationTest.php | 7 ++ src/Symfony/Component/Uid/BinaryUtil.php | 32 +++++- src/Symfony/Component/Uid/CHANGELOG.md | 1 + .../Uid/Factory/NameBasedUuidFactory.php | 47 ++++++++ .../Uid/Factory/RandomBasedUuidFactory.php | 31 ++++++ .../Uid/Factory/TimeBasedUuidFactory.php | 42 +++++++ .../Component/Uid/Factory/UlidFactory.php | 22 ++++ .../Component/Uid/Factory/UuidFactory.php | 104 ++++++++++++++++++ .../Uid/Tests/Factory/UlidFactoryTest.php | 44 ++++++++ .../Uid/Tests/Factory/UuidFactoryTest.php | 93 ++++++++++++++++ src/Symfony/Component/Uid/Ulid.php | 31 +++++- src/Symfony/Component/Uid/UuidV1.php | 18 ++- src/Symfony/Component/Uid/UuidV6.php | 43 +++++--- 23 files changed, 833 insertions(+), 27 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php create mode 100644 src/Symfony/Component/Uid/Factory/NameBasedUuidFactory.php create mode 100644 src/Symfony/Component/Uid/Factory/RandomBasedUuidFactory.php create mode 100644 src/Symfony/Component/Uid/Factory/TimeBasedUuidFactory.php create mode 100644 src/Symfony/Component/Uid/Factory/UlidFactory.php create mode 100644 src/Symfony/Component/Uid/Factory/UuidFactory.php create mode 100644 src/Symfony/Component/Uid/Tests/Factory/UlidFactoryTest.php create mode 100644 src/Symfony/Component/Uid/Tests/Factory/UuidFactoryTest.php 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] + ); + } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy