[Uid] use one class per type of UUID

This commit is contained in:
Nicolas Grekas 2020-03-13 21:22:20 +01:00
parent fa5d63685d
commit 62f6ac4d36
9 changed files with 245 additions and 99 deletions

View 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';
}
}

View File

@ -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'))));
}
/**

View File

@ -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);
}
}

View File

@ -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);

View File

@ -18,62 +18,71 @@ 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);
}
if (__CLASS__ !== static::class) {
return new static($uuid);
}
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);
}
throw new \InvalidArgumentException(sprintf('Invalid UUID: "%s".', $uuid));
}
public static function v3(self $uuidNamespace, string $name): self
final public static function v1(): UuidV1
{
return new self(uuid_generate_md5($uuidNamespace->uuid, $name));
return new UuidV1();
}
public static function v4(): self
final public static function v3(self $namespace, string $name): UuidV3
{
return new self(uuid_create(self::TYPE_4));
return new UuidV3(uuid_generate_md5($namespace->uuid, $name));
}
public static function v5(self $uuidNamespace, string $name): self
final public static function v4(): UuidV4
{
return new self(uuid_generate_sha1($uuidNamespace->uuid, $name));
return new UuidV4();
}
public static function fromBinary(string $uuidAsBinary): self
final public static function v5(self $namespace, string $name): UuidV5
{
return new self(uuid_unparse($uuidAsBinary));
return new UuidV5(uuid_generate_sha1($namespace->uuid, $name));
}
public static function isValid(string $uuid): bool
{
return uuid_is_valid($uuid);
if (__CLASS__ === static::class) {
return uuid_is_valid($uuid);
}
return static::TYPE === uuid_type($uuid);
}
public function toBinary(): string
@ -81,11 +90,6 @@ class Uuid implements \JsonSerializable
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;

View 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);
}
}

View 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;
}

View 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);
}
}
}

View 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;
}