diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index a4e14666587e..ea2909036fae 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -43,6 +43,13 @@ class Str */ protected static $studlyCache = []; + /** + * The callback that should be used to generate ULIDs. + * + * @var callable|null + */ + protected static $ulidFactory; + /** * The callback that should be used to generate UUIDs. * @@ -1543,6 +1550,10 @@ public static function createUuidsNormally() */ public static function ulid($time = null) { + if (static::$ulidFactory) { + return call_user_func(static::$ulidFactory); + } + if ($time === null) { return new Ulid(); } @@ -1550,6 +1561,84 @@ public static function ulid($time = null) return new Ulid(Ulid::generate($time)); } + /** + * Indicate that ULIDs should be created normally and not using a custom factory. + * + * @return void + */ + public static function createUlidsNormally() + { + static::$ulidFactory = null; + } + + /** + * Set the callable that will be used to generate ULIDs. + * + * @param callable|null $factory + * @return void + */ + public static function createUlidsUsing(callable $factory = null) + { + static::$ulidFactory = $factory; + } + + /** + * Set the sequence that will be used to generate ULIDs. + * + * @param array $sequence + * @param callable|null $whenMissing + * @return void + */ + public static function createUlidsUsingSequence(array $sequence, $whenMissing = null) + { + $next = 0; + + $whenMissing ??= function () use (&$next) { + $factoryCache = static::$ulidFactory; + + static::$ulidFactory = null; + + $ulid = static::ulid(); + + static::$ulidFactory = $factoryCache; + + $next++; + + return $ulid; + }; + + static::createUlidsUsing(function () use (&$next, $sequence, $whenMissing) { + if (array_key_exists($next, $sequence)) { + return $sequence[$next++]; + } + + return $whenMissing(); + }); + } + + /** + * Always return the same ULID when generating new ULIDs. + * + * @param Closure|null $callback + * @return Ulid + */ + public static function freezeUlids(Closure $callback = null) + { + $ulid = Str::ulid(); + + Str::createUlidsUsing(fn() => $ulid); + + if ($callback !== null) { + try { + $callback($ulid); + } finally { + Str::createUlidsNormally(); + } + } + + return $ulid; + } + /** * Remove all strings from the casing caches. * diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 0377762dc1ed..da612dddc7d6 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -1155,6 +1155,111 @@ public function testItCanSpecifyAFallbackForASequence() } } + public function testItCanFreezeUlids() + { + $this->assertNotSame((string) Str::ulid(), (string) Str::ulid()); + $this->assertNotSame(Str::ulid(), Str::ulid()); + + $ulid = Str::freezeUlids(); + + $this->assertSame($ulid, Str::ulid()); + $this->assertSame(Str::ulid(), Str::ulid()); + $this->assertSame((string) $ulid, (string) Str::ulid()); + $this->assertSame((string) Str::ulid(), (string) Str::ulid()); + + Str::createUlidsNormally(); + + $this->assertNotSame(Str::ulid(), Str::ulid()); + $this->assertNotSame((string) Str::ulid(), (string) Str::ulid()); + } + + public function testItCanFreezeUlidsInAClosure() + { + $ulids = []; + + $ulid = Str::freezeUlids(function ($ulid) use (&$ulids) { + $ulids[] = $ulid; + $ulids[] = Str::ulid(); + $ulids[] = Str::ulid(); + }); + + $this->assertSame($ulid, $ulids[0]); + $this->assertSame((string) $ulid, (string) $ulids[0]); + $this->assertSame((string) $ulids[0], (string) $ulids[1]); + $this->assertSame($ulids[0], $ulids[1]); + $this->assertSame((string) $ulids[0], (string) $ulids[1]); + $this->assertSame($ulids[1], $ulids[2]); + $this->assertSame((string) $ulids[1], (string) $ulids[2]); + $this->assertNotSame(Str::ulid(), Str::ulid()); + $this->assertNotSame((string) Str::ulid(), (string) Str::ulid()); + + Str::createUlidsNormally(); + } + + public function testItCreatesUlidsNormallyAfterFailureWithinFreezeMethod() + { + try { + Str::freezeUlids(function () { + Str::createUlidsUsing(fn() => Str::of('1234')); + $this->assertSame('1234', (string) Str::ulid()); + throw new \Exception('Something failed'); + }); + } catch (\Exception) { + $this->assertNotSame('1234', (string) Str::ulid()); + } + } + + public function testItCanSpecifyASequenceOfUlidsToUtilise() + { + Str::createUlidsUsingSequence([ + 0 => ($zeroth = Str::ulid()), + 1 => ($first = Str::ulid()), + // just generate a random one here... + 3 => ($third = Str::ulid()), + // continue to generate random ulids... + ]); + + $retrieved = Str::ulid(); + $this->assertSame($zeroth, $retrieved); + $this->assertSame((string) $zeroth, (string) $retrieved); + + $retrieved = Str::ulid(); + $this->assertSame($first, $retrieved); + $this->assertSame((string) $first, (string) $retrieved); + + $retrieved = Str::ulid(); + $this->assertFalse(in_array($retrieved, [$zeroth, $first, $third], true)); + $this->assertFalse(in_array((string) $retrieved, [(string) $zeroth, (string) $first, (string) $third], true)); + + $retrieved = Str::ulid(); + $this->assertSame($third, $retrieved); + $this->assertSame((string) $third, (string) $retrieved); + + $retrieved = Str::ulid(); + $this->assertFalse(in_array($retrieved, [$zeroth, $first, $third], true)); + $this->assertFalse(in_array((string) $retrieved, [(string) $zeroth, (string) $first, (string) $third], true)); + + Str::createUlidsNormally(); + } + + public function testItCanSpecifyAFallbackForAUlidSequence() + { + Str::createUlidsUsingSequence( + [Str::ulid(), Str::ulid()], + fn() => throw new Exception('Out of Ulids'), + ); + Str::ulid(); + Str::ulid(); + + try { + $this->expectExceptionMessage('Out of Ulids'); + Str::ulid(); + $this->fail(); + } finally { + Str::createUlidsNormally(); + } + } + public function testPasswordCreation() { $this->assertTrue(strlen(Str::password()) === 32);