feature #36066 [Uid] use one class per type of UUID (nicolas-grekas)
This PR was merged into the 5.1-dev branch.
Discussion
----------
[Uid] use one class per type of UUID
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Tickets | -
| License | MIT
| Doc PR | -
(embeds #36064 for now)
Would it make sense to have one class per type of UUID?
This aligns the type system and UUID types, so that one could type hint e.g. `UuidV4 $uuid`.
This PR does so. `UuidV1`/2/3/4 and `NullUuid` all extend the base `Uuid` class, which provides common methods and the factories needed to create each king of UUID.
This means we don't need the `getType()` nor the `isNull()` methods since they can be replaced by instanceof checks.
As expected, `getTime()` and `getMac()` then now exist only on the `UuidV1` class - no need for any version check nor any `LogicException` anymore.
Each type is guaranteed to contain a UUID that matches its class' type. The base `Uuid` class is used for the "no type" type.
Commits
-------
62f6ac4d36
[Uid] use one class per type of UUID
This commit is contained in:
commit
7dc6da6c26
27
src/Symfony/Component/Uid/NullUuid.php
Normal file
27
src/Symfony/Component/Uid/NullUuid.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Uid;
|
||||
|
||||
/**
|
||||
* @experimental in 5.1
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class NullUuid extends Uuid
|
||||
{
|
||||
protected const TYPE = UUID_TYPE_NULL;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->uuid = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ class UlidTest extends TestCase
|
||||
$ulid = new Ulid('3zzzzzzzzzzzzzzzzzzzzzzzzz');
|
||||
$this->assertSame('7fffffffffffffffffffffffffffffff', bin2hex($ulid->toBinary()));
|
||||
|
||||
$this->assertTrue($ulid->equals(Ulid::fromBinary(hex2bin('7fffffffffffffffffffffffffffffff'))));
|
||||
$this->assertTrue($ulid->equals(Ulid::fromString(hex2bin('7fffffffffffffffffffffffffffffff'))));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,12 @@
|
||||
namespace Symfony\Tests\Component\Uid;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Uid\NullUuid;
|
||||
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;
|
||||
|
||||
class UuidTest extends TestCase
|
||||
{
|
||||
@ -24,12 +29,12 @@ class UuidTest extends TestCase
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Invalid UUID: "this is not a uuid".');
|
||||
|
||||
new Uuid('this is not a uuid');
|
||||
Uuid::fromString('this is not a uuid');
|
||||
}
|
||||
|
||||
public function testConstructorWithValidUuid()
|
||||
{
|
||||
$uuid = new Uuid(self::A_UUID_V4);
|
||||
$uuid = new UuidV4(self::A_UUID_V4);
|
||||
|
||||
$this->assertSame(self::A_UUID_V4, (string) $uuid);
|
||||
$this->assertSame('"'.self::A_UUID_V4.'"', json_encode($uuid));
|
||||
@ -39,56 +44,56 @@ class UuidTest extends TestCase
|
||||
{
|
||||
$uuid = Uuid::v1();
|
||||
|
||||
$this->assertSame(Uuid::TYPE_1, $uuid->getType());
|
||||
$this->assertInstanceOf(UuidV1::class, $uuid);
|
||||
|
||||
$uuid = new UuidV1(self::A_UUID_V1);
|
||||
|
||||
$this->assertSame(1583245966.746458, $uuid->getTime());
|
||||
$this->assertSame('3499710062d0', $uuid->getNode());
|
||||
}
|
||||
|
||||
public function testV3()
|
||||
{
|
||||
$uuid = Uuid::v3(new Uuid(self::A_UUID_V4), 'the name');
|
||||
$uuid = Uuid::v3(new UuidV4(self::A_UUID_V4), 'the name');
|
||||
|
||||
$this->assertSame(Uuid::TYPE_3, $uuid->getType());
|
||||
$this->assertInstanceOf(UuidV3::class, $uuid);
|
||||
}
|
||||
|
||||
public function testV4()
|
||||
{
|
||||
$uuid = Uuid::v4();
|
||||
|
||||
$this->assertSame(Uuid::TYPE_4, $uuid->getType());
|
||||
$this->assertInstanceOf(UuidV4::class, $uuid);
|
||||
}
|
||||
|
||||
public function testV5()
|
||||
{
|
||||
$uuid = Uuid::v5(new Uuid(self::A_UUID_V4), 'the name');
|
||||
$uuid = Uuid::v5(new UuidV4(self::A_UUID_V4), 'the name');
|
||||
|
||||
$this->assertSame(Uuid::TYPE_5, $uuid->getType());
|
||||
$this->assertInstanceOf(UuidV5::class, $uuid);
|
||||
}
|
||||
|
||||
public function testBinary()
|
||||
{
|
||||
$uuid = new Uuid(self::A_UUID_V4);
|
||||
$uuid = new UuidV4(self::A_UUID_V4);
|
||||
$uuid = Uuid::fromString($uuid->toBinary());
|
||||
|
||||
$this->assertSame(self::A_UUID_V4, (string) Uuid::fromBinary($uuid->toBinary()));
|
||||
$this->assertInstanceOf(UuidV4::class, $uuid);
|
||||
$this->assertSame(self::A_UUID_V4, (string) $uuid);
|
||||
}
|
||||
|
||||
public function testIsValid()
|
||||
{
|
||||
$this->assertFalse(Uuid::isValid('not a uuid'));
|
||||
$this->assertTrue(Uuid::isValid(self::A_UUID_V4));
|
||||
}
|
||||
|
||||
public function testIsNull()
|
||||
{
|
||||
$uuid = new Uuid(self::A_UUID_V1);
|
||||
$this->assertFalse($uuid->isNull());
|
||||
|
||||
$uuid = new Uuid('00000000-0000-0000-0000-000000000000');
|
||||
$this->assertTrue($uuid->isNull());
|
||||
$this->assertFalse(UuidV4::isValid(self::A_UUID_V1));
|
||||
$this->assertTrue(UuidV4::isValid(self::A_UUID_V4));
|
||||
}
|
||||
|
||||
public function testEquals()
|
||||
{
|
||||
$uuid1 = new Uuid(self::A_UUID_V1);
|
||||
$uuid2 = new Uuid(self::A_UUID_V4);
|
||||
$uuid1 = new UuidV1(self::A_UUID_V1);
|
||||
$uuid2 = new UuidV4(self::A_UUID_V4);
|
||||
|
||||
$this->assertTrue($uuid1->equals($uuid1));
|
||||
$this->assertFalse($uuid1->equals($uuid2));
|
||||
@ -99,7 +104,7 @@ class UuidTest extends TestCase
|
||||
*/
|
||||
public function testEqualsAgainstOtherType($other)
|
||||
{
|
||||
$this->assertFalse((new Uuid(self::A_UUID_V4))->equals($other));
|
||||
$this->assertFalse((new UuidV4(self::A_UUID_V4))->equals($other));
|
||||
}
|
||||
|
||||
public function provideInvalidEqualType()
|
||||
@ -128,12 +133,11 @@ class UuidTest extends TestCase
|
||||
$this->assertSame([$a, $b, $c, $d], $uuids);
|
||||
}
|
||||
|
||||
public function testExtraMethods()
|
||||
public function testNullUuid()
|
||||
{
|
||||
$uuid = new Uuid(self::A_UUID_V1);
|
||||
$uuid = Uuid::fromString('00000000-0000-0000-0000-000000000000');
|
||||
|
||||
$this->assertSame(1583245966.746458, $uuid->getTime());
|
||||
$this->assertSame('3499710062d0', $uuid->getMac());
|
||||
$this->assertSame(self::A_UUID_V1, (string) $uuid);
|
||||
$this->assertInstanceOf(NullUuid::class, $uuid);
|
||||
$this->assertSame('00000000-0000-0000-0000-000000000000', (string) $uuid);
|
||||
}
|
||||
}
|
||||
|
@ -53,10 +53,10 @@ class Ulid implements \JsonSerializable
|
||||
return $ulid[0] <= '7';
|
||||
}
|
||||
|
||||
public static function fromBinary(string $ulid): self
|
||||
public static function fromString(string $ulid): self
|
||||
{
|
||||
if (16 !== \strlen($ulid)) {
|
||||
throw new \InvalidArgumentException('Invalid binary ULID.');
|
||||
return new static($ulid);
|
||||
}
|
||||
|
||||
$ulid = bin2hex($ulid);
|
||||
|
@ -18,74 +18,78 @@ namespace Symfony\Component\Uid;
|
||||
*/
|
||||
class Uuid implements \JsonSerializable
|
||||
{
|
||||
public const TYPE_1 = UUID_TYPE_TIME;
|
||||
public const TYPE_3 = UUID_TYPE_MD5;
|
||||
public const TYPE_4 = UUID_TYPE_RANDOM;
|
||||
public const TYPE_5 = UUID_TYPE_SHA1;
|
||||
protected const TYPE = UUID_TYPE_DEFAULT;
|
||||
|
||||
// https://tools.ietf.org/html/rfc4122#section-4.1.4
|
||||
// 0x01b21dd213814000 is the number of 100-ns intervals between the
|
||||
// UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
|
||||
private const TIME_OFFSET_INT = 0x01b21dd213814000;
|
||||
private const TIME_OFFSET_COM = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00";
|
||||
protected $uuid;
|
||||
|
||||
private $uuid;
|
||||
|
||||
public function __construct(string $uuid = null)
|
||||
public function __construct(string $uuid)
|
||||
{
|
||||
if (null === $uuid) {
|
||||
$this->uuid = uuid_create(self::TYPE_4);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uuid_is_valid($uuid)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid UUID: "%s".', $uuid));
|
||||
if (static::TYPE !== uuid_type($uuid)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid));
|
||||
}
|
||||
|
||||
$this->uuid = strtr($uuid, 'ABCDEF', 'abcdef');
|
||||
}
|
||||
|
||||
public static function v1(): self
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function fromString(string $uuid): self
|
||||
{
|
||||
return new self(uuid_create(self::TYPE_1));
|
||||
if (16 === \strlen($uuid)) {
|
||||
$uuid = uuid_unparse($uuid);
|
||||
}
|
||||
|
||||
public static function v3(self $uuidNamespace, string $name): self
|
||||
{
|
||||
return new self(uuid_generate_md5($uuidNamespace->uuid, $name));
|
||||
if (__CLASS__ !== static::class) {
|
||||
return new static($uuid);
|
||||
}
|
||||
|
||||
public static function v4(): self
|
||||
{
|
||||
return new self(uuid_create(self::TYPE_4));
|
||||
switch (uuid_type($uuid)) {
|
||||
case UuidV1::TYPE: return new UuidV1($uuid);
|
||||
case UuidV3::TYPE: return new UuidV3($uuid);
|
||||
case UuidV4::TYPE: return new UuidV4($uuid);
|
||||
case UuidV5::TYPE: return new UuidV5($uuid);
|
||||
case NullUuid::TYPE: return new NullUuid();
|
||||
case self::TYPE: return new self($uuid);
|
||||
}
|
||||
|
||||
public static function v5(self $uuidNamespace, string $name): self
|
||||
{
|
||||
return new self(uuid_generate_sha1($uuidNamespace->uuid, $name));
|
||||
throw new \InvalidArgumentException(sprintf('Invalid UUID: "%s".', $uuid));
|
||||
}
|
||||
|
||||
public static function fromBinary(string $uuidAsBinary): self
|
||||
final public static function v1(): UuidV1
|
||||
{
|
||||
return new self(uuid_unparse($uuidAsBinary));
|
||||
return new UuidV1();
|
||||
}
|
||||
|
||||
final public static function v3(self $namespace, string $name): UuidV3
|
||||
{
|
||||
return new UuidV3(uuid_generate_md5($namespace->uuid, $name));
|
||||
}
|
||||
|
||||
final public static function v4(): UuidV4
|
||||
{
|
||||
return new UuidV4();
|
||||
}
|
||||
|
||||
final public static function v5(self $namespace, string $name): UuidV5
|
||||
{
|
||||
return new UuidV5(uuid_generate_sha1($namespace->uuid, $name));
|
||||
}
|
||||
|
||||
public static function isValid(string $uuid): bool
|
||||
{
|
||||
if (__CLASS__ === static::class) {
|
||||
return uuid_is_valid($uuid);
|
||||
}
|
||||
|
||||
return static::TYPE === uuid_type($uuid);
|
||||
}
|
||||
|
||||
public function toBinary(): string
|
||||
{
|
||||
return uuid_parse($this->uuid);
|
||||
}
|
||||
|
||||
public function isNull(): bool
|
||||
{
|
||||
return uuid_is_null($this->uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the argument is of class Uuid and contains the same value as the current instance.
|
||||
*/
|
||||
@ -103,39 +107,6 @@ class Uuid implements \JsonSerializable
|
||||
return uuid_compare($this->uuid, $other->uuid);
|
||||
}
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return uuid_type($this->uuid);
|
||||
}
|
||||
|
||||
public function getTime(): float
|
||||
{
|
||||
if (self::TYPE_1 !== $t = uuid_type($this->uuid)) {
|
||||
throw new \LogicException("UUID of type $t doesn't contain a time.");
|
||||
}
|
||||
|
||||
$time = '0'.substr($this->uuid, 15, 3).substr($this->uuid, 9, 4).substr($this->uuid, 0, 8);
|
||||
|
||||
if (\PHP_INT_SIZE >= 8) {
|
||||
return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000;
|
||||
}
|
||||
|
||||
$time = str_pad(hex2bin($time), 8, "\0", STR_PAD_LEFT);
|
||||
$time = BinaryUtil::add($time, self::TIME_OFFSET_COM);
|
||||
$time[0] = $time[0] & "\x7F";
|
||||
|
||||
return BinaryUtil::toBase($time, BinaryUtil::BASE10) / 10000000;
|
||||
}
|
||||
|
||||
public function getMac(): string
|
||||
{
|
||||
if (self::TYPE_1 !== $t = uuid_type($this->uuid)) {
|
||||
throw new \LogicException("UUID of type $t doesn't contain a MAC.");
|
||||
}
|
||||
|
||||
return uuid_mac($this->uuid);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->uuid;
|
||||
|
59
src/Symfony/Component/Uid/UuidV1.php
Normal file
59
src/Symfony/Component/Uid/UuidV1.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Uid;
|
||||
|
||||
/**
|
||||
* A v1 UUID contains a 60-bit timestamp and ~60 extra unique bits.
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class UuidV1 extends Uuid
|
||||
{
|
||||
protected const TYPE = UUID_TYPE_TIME;
|
||||
|
||||
// https://tools.ietf.org/html/rfc4122#section-4.1.4
|
||||
// 0x01b21dd213814000 is the number of 100-ns intervals between the
|
||||
// UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
|
||||
private const TIME_OFFSET_INT = 0x01b21dd213814000;
|
||||
private const TIME_OFFSET_COM = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00";
|
||||
|
||||
public function __construct(string $uuid = null)
|
||||
{
|
||||
if (null === $uuid) {
|
||||
$this->uuid = uuid_create(static::TYPE);
|
||||
} else {
|
||||
parent::__construct($uuid);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTime(): float
|
||||
{
|
||||
$time = '0'.substr($this->uuid, 15, 3).substr($this->uuid, 9, 4).substr($this->uuid, 0, 8);
|
||||
|
||||
if (\PHP_INT_SIZE >= 8) {
|
||||
return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000;
|
||||
}
|
||||
|
||||
$time = str_pad(hex2bin($time), 8, "\0", STR_PAD_LEFT);
|
||||
$time = BinaryUtil::add($time, self::TIME_OFFSET_COM);
|
||||
$time[0] = $time[0] & "\x7F";
|
||||
|
||||
return BinaryUtil::toBase($time, BinaryUtil::BASE10) / 10000000;
|
||||
}
|
||||
|
||||
public function getNode(): string
|
||||
{
|
||||
return uuid_mac($this->uuid);
|
||||
}
|
||||
}
|
26
src/Symfony/Component/Uid/UuidV3.php
Normal file
26
src/Symfony/Component/Uid/UuidV3.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Uid;
|
||||
|
||||
/**
|
||||
* A v3 UUID contains an MD5 hash of another UUID and a name.
|
||||
*
|
||||
* Use Uuid::v3() to compute one.
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class UuidV3 extends Uuid
|
||||
{
|
||||
protected const TYPE = UUID_TYPE_MD5;
|
||||
}
|
33
src/Symfony/Component/Uid/UuidV4.php
Normal file
33
src/Symfony/Component/Uid/UuidV4.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Uid;
|
||||
|
||||
/**
|
||||
* A v4 UUID contains a 122-bit random number.
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class UuidV4 extends Uuid
|
||||
{
|
||||
protected const TYPE = UUID_TYPE_RANDOM;
|
||||
|
||||
public function __construct(string $uuid = null)
|
||||
{
|
||||
if (null === $uuid) {
|
||||
$this->uuid = uuid_create(static::TYPE);
|
||||
} else {
|
||||
parent::__construct($uuid);
|
||||
}
|
||||
}
|
||||
}
|
26
src/Symfony/Component/Uid/UuidV5.php
Normal file
26
src/Symfony/Component/Uid/UuidV5.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Uid;
|
||||
|
||||
/**
|
||||
* A v5 UUID contains a SHA1 hash of another UUID and a name.
|
||||
*
|
||||
* Use Uuid::v5() to compute one.
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class UuidV5 extends Uuid
|
||||
{
|
||||
protected const TYPE = UUID_TYPE_SHA1;
|
||||
}
|
Reference in New Issue
Block a user