diff --git a/src/Symfony/Component/Uid/NullUuid.php b/src/Symfony/Component/Uid/NullUuid.php new file mode 100644 index 0000000000..308b4efad0 --- /dev/null +++ b/src/Symfony/Component/Uid/NullUuid.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +class NullUuid extends Uuid +{ + protected const TYPE = UUID_TYPE_NULL; + + public function __construct() + { + $this->uuid = '00000000-0000-0000-0000-000000000000'; + } +} diff --git a/src/Symfony/Component/Uid/Tests/UlidTest.php b/src/Symfony/Component/Uid/Tests/UlidTest.php index c609998d94..4558ec85e9 100644 --- a/src/Symfony/Component/Uid/Tests/UlidTest.php +++ b/src/Symfony/Component/Uid/Tests/UlidTest.php @@ -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')))); } /** diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 5ee55cf5ce..f7f99b29a4 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -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); } } diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index a09918f0ea..1036602418 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -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); diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index d6ae0f1d7d..2e0856b314 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -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; diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php new file mode 100644 index 0000000000..d4bf2c023a --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -0,0 +1,59 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Uid/UuidV3.php b/src/Symfony/Component/Uid/UuidV3.php new file mode 100644 index 0000000000..cfdf3e48fc --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV3.php @@ -0,0 +1,26 @@ + + * + * 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 + */ +class UuidV3 extends Uuid +{ + protected const TYPE = UUID_TYPE_MD5; +} diff --git a/src/Symfony/Component/Uid/UuidV4.php b/src/Symfony/Component/Uid/UuidV4.php new file mode 100644 index 0000000000..4d7b71e1d2 --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV4.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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); + } + } +} diff --git a/src/Symfony/Component/Uid/UuidV5.php b/src/Symfony/Component/Uid/UuidV5.php new file mode 100644 index 0000000000..a36f2c94c2 --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV5.php @@ -0,0 +1,26 @@ + + * + * 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 + */ +class UuidV5 extends Uuid +{ + protected const TYPE = UUID_TYPE_SHA1; +}