Skip to content

Commit 680b5a1

Browse files
fancywebnicolas-grekas
authored andcommitted
[Uid] Add UuidFactory to create Ulid and Uuid from timestamps, namespaces and nodes
1 parent 47da664 commit 680b5a1

File tree

21 files changed

+715
-25
lines changed

21 files changed

+715
-25
lines changed

src/Symfony/Bridge/Doctrine/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Deprecate `DoctrineTestHelper` and `TestRepositoryFactory`
88
* [BC BREAK] Remove `UuidV*Generator` classes
9+
* Add `UuidGenerator`
910

1011
5.2.0
1112
-----

src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,23 @@
1414
use Doctrine\ORM\EntityManager;
1515
use Doctrine\ORM\Id\AbstractIdGenerator;
1616
use Symfony\Component\Uid\Ulid;
17+
use Symfony\Component\Uid\Factory\UlidFactory;
1718

1819
final class UlidGenerator extends AbstractIdGenerator
1920
{
21+
private $factory;
22+
23+
public function __construct(UlidFactory $factory = null)
24+
{
25+
$this->factory = $factory;
26+
}
27+
2028
public function generate(EntityManager $em, $entity): Ulid
2129
{
30+
if ($this->factory) {
31+
return $this->factory->create();
32+
}
33+
2234
return new Ulid();
2335
}
2436
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\IdGenerator;
13+
14+
use Doctrine\ORM\EntityManager;
15+
use Doctrine\ORM\Id\AbstractIdGenerator;
16+
use Symfony\Component\Uid\Factory\UuidFactory;
17+
use Symfony\Component\Uid\UuidV6;
18+
19+
final class UuidGenerator extends AbstractIdGenerator
20+
{
21+
private $protoFactory;
22+
private $factory;
23+
private $entityGetter;
24+
25+
public function __construct(UuidFactory $factory = null)
26+
{
27+
$this->protoFactory = $this->factory = $factory ?? new UuidFactory();
28+
}
29+
30+
public function generate(EntityManager $em, $entity): Uuid
31+
{
32+
if (null !== $this->entityGetter) {
33+
return $this->factory->create($entity->{$this->entityGetter}());
34+
}
35+
36+
return $this->factory->create();
37+
}
38+
39+
/**
40+
* @param Uuid|string|null $node
41+
*
42+
* @return static
43+
*/
44+
public function nameBased(string $entityGetter, $namespace = null): self
45+
{
46+
$clone = clone $this;
47+
$clone->factory = $clone->protoFactory->nameBased($namespace);
48+
$clone->entityGetter = $entityGetter;
49+
50+
return $clone;
51+
}
52+
53+
/**
54+
* @return static
55+
*/
56+
public function randomBased(): self
57+
{
58+
$clone = clone $this;
59+
$clone->factory = $clone->protoFactory->randomBased();
60+
$clone->entityGetter = null;
61+
62+
return $clone;
63+
}
64+
65+
/**
66+
* @param Uuid|string|null $node
67+
*
68+
* @return static
69+
*/
70+
public function timeBased($node = null): self
71+
{
72+
$clone = clone $this;
73+
$clone->factory = $clone->protoFactory->timeBased($node);
74+
$clone->entityGetter = null;
75+
76+
return $clone;
77+
}
78+
}

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Added the `dispatcher` option to `debug:event-dispatcher`
1111
* Added the `event_dispatcher.dispatcher` tag
1212
* Added `assertResponseFormatSame()` in `BrowserKitAssertionsTrait`
13+
* Add support for configuring UUID factory services
1314

1415
5.2.0
1516
-----

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
3535
use Symfony\Component\Serializer\Serializer;
3636
use Symfony\Component\Translation\Translator;
37+
use Symfony\Component\Uid\Factory\UuidFactory;
3738
use Symfony\Component\Validator\Validation;
3839
use Symfony\Component\WebLink\HttpHeaderSerializer;
3940
use Symfony\Component\Workflow\WorkflowEvents;
@@ -136,6 +137,7 @@ public function getConfigTreeBuilder()
136137
$this->addSecretsSection($rootNode);
137138
$this->addNotifierSection($rootNode);
138139
$this->addRateLimiterSection($rootNode);
140+
$this->addUidSection($rootNode);
139141

140142
return $treeBuilder;
141143
}
@@ -1891,4 +1893,37 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
18911893
->end()
18921894
;
18931895
}
1896+
1897+
private function addUidSection(ArrayNodeDefinition $rootNode)
1898+
{
1899+
$rootNode
1900+
->children()
1901+
->arrayNode('uid')
1902+
->info('Uid configuration')
1903+
->{class_exists(UuidFactory::class) ? 'canBeDisabled' : 'canBeEnabled'}()
1904+
->addDefaultsIfNotSet()
1905+
->children()
1906+
->enumNode('default_uuid_version')
1907+
->defaultValue(6)
1908+
->values([6, 4, 1])
1909+
->end()
1910+
->enumNode('name_based_uuid_version')
1911+
->defaultValue(5)
1912+
->values([5, 3])
1913+
->end()
1914+
->scalarNode('name_based_uuid_namespace')
1915+
->cannotBeEmpty()
1916+
->end()
1917+
->enumNode('time_based_uuid_version')
1918+
->defaultValue(6)
1919+
->values([6, 1])
1920+
->end()
1921+
->scalarNode('time_based_uuid_node')
1922+
->cannotBeEmpty()
1923+
->end()
1924+
->end()
1925+
->end()
1926+
->end()
1927+
;
1928+
}
18941929
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@
159159
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
160160
use Symfony\Component\Translation\PseudoLocalizationTranslator;
161161
use Symfony\Component\Translation\Translator;
162+
use Symfony\Component\Uid\Factory\UuidFactory;
163+
use Symfony\Component\Uid\UuidV4;
162164
use Symfony\Component\Validator\ConstraintValidatorInterface;
163165
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
164166
use Symfony\Component\Validator\ObjectInitializerInterface;
@@ -448,6 +450,14 @@ public function load(array $configs, ContainerBuilder $container)
448450
$loader->load('web_link.php');
449451
}
450452

453+
if ($this->isConfigEnabled($container, $config['uid'])) {
454+
if (!class_exists(UuidFactory::class)) {
455+
throw new LogicException('Uid support cannot be enabled as the Uid component is not installed. Try running "composer require symfony/uid".');
456+
}
457+
458+
$this->registerUidConfiguration($config['uid'], $container, $loader);
459+
}
460+
451461
$this->addAnnotatedClassesToCompile([
452462
'**\\Controller\\',
453463
'**\\Entity\\',
@@ -2315,6 +2325,27 @@ public static function registerRateLimiter(ContainerBuilder $container, string $
23152325
$container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter');
23162326
}
23172327

2328+
private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2329+
{
2330+
$loader->load('uid.php');
2331+
2332+
$container->getDefinition('uuid.factory')
2333+
->setArguments([
2334+
$config['default_uuid_version'],
2335+
$config['time_based_uuid_version'],
2336+
$config['name_based_uuid_version'],
2337+
UuidV4::class,
2338+
$config['time_based_uuid_node'],
2339+
$config['name_based_uuid_namespace'],
2340+
])
2341+
;
2342+
2343+
if (isset($config['name_based_uuid_namespace'])) {
2344+
$container->getDefinition('name_based_uuid.factory')
2345+
->setArguments([$config['name_based_uuid_namespace']]);
2346+
}
2347+
}
2348+
23182349
private function resolveTrustedHeaders(array $headers): int
23192350
{
23202351
$trustedHeaders = 0;

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<xsd:element name="mailer" type="mailer" minOccurs="0" maxOccurs="1" />
3636
<xsd:element name="http-cache" type="http_cache" minOccurs="0" maxOccurs="1" />
3737
<xsd:element name="rate-limiter" type="rate_limiter" minOccurs="0" maxOccurs="1" />
38+
<xsd:element name="uid" type="uid" minOccurs="0" maxOccurs="1" />
3839
</xsd:choice>
3940

4041
<xsd:attribute name="http-method-override" type="xsd:boolean" />
@@ -692,4 +693,26 @@
692693
<xsd:attribute name="interval" type="xsd:string" />
693694
<xsd:attribute name="amount" type="xsd:int" />
694695
</xsd:complexType>
696+
697+
<xsd:complexType name="uid">
698+
<xsd:attribute name="enabled" type="xsd:boolean" />
699+
<xsd:attribute name="name_based_uuid_version" type="name_based_uuid_version" />
700+
<xsd:attribute name="time_based_uuid_version" type="time_based_uuid_version" />
701+
<xsd:attribute name="name_based_uuid_namespace" type="xsd:string" />
702+
<xsd:attribute name="time_based_uuid_node" type="xsd:string" />
703+
</xsd:complexType>
704+
705+
<xsd:simpleType name="name_based_uuid_version">
706+
<xsd:restriction base="xsd:int">
707+
<xsd:enumeration value="5" />
708+
<xsd:enumeration value="3" />
709+
</xsd:restriction>
710+
</xsd:simpleType>
711+
712+
<xsd:simpleType name="time_based_uuid_version">
713+
<xsd:restriction base="xsd:int">
714+
<xsd:enumeration value="6" />
715+
<xsd:enumeration value="1" />
716+
</xsd:restriction>
717+
</xsd:simpleType>
695718
</xsd:schema>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\Uid\Factory\NameBasedUuidFactory;
15+
use Symfony\Component\Uid\Factory\RandomBasedUuidFactory;
16+
use Symfony\Component\Uid\Factory\TimeBasedUuidFactory;
17+
use Symfony\Component\Uid\Factory\UlidFactory;
18+
use Symfony\Component\Uid\Factory\UuidFactory;
19+
20+
return static function (ContainerConfigurator $container) {
21+
$container->services()
22+
->set('ulid.factory', UlidFactory::class)
23+
->alias(UlidFactory::class, 'ulid.factory')
24+
25+
->set('uuid.factory', UuidFactory::class)
26+
->alias(UuidFactory::class, 'uuid.factory')
27+
28+
->set('name_based_uuid.factory', NameBasedUuidFactory::class)
29+
->factory([service('uuid.factory'), 'nameBased'])
30+
->args([abstract_arg('Please set the "framework.uid.name_based_uuid_namespace" configuration option to use the "name_based_uuid.factory" service')])
31+
->alias(NameBasedUuidFactory::class, 'name_based_uuid.factory')
32+
33+
->set('random_based_uuid.factory', RandomBasedUuidFactory::class)
34+
->factory([service('uuid.factory'), 'randomBased'])
35+
->alias(RandomBasedUuidFactory::class, 'random_based_uuid.factory')
36+
37+
->set('time_based_uuid.factory', TimeBasedUuidFactory::class)
38+
->factory([service('uuid.factory'), 'timeBased'])
39+
->alias(TimeBasedUuidFactory::class, 'time_based_uuid.factory')
40+
;
41+
};

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Mailer\Mailer;
2323
use Symfony\Component\Messenger\MessageBusInterface;
2424
use Symfony\Component\Notifier\Notifier;
25+
use Symfony\Component\Uid\Factory\UuidFactory;
2526

2627
class ConfigurationTest extends TestCase
2728
{
@@ -563,6 +564,12 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
563564
'enabled' => false,
564565
'limiters' => [],
565566
],
567+
'uid' => [
568+
'enabled' => class_exists(UuidFactory::class),
569+
'default_uuid_version' => 6,
570+
'name_based_uuid_version' => 5,
571+
'time_based_uuid_version' => 6,
572+
],
566573
];
567574
}
568575
}

src/Symfony/Component/Uid/BinaryUtil.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public static function add(string $a, string $b): string
119119
/**
120120
* @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
121121
*/
122-
public static function timeToDateTime(string $time): \DateTimeImmutable
122+
public static function hexToDateTime(string $time): \DateTimeImmutable
123123
{
124124
if (\PHP_INT_SIZE >= 8) {
125125
$time = (string) (hexdec($time) - self::TIME_OFFSET_INT);
@@ -142,4 +142,34 @@ public static function timeToDateTime(string $time): \DateTimeImmutable
142142

143143
return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0));
144144
}
145+
146+
/**
147+
* @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
148+
*/
149+
public static function dateTimeToHex(\DateTimeInterface $time): string
150+
{
151+
if (\PHP_INT_SIZE >= 8) {
152+
if (-self::TIME_OFFSET_INT > $time = (int) $time->format('Uu0')) {
153+
throw new \InvalidArgumentException('The provided UUID timestamp must be higher than 1582-10-15.');
154+
}
155+
156+
return str_pad(dechex(self::TIME_OFFSET_INT + $time), 16, '0', \STR_PAD_LEFT);
157+
}
158+
159+
$time = $time->format('Uu0');
160+
$negative = '-' === $time[0];
161+
if ($negative && self::TIME_OFFSET_INT < $time = substr($time, 1)) {
162+
throw new \InvalidArgumentException('The provided UUID timestamp must be higher than 1582-10-15.');
163+
}
164+
$time = self::fromBase($time, self::BASE10);
165+
$time = str_pad($time, 8, "\0", \STR_PAD_LEFT);
166+
167+
if ($negative) {
168+
$time = self::add($time, self::TIME_OFFSET_COM1) ^ "\xff\xff\xff\xff\xff\xff\xff\xff";
169+
} else {
170+
$time = self::add($time, self::TIME_OFFSET_BIN);
171+
}
172+
173+
return bin2hex($time);
174+
}
145175
}

0 commit comments

Comments
 (0)