diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 771aae12a4f0f..6e866ce2e2c57 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -49,6 +49,8 @@ jobs: git checkout composer.json git checkout -m ${{ github.base_ref }} + # to be removed when psalm adds support for intersection types + sed -i 's/Uuid&/Uuid|/' src/Symfony/Component/Uid/Factory/TimeBasedUuidFactory.php ./vendor/bin/psalm.phar --set-baseline=.github/psalm/psalm.baseline.xml --no-progress git checkout -m FETCH_HEAD diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/UidTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/UidTest.php index 28406b65bd70c..642e363496b3b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/UidTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/UidTest.php @@ -18,9 +18,6 @@ use Symfony\Component\Uid\UuidV4; use Symfony\Component\Uid\UuidV6; -/** - * @see UidController - */ class UidTest extends AbstractWebTestCase { protected function setUp(): void @@ -41,7 +38,7 @@ public function testArgumentValueResolverDisabled() $exception = reset($exceptions); $this->assertInstanceOf(\TypeError::class, $exception); - $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\UidController::anyFormat(): Argument #1 ($userId) must be of type Symfony\Component\Uid\UuidV1, string given', $exception->getMessage()); + $this->assertStringContainsString(UidController::class.'::anyFormat(): Argument #1 ($userId) must be of type Symfony\Component\Uid\UuidV1, string given', $exception->getMessage()); } public function testArgumentValueResolverEnabled() diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index ccf349411d48b..7b5142ba5c7d2 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -4,7 +4,8 @@ CHANGELOG 6.2 --- - * Add `TimeBasedUidInterface` to `Ulid`, `UuidV1`, and `UuidV6` to describe that they present `getDateTime()` as an available method + * Add `TimeBasedUidInterface` to describe UIDs that embed a timestamp + * Add `MaxUuid` and `MaxUlid` 5.4 --- diff --git a/src/Symfony/Component/Uid/Command/InspectUuidCommand.php b/src/Symfony/Component/Uid/Command/InspectUuidCommand.php index 25a7b6fda3025..cc830bc35f47d 100644 --- a/src/Symfony/Component/Uid/Command/InspectUuidCommand.php +++ b/src/Symfony/Component/Uid/Command/InspectUuidCommand.php @@ -19,9 +19,10 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Uid\MaxUuid; +use Symfony\Component\Uid\NilUuid; +use Symfony\Component\Uid\TimeBasedUidInterface; use Symfony\Component\Uid\Uuid; -use Symfony\Component\Uid\UuidV1; -use Symfony\Component\Uid\UuidV6; #[AsCommand(name: 'uuid:inspect', description: 'Inspect a UUID')] class InspectUuidCommand extends Command @@ -56,10 +57,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - if (-1 === $version = uuid_type($uuid)) { + if (new NilUuid() == $uuid) { $version = 'nil'; - } elseif (0 === $version || 2 === $version || 6 < $version) { - $version = 'unknown'; + } elseif (new MaxUuid() == $uuid) { + $version = 'max'; + } else { + $version = uuid_type($uuid); } $rows = [ @@ -70,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['toHex', $uuid->toHex()], ]; - if ($uuid instanceof UuidV1 || $uuid instanceof UuidV6) { + if ($uuid instanceof TimeBasedUidInterface) { $rows[] = new TableSeparator(); $rows[] = ['Time', $uuid->getDateTime()->format('Y-m-d H:i:s.u \U\T\C')]; } diff --git a/src/Symfony/Component/Uid/MaxUlid.php b/src/Symfony/Component/Uid/MaxUlid.php new file mode 100644 index 0000000000000..9cc34576ed4fd --- /dev/null +++ b/src/Symfony/Component/Uid/MaxUlid.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +class MaxUlid extends Ulid +{ + public function __construct() + { + $this->uid = parent::MAX; + } +} diff --git a/src/Symfony/Component/Uid/MaxUuid.php b/src/Symfony/Component/Uid/MaxUuid.php new file mode 100644 index 0000000000000..48eb656e159b9 --- /dev/null +++ b/src/Symfony/Component/Uid/MaxUuid.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +class MaxUuid extends Uuid +{ + protected const TYPE = -1; + + public function __construct() + { + $this->uid = parent::MAX; + } +} diff --git a/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php b/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php index e1d70388e6381..f581b37a7a476 100644 --- a/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php +++ b/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php @@ -55,7 +55,7 @@ public function testUnknown() ----------------------- -------------------------------------- Label Value ----------------------- -------------------------------------- - Version unknown + Version 0 toRfc4122 (canonical) 461cc9b9-2397-0dba-91e9-33af4c63f7ec toBase58 9f9nftX6dw4oVPm5uT17um toBase32 263K4VJ8WQ1PX93T9KNX667XZC @@ -71,7 +71,7 @@ public function testUnknown() ----------------------- -------------------------------------- Label Value ----------------------- -------------------------------------- - Version unknown + Version 2 toRfc4122 (canonical) 461cc9b9-2397-2dba-91e9-33af4c63f7ec toBase58 9f9nftX6fjLfNnvSAHMV7Z toBase32 263K4VJ8WQ5PX93T9KNX667XZC @@ -87,7 +87,7 @@ public function testUnknown() ----------------------- -------------------------------------- Label Value ----------------------- -------------------------------------- - Version unknown + Version 7 toRfc4122 (canonical) 461cc9b9-2397-7dba-91e9-33af4c63f7ec toBase58 9f9nftX6kE2K6HpooNEQ83 toBase32 263K4VJ8WQFPX93T9KNX667XZC @@ -103,7 +103,7 @@ public function testUnknown() ----------------------- -------------------------------------- Label Value ----------------------- -------------------------------------- - Version unknown + Version 12 toRfc4122 (canonical) 461cc9b9-2397-cdba-91e9-33af4c63f7ec toBase58 9f9nftX6pihxonjBST7K8X toBase32 263K4VJ8WQSPX93T9KNX667XZC diff --git a/src/Symfony/Component/Uid/Tests/UlidTest.php b/src/Symfony/Component/Uid/Tests/UlidTest.php index 98ecaac2f9eab..f68922b1e5724 100644 --- a/src/Symfony/Component/Uid/Tests/UlidTest.php +++ b/src/Symfony/Component/Uid/Tests/UlidTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Uid\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Uid\MaxUlid; use Symfony\Component\Uid\NilUlid; use Symfony\Component\Uid\Tests\Fixtures\CustomUlid; use Symfony\Component\Uid\Ulid; @@ -267,4 +268,21 @@ public function testNewNilUlid() { $this->assertSame('00000000000000000000000000', (string) new NilUlid()); } + + /** + * @testWith ["ffffffff-ffff-ffff-ffff-ffffffffffff"] + * ["7zzzzzzzzzzzzzzzzzzzzzzzzz"] + */ + public function testMaxUlid(string $ulid) + { + $ulid = Ulid::fromString($ulid); + + $this->assertInstanceOf(MaxUlid::class, $ulid); + $this->assertSame('7ZZZZZZZZZZZZZZZZZZZZZZZZZ', (string) $ulid); + } + + public function testNewMaxUlid() + { + $this->assertSame('7ZZZZZZZZZZZZZZZZZZZZZZZZZ', (string) new MaxUlid()); + } } diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 104a1ca6bfd91..93c2340ff9375 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Uid\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Uid\MaxUuid; use Symfony\Component\Uid\NilUuid; use Symfony\Component\Uid\Tests\Fixtures\CustomUuid; use Symfony\Component\Uid\Ulid; @@ -238,6 +239,23 @@ public function testNewNilUuid() $this->assertSame('00000000-0000-0000-0000-000000000000', (string) new NilUuid()); } + /** + * @testWith ["ffffffff-ffff-ffff-ffff-ffffffffffff"] + * ["7zzzzzzzzzzzzzzzzzzzzzzzzz"] + */ + public function testMaxUuid(string $uuid) + { + $uuid = Uuid::fromString($uuid); + + $this->assertInstanceOf(MaxUuid::class, $uuid); + $this->assertSame('ffffffff-ffff-ffff-ffff-ffffffffffff', (string) $uuid); + } + + public function testNewMaxUuid() + { + $this->assertSame('ffffffff-ffff-ffff-ffff-ffffffffffff', (string) new MaxUuid()); + } + public function testFromBinary() { $this->assertEquals( diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index 5470a1ad1827f..bfa6599a11734 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -21,6 +21,7 @@ class Ulid extends AbstractUid implements TimeBasedUidInterface { protected const NIL = '00000000000000000000000000'; + protected const MAX = '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'; private static string $time = ''; private static array $rand = []; @@ -29,21 +30,17 @@ public function __construct(string $ulid = null) { if (null === $ulid) { $this->uid = static::generate(); - - return; - } - - if (self::NIL === $ulid) { + } elseif (self::NIL === $ulid) { $this->uid = $ulid; + } elseif (self::MAX === strtr($ulid, 'z', 'Z')) { + $this->uid = $ulid; + } else { + if (!self::isValid($ulid)) { + throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid)); + } - return; - } - - if (!self::isValid($ulid)) { - throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid)); + $this->uid = strtoupper($ulid); } - - $this->uid = strtoupper($ulid); } public static function isValid(string $ulid): bool @@ -68,11 +65,11 @@ public static function fromString(string $ulid): static } if (16 !== \strlen($ulid)) { - if (self::NIL === $ulid) { - return new NilUlid(); - } - - return new static($ulid); + return match (strtr($ulid, 'z', 'Z')) { + self::NIL => new NilUlid(), + self::MAX => new MaxUlid(), + default => new static($ulid), + }; } $ulid = bin2hex($ulid); @@ -90,8 +87,12 @@ public static function fromString(string $ulid): static return new NilUlid(); } + if (self::MAX === $ulid = strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ')) { + return new MaxUlid(); + } + $u = new static(self::NIL); - $u->uid = strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); + $u->uid = $ulid; return $u; } diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index 70f3476595b92..8f763c6588a4f 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -25,6 +25,7 @@ class Uuid extends AbstractUid protected const TYPE = 0; protected const NIL = '00000000-0000-0000-0000-000000000000'; + protected const MAX = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; public function __construct(string $uuid, bool $checkVariant = false) { @@ -68,6 +69,10 @@ public static function fromString(string $uuid): static return new NilUuid(); } + if (self::MAX === $uuid = strtr($uuid, 'F', 'f')) { + return new MaxUuid(); + } + if (!\in_array($uuid[19], ['8', '9', 'a', 'b', 'A', 'B'], true)) { return new self($uuid); }