Skip to content

Commit

Permalink
feature #36066 [Uid] use one class per type of UUID (nicolas-grekas)
Browse files Browse the repository at this point in the history
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
-------

62f6ac4 [Uid] use one class per type of UUID
  • Loading branch information
nicolas-grekas committed Mar 14, 2020
2 parents fa5d636 + 62f6ac4 commit 7dc6da6
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 99 deletions.
27 changes: 27 additions & 0 deletions src/Symfony/Component/Uid/NullUuid.php
@@ -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';
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/Uid/Tests/UlidTest.php
Expand Up @@ -46,7 +46,7 @@ public function testBinary()
$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'))));
}

/**
Expand Down
58 changes: 31 additions & 27 deletions src/Symfony/Component/Uid/Tests/UuidTest.php
Expand Up @@ -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
{
Expand All @@ -24,12 +29,12 @@ public function testConstructorWithInvalidUuid()
$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));
Expand All @@ -39,56 +44,56 @@ public function testV1()
{
$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));
Expand All @@ -99,7 +104,7 @@ public function testEquals()
*/
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()
Expand Down Expand Up @@ -128,12 +133,11 @@ public function testCompare()
$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);
}
}
4 changes: 2 additions & 2 deletions src/Symfony/Component/Uid/Ulid.php
Expand Up @@ -53,10 +53,10 @@ public static function isValid(string $ulid): bool
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);
Expand Down
109 changes: 40 additions & 69 deletions src/Symfony/Component/Uid/Uuid.php
Expand Up @@ -18,74 +18,78 @@
*/
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
{
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.
*/
Expand All @@ -103,39 +107,6 @@ public function compare(self $other): int
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;
Expand Down
59 changes: 59 additions & 0 deletions src/Symfony/Component/Uid/UuidV1.php
@@ -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);
}
}

0 comments on commit 7dc6da6

Please sign in to comment.