Skip to content

Commit 311c10b

Browse files
committed
[Uid] Add UidFactory to create Ulid and Uuid from timestamps and randomness/nodes
1 parent 72a82c3 commit 311c10b

File tree

13 files changed

+518
-22
lines changed

13 files changed

+518
-22
lines changed

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

+69
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
3535
use Symfony\Component\Serializer\Serializer;
3636
use Symfony\Component\Translation\Translator;
37+
use Symfony\Component\Uid\AbstractUid;
38+
use Symfony\Component\Uid\UuidV1;
39+
use Symfony\Component\Uid\UuidV3;
40+
use Symfony\Component\Uid\UuidV5;
41+
use Symfony\Component\Uid\UuidV6;
3742
use Symfony\Component\Validator\Validation;
3843
use Symfony\Component\WebLink\HttpHeaderSerializer;
3944
use Symfony\Component\Workflow\WorkflowEvents;
@@ -136,6 +141,7 @@ public function getConfigTreeBuilder()
136141
$this->addSecretsSection($rootNode);
137142
$this->addNotifierSection($rootNode);
138143
$this->addRateLimiterSection($rootNode);
144+
$this->addUidSection($rootNode);
139145

140146
return $treeBuilder;
141147
}
@@ -1891,4 +1897,67 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
18911897
->end()
18921898
;
18931899
}
1900+
1901+
private function addUidSection(ArrayNodeDefinition $rootNode)
1902+
{
1903+
$rootNode
1904+
->children()
1905+
->arrayNode('uid')
1906+
->info('Uid configuration')
1907+
->{class_exists(AbstractUid::class) ? 'canBeDisabled' : 'canBeEnabled'}()
1908+
->children()
1909+
->arrayNode('uuid_factory')
1910+
->addDefaultsIfNotSet()
1911+
->children()
1912+
->enumNode('default_named_version')
1913+
->defaultValue(UuidV5::class)
1914+
->cannotBeEmpty()
1915+
->values([UuidV5::class, UuidV3::class])
1916+
->beforeNormalization()
1917+
->always()
1918+
->then(static function ($value) {
1919+
if ('v5' === $value) {
1920+
return UuidV5::class;
1921+
}
1922+
1923+
if ('v3' === $value) {
1924+
return UuidV3::class;
1925+
}
1926+
1927+
return $value;
1928+
})
1929+
->end()
1930+
->end()
1931+
->enumNode('default_timed_version')
1932+
->defaultValue(UuidV6::class)
1933+
->cannotBeEmpty()
1934+
->values([UuidV6::class, UuidV1::class])
1935+
->beforeNormalization()
1936+
->always()
1937+
->then(static function ($value) {
1938+
if ('v6' === $value) {
1939+
return UuidV6::class;
1940+
}
1941+
1942+
if ('v1' === $value) {
1943+
return UuidV1::class;
1944+
}
1945+
1946+
return $value;
1947+
})
1948+
->end()
1949+
->end()
1950+
->scalarNode('default_namespace')
1951+
->cannotBeEmpty()
1952+
->end()
1953+
->scalarNode('default_node')
1954+
->cannotBeEmpty()
1955+
->end()
1956+
->end()
1957+
->end()
1958+
->end()
1959+
->end()
1960+
->end()
1961+
;
1962+
}
18941963
}

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

+28
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
159159
use Symfony\Component\Translation\PseudoLocalizationTranslator;
160160
use Symfony\Component\Translation\Translator;
161+
use Symfony\Component\Uid\AbstractUid;
161162
use Symfony\Component\Validator\ConstraintValidatorInterface;
162163
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
163164
use Symfony\Component\Validator\ObjectInitializerInterface;
@@ -447,6 +448,14 @@ public function load(array $configs, ContainerBuilder $container)
447448
$loader->load('web_link.php');
448449
}
449450

451+
if ($this->isConfigEnabled($container, $config['uid'])) {
452+
if (!class_exists(AbstractUid::class)) {
453+
throw new LogicException('Uid support cannot be enabled as the Uid component is not installed. Try running "composer require symfony/uid".');
454+
}
455+
456+
$this->registerUidConfiguration($config['uid'], $container, $loader);
457+
}
458+
450459
$this->addAnnotatedClassesToCompile([
451460
'**\\Controller\\',
452461
'**\\Entity\\',
@@ -2312,6 +2321,25 @@ public static function registerRateLimiter(ContainerBuilder $container, string $
23122321
$container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter');
23132322
}
23142323

2324+
private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2325+
{
2326+
$loader->load('uid.php');
2327+
2328+
$uidFactory = $container->getDefinition('uuid.factory');
2329+
$uidFactory->setArguments([
2330+
$config['uuid_factory']['default_named_version'],
2331+
$config['uuid_factory']['default_timed_version'],
2332+
]);
2333+
2334+
if (isset($config['uuid_factory']['default_namespace'])) {
2335+
$uidFactory->addMethodCall('withDefaultNamespace', [$config['uuid_factory']['default_namespace']], true);
2336+
}
2337+
2338+
if (isset($config['uuid_factory']['default_node'])) {
2339+
$uidFactory->addMethodCall('withDefaultNode', [$config['uuid_factory']['default_node']], true);
2340+
}
2341+
}
2342+
23152343
private function resolveTrustedHeaders(array $headers): int
23162344
{
23172345
$trustedHeaders = 0;

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

+27
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,30 @@
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:sequence>
699+
<xsd:element name="uid_factory" type="uid_factory" minOccurs="0" maxOccurs="1" />
700+
</xsd:sequence>
701+
<xsd:attribute name="enabled" type="xsd:boolean" />
702+
</xsd:complexType>
703+
704+
<xsd:complexType name="uid_factory">
705+
<xsd:attribute name="default_named_version" type="uid_named_version" />
706+
<xsd:attribute name="default_timed_version" type="uid_timed_version" />
707+
</xsd:complexType>
708+
709+
<xsd:simpleType name="uid_named_version">
710+
<xsd:restriction base="xsd:string">
711+
<xsd:enumeration value="v5" />
712+
<xsd:enumeration value="v3" />
713+
</xsd:restriction>
714+
</xsd:simpleType>
715+
716+
<xsd:simpleType name="uid_timed_version">
717+
<xsd:restriction base="xsd:string">
718+
<xsd:enumeration value="v6" />
719+
<xsd:enumeration value="v1" />
720+
</xsd:restriction>
721+
</xsd:simpleType>
695722
</xsd:schema>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\UlidFactory;
15+
use Symfony\Component\Uid\UuidFactory;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('uuid.factory', UuidFactory::class)
20+
->alias(UuidFactory::class, 'uuid.factory')
21+
22+
->set('ulid.factory', UlidFactory::class)
23+
->alias(UlidFactory::class, 'ulid.factory')
24+
;
25+
};

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

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Symfony\Component\Mailer\Mailer;
2323
use Symfony\Component\Messenger\MessageBusInterface;
2424
use Symfony\Component\Notifier\Notifier;
25+
use Symfony\Component\Uid\UuidV5;
26+
use Symfony\Component\Uid\UuidV6;
2527

2628
class ConfigurationTest extends TestCase
2729
{
@@ -563,6 +565,13 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
563565
'enabled' => false,
564566
'limiters' => [],
565567
],
568+
'uid' => [
569+
'enabled' => true,
570+
'uuid_factory' => [
571+
'default_named_version' => UuidV5::class,
572+
'default_timed_version' => UuidV6::class,
573+
],
574+
],
566575
];
567576
}
568577
}

src/Symfony/Component/Uid/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `AbstractUid::fromBinary()`, `AbstractUid::fromBase58()`, `AbstractUid::fromBase32()` and `AbstractUid::fromRfc4122()`
8+
* Add `UuidFactory` and `UlidFactory`
89

910
5.2.0
1011
-----
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 <fabien@symfony.com>
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\Uid\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uid\UlidFactory;
16+
17+
final class UlidFactoryTest extends TestCase
18+
{
19+
public function testCreate()
20+
{
21+
$ulidFactory = new UlidFactory();
22+
23+
$ulidFactory->create();
24+
25+
$ulid1 = $ulidFactory->create('999999');
26+
$this->assertSame(999.999, $ulid1->getTime());
27+
$ulid2 = $ulidFactory->create('999999');
28+
$this->assertSame(999.999, $ulid2->getTime());
29+
30+
$this->assertFalse($ulid1->equals($ulid2));
31+
$this->assertSame(-1, ($ulid1->compare($ulid2)));
32+
}
33+
34+
public function testCreateWithInvalidTimestamp()
35+
{
36+
$this->expectException(\InvalidArgumentException::class);
37+
$this->expectExceptionMessage('The timestamp must be a positive number.');
38+
39+
(new UlidFactory())->create('-1000');
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Uid\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uid\UuidFactory;
16+
use Symfony\Component\Uid\Uuid;
17+
use Symfony\Component\Uid\UuidV1;
18+
use Symfony\Component\Uid\UuidV3;
19+
use Symfony\Component\Uid\UuidV4;
20+
use Symfony\Component\Uid\UuidV5;
21+
use Symfony\Component\Uid\UuidV6;
22+
23+
final class UuidFactoryTest extends TestCase
24+
{
25+
public function testCreateNamedDefaultVersion()
26+
{
27+
$this->assertInstanceOf(UuidV5::class, (new UuidFactory())->createNamed('foo'));
28+
$this->assertInstanceOf(UuidV3::class, (new UuidFactory(UuidV3::class))->createNamed('foo'));
29+
}
30+
31+
public function testCreateNamed()
32+
{
33+
$uuidFactory = new UuidFactory();
34+
35+
// Test custom namespace
36+
$uuid1 = $uuidFactory->createNamed('foo', Uuid::fromString('6f80c216-0492-4421-bd82-c10ab929ae84'));
37+
$this->assertInstanceOf(UuidV5::class, $uuid1);
38+
$this->assertSame('d521ceb7-3e31-5954-b873-92992c697ab9', (string) $uuid1);
39+
40+
// Test default namespace
41+
$uuidFactory = $uuidFactory->withDefaultNamespace(Uuid::fromString('6f80c216-0492-4421-bd82-c10ab929ae84'));
42+
$uuid2 = $uuidFactory->createNamed('foo');
43+
$this->assertInstanceOf(UuidV5::class, $uuid2);
44+
$this->assertTrue($uuid1->equals($uuid2));
45+
46+
// Test default namespace override
47+
$uuid3 = $uuidFactory->createNamed('foo', Uuid::v4());
48+
$this->assertFalse($uuid2->equals($uuid3));
49+
50+
// Test version override
51+
$uuid4 = $uuidFactory->createNamed('foo', null, UuidV3::class);
52+
$this->assertInstanceOf(UuidV3::class, $uuid4);
53+
}
54+
55+
public function testCreateNamedWithUnsupportedVersion()
56+
{
57+
$this->expectException(\InvalidArgumentException::class);
58+
$this->expectExceptionMessage('The "Symfony\Component\Uid\UuidV1" version is unsupported.');
59+
60+
(new UuidFactory())->createNamed('foo', null, UuidV1::class);
61+
}
62+
63+
public function testCreateTimedDefaultVersion()
64+
{
65+
$this->assertInstanceOf(UuidV6::class, (new UuidFactory())->createTimed());
66+
$this->assertInstanceOf(UuidV1::class, (new UuidFactory(null, UuidV1::class))->createTimed());
67+
}
68+
69+
public function testCreateTimed()
70+
{
71+
$uuidFactory = new UuidFactory();
72+
73+
// Test custom timestamp and node
74+
$uuid1 = $uuidFactory->createTimed('138303697380560000', Uuid::fromString('6f80c216-0492-4421-bd82-c10ab929ae84'));
75+
$this->assertInstanceOf(UuidV6::class, $uuid1);
76+
$this->assertSame(1611076938.056, $uuid1->getTime());
77+
$this->assertSame('c10ab929ae84', $uuid1->getNode());
78+
79+
// Test default node
80+
$uuidFactory = $uuidFactory->withDefaultNode(Uuid::fromString('6f80c216-0492-4421-bd82-c10ab929ae84'));
81+
$uuid2 = $uuidFactory->createTimed();
82+
$this->assertInstanceOf(UuidV6::class, $uuid2);
83+
$this->assertSame('c10ab929ae84', $uuid2->getNode());
84+
85+
// Test default node override
86+
$uuid3 = $uuidFactory->createTimed(null, Uuid::fromString('7c1ede70-3586-48ed-a984-23c8018d9174'));
87+
$this->assertInstanceOf(UuidV6::class, $uuid3);
88+
$this->assertSame('23c8018d9174', $uuid3->getNode());
89+
90+
// Test version override
91+
$uuid4 = $uuidFactory->createTimed(null, null, UuidV1::class);
92+
$this->assertInstanceOf(UuidV1::class, $uuid4);
93+
$this->assertSame($uuid2->getNode(), $uuid4->getNode());
94+
}
95+
96+
public function testCreateTimedWithUnsupportedVersion()
97+
{
98+
$this->expectException(\InvalidArgumentException::class);
99+
$this->expectExceptionMessage('The "Symfony\Component\Uid\UuidV4" version is unsupported.');
100+
101+
(new UuidFactory())->createTimed(null, null, UuidV4::class);
102+
}
103+
104+
public function testCreateRandom()
105+
{
106+
$this->assertInstanceOf(UuidV4::class, (new UuidFactory())->createRandom());
107+
}
108+
}

0 commit comments

Comments
 (0)