From c0f0b589f4f8b42e2527b747e3138be82af193a4 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:40:06 +0000 Subject: [PATCH 01/19] feat(session): add BackedEnum support for session keys Enable using BackedEnums as session keys across all session methods, leveraging the Str::from() and Str::fromAll() helpers for normalization. Methods updated: - get, put, pull, remove, forget - exists, missing, has, hasAny - remember, push, increment, decrement - flash, now - only, except - hasOldInput, getOldInput Includes 41 new tests covering single enums, arrays of enums, mixed arrays (enums + strings), and int-backed enum support. --- src/session/src/Contracts/Session.php | 15 +- src/session/src/Store.php | 53 +- tests/Session/SessionStoreBackedEnumTest.php | 608 +++++++++++++++++++ 3 files changed, 649 insertions(+), 27 deletions(-) create mode 100644 tests/Session/SessionStoreBackedEnumTest.php diff --git a/src/session/src/Contracts/Session.php b/src/session/src/Contracts/Session.php index 4717c1d53..51b0058e3 100644 --- a/src/session/src/Contracts/Session.php +++ b/src/session/src/Contracts/Session.php @@ -4,6 +4,7 @@ namespace Hypervel\Session\Contracts; +use BackedEnum; use SessionHandlerInterface; interface Session @@ -46,27 +47,27 @@ public function all(): array; /** * Checks if a key exists. */ - public function exists(array|string $key): bool; + public function exists(array|BackedEnum|string $key): bool; /** * Checks if a key is present and not null. */ - public function has(array|string $key): bool; + public function has(array|BackedEnum|string $key): bool; /** * Get an item from the session. */ - public function get(string $key, mixed $default = null): mixed; + public function get(BackedEnum|string $key, mixed $default = null): mixed; /** * Get the value of a given key and then forget it. */ - public function pull(string $key, mixed $default = null): mixed; + public function pull(BackedEnum|string $key, mixed $default = null): mixed; /** * Put a key / value pair or array of key / value pairs in the session. */ - public function put(array|string $key, mixed $value = null): void; + public function put(array|BackedEnum|string $key, mixed $value = null): void; /** * Get the CSRF token value. @@ -81,12 +82,12 @@ public function regenerateToken(): void; /** * Remove an item from the session, returning its value. */ - public function remove(string $key): mixed; + public function remove(BackedEnum|string $key): mixed; /** * Remove one or many items from the session. */ - public function forget(array|string $keys): void; + public function forget(array|BackedEnum|string $keys): void; /** * Remove all of the items from the session. diff --git a/src/session/src/Store.php b/src/session/src/Store.php index 10c7107e7..daf852599 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -4,15 +4,16 @@ namespace Hypervel\Session; +use BackedEnum; use Closure; use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; use Hyperf\Context\Context; use Hyperf\Macroable\Macroable; -use Hyperf\Stringable\Str; use Hyperf\Support\MessageBag; use Hyperf\ViewEngine\ViewErrorBag; use Hypervel\Session\Contracts\Session; +use Hypervel\Support\Str; use SessionHandlerInterface; use stdClass; @@ -203,6 +204,7 @@ public function all(): array */ public function only(array $keys): array { + $keys = Str::fromAll($keys); $attributes = $this->getAttributes(); return Arr::only($attributes, $keys); @@ -213,6 +215,7 @@ public function only(array $keys): array */ public function except(array $keys): array { + $keys = Str::fromAll($keys); $attributes = $this->getAttributes(); return Arr::except($attributes, $keys); @@ -221,7 +224,7 @@ public function except(array $keys): array /** * Checks if a key exists. */ - public function exists(array|string $key): bool + public function exists(array|BackedEnum|string $key): bool { $placeholder = new stdClass(); @@ -233,7 +236,7 @@ public function exists(array|string $key): bool /** * Determine if the given key is missing from the session data. */ - public function missing(array|string $key): bool + public function missing(array|BackedEnum|string $key): bool { return ! $this->exists($key); } @@ -241,7 +244,7 @@ public function missing(array|string $key): bool /** * Determine if a key is present and not null. */ - public function has(array|string $key): bool + public function has(array|BackedEnum|string $key): bool { return ! (new Collection(is_array($key) ? $key : func_get_args()))->contains(function ($key) { return is_null($this->get($key)); @@ -251,7 +254,7 @@ public function has(array|string $key): bool /** * Determine if any of the given keys are present and not null. */ - public function hasAny(array|string $key): bool + public function hasAny(array|BackedEnum|string $key): bool { return (new Collection(is_array($key) ? $key : func_get_args()))->filter(function ($key) { return ! is_null($this->get($key)); @@ -261,8 +264,9 @@ public function hasAny(array|string $key): bool /** * Get an item from the session. */ - public function get(string $key, mixed $default = null): mixed + public function get(BackedEnum|string $key, mixed $default = null): mixed { + $key = Str::from($key); $attributes = $this->getAttributes(); return Arr::get($attributes, $key, $default); @@ -271,8 +275,9 @@ public function get(string $key, mixed $default = null): mixed /** * Get the value of a given key and then forget it. */ - public function pull(string $key, mixed $default = null): mixed + public function pull(BackedEnum|string $key, mixed $default = null): mixed { + $key = Str::from($key); $attributes = $this->getAttributes(); $result = Arr::pull($attributes, $key, $default); @@ -284,7 +289,7 @@ public function pull(string $key, mixed $default = null): mixed /** * Determine if the session contains old input. */ - public function hasOldInput(?string $key = null): bool + public function hasOldInput(BackedEnum|string|null $key = null): bool { $old = $this->getOldInput($key); @@ -294,8 +299,10 @@ public function hasOldInput(?string $key = null): bool /** * Get the requested item from the flashed input array. */ - public function getOldInput(?string $key = null, mixed $default = null): mixed + public function getOldInput(BackedEnum|string|null $key = null, mixed $default = null): mixed { + $key = $key === null ? null : Str::from($key); + return Arr::get($this->get('_old_input', []), $key, $default); } @@ -310,15 +317,15 @@ public function replace(array $attributes): void /** * Put a key / value pair or array of key / value pairs in the session. */ - public function put(array|string $key, mixed $value = null): void + public function put(array|BackedEnum|string $key, mixed $value = null): void { if (! is_array($key)) { - $key = [$key => $value]; + $key = [Str::from($key) => $value]; } $attributes = $this->getAttributes(); foreach ($key as $arrayKey => $arrayValue) { - Arr::set($attributes, $arrayKey, $arrayValue); + Arr::set($attributes, Str::from($arrayKey), $arrayValue); } $this->setAttributes($attributes); @@ -327,7 +334,7 @@ public function put(array|string $key, mixed $value = null): void /** * Get an item from the session, or store the default value. */ - public function remember(string $key, Closure $callback): mixed + public function remember(BackedEnum|string $key, Closure $callback): mixed { if (! is_null($value = $this->get($key))) { return $value; @@ -341,7 +348,7 @@ public function remember(string $key, Closure $callback): mixed /** * Push a value onto a session array. */ - public function push(string $key, mixed $value): void + public function push(BackedEnum|string $key, mixed $value): void { $array = $this->get($key, []); @@ -353,7 +360,7 @@ public function push(string $key, mixed $value): void /** * Increment the value of an item in the session. */ - public function increment(string $key, int $amount = 1): mixed + public function increment(BackedEnum|string $key, int $amount = 1): mixed { $this->put($key, $value = $this->get($key, 0) + $amount); @@ -363,7 +370,7 @@ public function increment(string $key, int $amount = 1): mixed /** * Decrement the value of an item in the session. */ - public function decrement(string $key, int $amount = 1): int + public function decrement(BackedEnum|string $key, int $amount = 1): int { return $this->increment($key, $amount * -1); } @@ -371,8 +378,10 @@ public function decrement(string $key, int $amount = 1): int /** * Flash a key / value pair to the session. */ - public function flash(string $key, mixed $value = true): void + public function flash(BackedEnum|string $key, mixed $value = true): void { + $key = Str::from($key); + $this->put($key, $value); $this->push('_flash.new', $key); @@ -383,8 +392,10 @@ public function flash(string $key, mixed $value = true): void /** * Flash a key / value pair to the session for immediate use. */ - public function now(string $key, mixed $value): void + public function now(BackedEnum|string $key, mixed $value): void { + $key = Str::from($key); + $this->put($key, $value); $this->push('_flash.old', $key); @@ -441,8 +452,9 @@ public function flashInput(array $value): void /** * Remove an item from the session, returning its value. */ - public function remove(string $key): mixed + public function remove(BackedEnum|string $key): mixed { + $key = Str::from($key); $attributes = $this->getAttributes(); $result = Arr::pull($attributes, $key); @@ -454,8 +466,9 @@ public function remove(string $key): mixed /** * Remove one or many items from the session. */ - public function forget(array|string $keys): void + public function forget(array|BackedEnum|string $keys): void { + $keys = is_array($keys) ? Str::fromAll($keys) : Str::from($keys); $attributes = $this->getAttributes(); Arr::forget($attributes, $keys); diff --git a/tests/Session/SessionStoreBackedEnumTest.php b/tests/Session/SessionStoreBackedEnumTest.php new file mode 100644 index 000000000..8b4d9511c --- /dev/null +++ b/tests/Session/SessionStoreBackedEnumTest.php @@ -0,0 +1,608 @@ +getSession(); + $session->put('user', 'john'); + + $this->assertSame('john', $session->get(SessionKey::User)); + } + + public function testGetWithIntBackedEnum(): void + { + $session = $this->getSession(); + $session->put('1', 'first-value'); + + $this->assertSame('first-value', $session->get(IntBackedKey::First)); + } + + public function testGetWithEnumReturnsDefault(): void + { + $session = $this->getSession(); + + $this->assertSame('default', $session->get(SessionKey::User, 'default')); + } + + // ========================================================================= + // put() tests + // ========================================================================= + + public function testPutWithSingleEnum(): void + { + $session = $this->getSession(); + $session->put(SessionKey::User, 'jane'); + + $this->assertSame('jane', $session->get('user')); + $this->assertSame('jane', $session->get(SessionKey::User)); + } + + public function testPutWithArrayOfStringKeys(): void + { + $session = $this->getSession(); + $session->put([ + SessionKey::User->value => 'john', + SessionKey::Token->value => 'abc123', + ]); + + $this->assertSame('john', $session->get(SessionKey::User)); + $this->assertSame('abc123', $session->get(SessionKey::Token)); + } + + /** + * Test that put() normalizes enum keys in arrays. + * Note: PHP auto-converts BackedEnums to their values when used as array keys, + * so by the time the array reaches put(), keys are already strings. + * This test verifies the overall behavior works correctly. + */ + public function testPutWithMixedArrayKeysUsingEnumValues(): void + { + $session = $this->getSession(); + $session->put([ + SessionKey::User->value => 'john', + 'legacy_key' => 'legacy_value', + SessionKey::Token->value => 'token123', + ]); + + $this->assertSame('john', $session->get('user')); + $this->assertSame('john', $session->get(SessionKey::User)); + $this->assertSame('legacy_value', $session->get('legacy_key')); + $this->assertSame('token123', $session->get('token')); + $this->assertSame('token123', $session->get(SessionKey::Token)); + } + + public function testPutWithIntBackedEnumKeyValues(): void + { + $session = $this->getSession(); + $session->put([ + (string) IntBackedKey::First->value => 'first-value', + (string) IntBackedKey::Second->value => 'second-value', + ]); + + $this->assertSame('first-value', $session->get('1')); + $this->assertSame('first-value', $session->get(IntBackedKey::First)); + $this->assertSame('second-value', $session->get('2')); + $this->assertSame('second-value', $session->get(IntBackedKey::Second)); + } + + // ========================================================================= + // exists() tests + // ========================================================================= + + public function testExistsWithSingleEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $this->assertTrue($session->exists(SessionKey::User)); + $this->assertFalse($session->exists(SessionKey::Token)); + } + + public function testExistsWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + + $this->assertTrue($session->exists([SessionKey::User, SessionKey::Token])); + $this->assertFalse($session->exists([SessionKey::User, SessionKey::Settings])); + } + + public function testExistsWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('legacy', 'value'); + + $this->assertTrue($session->exists([SessionKey::User, 'legacy'])); + $this->assertFalse($session->exists([SessionKey::User, 'nonexistent'])); + } + + // ========================================================================= + // missing() tests + // ========================================================================= + + public function testMissingWithSingleEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $this->assertFalse($session->missing(SessionKey::User)); + $this->assertTrue($session->missing(SessionKey::Token)); + } + + public function testMissingWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + + // All keys exist - missing returns false + $this->assertFalse($session->missing([SessionKey::User, SessionKey::Token])); + + // Some keys missing - missing returns true + $this->assertTrue($session->missing([SessionKey::Token, SessionKey::Settings])); + } + + public function testMissingWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + + $this->assertTrue($session->missing([SessionKey::User, 'legacy'])); + + $session->put('user', 'john'); + $session->put('legacy', 'value'); + + $this->assertFalse($session->missing([SessionKey::User, 'legacy'])); + } + + // ========================================================================= + // has() tests + // ========================================================================= + + public function testHasWithSingleEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', null); + + $this->assertTrue($session->has(SessionKey::User)); + $this->assertFalse($session->has(SessionKey::Token)); // null value + $this->assertFalse($session->has(SessionKey::Settings)); // doesn't exist + } + + public function testHasWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + + $this->assertTrue($session->has([SessionKey::User, SessionKey::Token])); + $this->assertFalse($session->has([SessionKey::User, SessionKey::Settings])); + } + + public function testHasWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('legacy', 'value'); + + $this->assertTrue($session->has([SessionKey::User, 'legacy'])); + $this->assertFalse($session->has([SessionKey::User, 'nonexistent'])); + } + + // ========================================================================= + // hasAny() tests + // ========================================================================= + + public function testHasAnyWithSingleEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $this->assertTrue($session->hasAny(SessionKey::User)); + $this->assertFalse($session->hasAny(SessionKey::Token)); + } + + public function testHasAnyWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $this->assertTrue($session->hasAny([SessionKey::User, SessionKey::Token])); + $this->assertFalse($session->hasAny([SessionKey::Token, SessionKey::Settings])); + } + + public function testHasAnyWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $this->assertTrue($session->hasAny([SessionKey::Token, 'user'])); + $this->assertTrue($session->hasAny(['nonexistent', SessionKey::User])); + $this->assertFalse($session->hasAny([SessionKey::Token, 'nonexistent'])); + } + + // ========================================================================= + // pull() tests + // ========================================================================= + + public function testPullWithEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $this->assertSame('john', $session->pull(SessionKey::User)); + $this->assertFalse($session->has('user')); + } + + public function testPullWithEnumReturnsDefault(): void + { + $session = $this->getSession(); + + $this->assertSame('default', $session->pull(SessionKey::User, 'default')); + } + + // ========================================================================= + // forget() tests + // ========================================================================= + + public function testForgetWithSingleEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + + $session->forget(SessionKey::User); + + $this->assertFalse($session->has('user')); + $this->assertTrue($session->has('token')); + } + + public function testForgetWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + $session->put('settings', ['dark' => true]); + + $session->forget([SessionKey::User, SessionKey::Token]); + + $this->assertFalse($session->has('user')); + $this->assertFalse($session->has('token')); + $this->assertTrue($session->has('settings')); + } + + public function testForgetWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('legacy', 'value'); + $session->put('token', 'abc'); + + $session->forget([SessionKey::User, 'legacy']); + + $this->assertFalse($session->has('user')); + $this->assertFalse($session->has('legacy')); + $this->assertTrue($session->has('token')); + } + + // ========================================================================= + // only() tests + // ========================================================================= + + public function testOnlyWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + $session->put('settings', ['dark' => true]); + + $result = $session->only([SessionKey::User, SessionKey::Token]); + + $this->assertSame(['user' => 'john', 'token' => 'abc'], $result); + } + + public function testOnlyWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('legacy', 'value'); + $session->put('token', 'abc'); + + $result = $session->only([SessionKey::User, 'legacy']); + + $this->assertSame(['user' => 'john', 'legacy' => 'value'], $result); + } + + public function testOnlyWithIntBackedEnums(): void + { + $session = $this->getSession(); + $session->put('1', 'first'); + $session->put('2', 'second'); + $session->put('3', 'third'); + + $result = $session->only([IntBackedKey::First, IntBackedKey::Second]); + + $this->assertSame(['1' => 'first', '2' => 'second'], $result); + } + + // ========================================================================= + // except() tests + // ========================================================================= + + public function testExceptWithArrayOfEnums(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('token', 'abc'); + $session->put('settings', ['dark' => true]); + + $result = $session->except([SessionKey::User, SessionKey::Token]); + + $this->assertSame(['settings' => ['dark' => true]], $result); + } + + public function testExceptWithMixedArrayEnumsAndStrings(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + $session->put('legacy', 'value'); + $session->put('token', 'abc'); + + $result = $session->except([SessionKey::User, 'legacy']); + + $this->assertSame(['token' => 'abc'], $result); + } + + // ========================================================================= + // remove() tests + // ========================================================================= + + public function testRemoveWithEnum(): void + { + $session = $this->getSession(); + $session->put('user', 'john'); + + $value = $session->remove(SessionKey::User); + + $this->assertSame('john', $value); + $this->assertFalse($session->has('user')); + } + + // ========================================================================= + // remember() tests + // ========================================================================= + + public function testRememberWithEnum(): void + { + $session = $this->getSession(); + + $result = $session->remember(SessionKey::User, fn () => 'computed'); + + $this->assertSame('computed', $result); + $this->assertSame('computed', $session->get(SessionKey::User)); + + // Second call should return cached value + $result2 = $session->remember(SessionKey::User, fn () => 'different'); + $this->assertSame('computed', $result2); + } + + // ========================================================================= + // push() tests + // ========================================================================= + + public function testPushWithEnum(): void + { + $session = $this->getSession(); + + $session->push(SessionKey::Items, 'item1'); + $session->push(SessionKey::Items, 'item2'); + + $this->assertSame(['item1', 'item2'], $session->get(SessionKey::Items)); + } + + // ========================================================================= + // increment() / decrement() tests + // ========================================================================= + + public function testIncrementWithEnum(): void + { + $session = $this->getSession(); + + $session->increment(SessionKey::Counter); + $this->assertSame(1, $session->get(SessionKey::Counter)); + + $session->increment(SessionKey::Counter, 5); + $this->assertSame(6, $session->get(SessionKey::Counter)); + } + + public function testDecrementWithEnum(): void + { + $session = $this->getSession(); + $session->put(SessionKey::Counter, 10); + + $session->decrement(SessionKey::Counter); + $this->assertSame(9, $session->get(SessionKey::Counter)); + + $session->decrement(SessionKey::Counter, 4); + $this->assertSame(5, $session->get(SessionKey::Counter)); + } + + // ========================================================================= + // flash() tests + // ========================================================================= + + public function testFlashWithEnum(): void + { + $session = $this->getSession(); + $session->getHandler()->shouldReceive('read')->once()->andReturn(serialize([])); + $session->start(); + + $session->flash(SessionKey::User, 'flash-value'); + + $this->assertTrue($session->has(SessionKey::User)); + $this->assertSame('flash-value', $session->get(SessionKey::User)); + + // Verify key is stored as string in _flash.new + $flashNew = $session->get('_flash.new'); + $this->assertContains('user', $flashNew); + } + + public function testFlashWithEnumIsProperlyAged(): void + { + $session = $this->getSession(); + $session->getHandler()->shouldReceive('read')->once()->andReturn(serialize([])); + $session->start(); + + $session->flash(SessionKey::User, 'flash-value'); + $session->ageFlashData(); + + // After aging, key should be in _flash.old + $this->assertContains('user', $session->get('_flash.old', [])); + $this->assertNotContains('user', $session->get('_flash.new', [])); + + // Value should still exist + $this->assertTrue($session->has(SessionKey::User)); + + // Age again - should be removed + $session->ageFlashData(); + $this->assertFalse($session->has(SessionKey::User)); + } + + // ========================================================================= + // now() tests + // ========================================================================= + + public function testNowWithEnum(): void + { + $session = $this->getSession(); + $session->getHandler()->shouldReceive('read')->once()->andReturn(serialize([])); + $session->start(); + + $session->now(SessionKey::User, 'now-value'); + + $this->assertTrue($session->has(SessionKey::User)); + $this->assertSame('now-value', $session->get(SessionKey::User)); + + // Verify key is stored as string in _flash.old (immediate expiry) + $flashOld = $session->get('_flash.old'); + $this->assertContains('user', $flashOld); + } + + // ========================================================================= + // hasOldInput() / getOldInput() tests + // ========================================================================= + + public function testHasOldInputWithEnum(): void + { + $session = $this->getSession(); + $session->put('_old_input', ['user' => 'john', 'email' => 'john@example.com']); + + $this->assertTrue($session->hasOldInput(SessionKey::User)); + $this->assertFalse($session->hasOldInput(SessionKey::Token)); + } + + public function testGetOldInputWithEnum(): void + { + $session = $this->getSession(); + $session->put('_old_input', ['user' => 'john', 'email' => 'john@example.com']); + + $this->assertSame('john', $session->getOldInput(SessionKey::User)); + $this->assertNull($session->getOldInput(SessionKey::Token)); + $this->assertSame('default', $session->getOldInput(SessionKey::Token, 'default')); + } + + // ========================================================================= + // Interoperability tests - enum and string access same data + // ========================================================================= + + public function testEnumAndStringAccessSameData(): void + { + $session = $this->getSession(); + + // Set with enum, get with string + $session->put(SessionKey::User, 'value1'); + $this->assertSame('value1', $session->get('user')); + + // Set with string, get with enum + $session->put('token', 'value2'); + $this->assertSame('value2', $session->get(SessionKey::Token)); + + // Verify both work together + $this->assertTrue($session->has('user')); + $this->assertTrue($session->has(SessionKey::User)); + $this->assertTrue($session->exists(['user', SessionKey::Token])); + } + + public function testIntBackedEnumInteroperability(): void + { + $session = $this->getSession(); + + $session->put(IntBackedKey::First, 'enum-value'); + $this->assertSame('enum-value', $session->get('1')); + + $session->put('2', 'string-value'); + $this->assertSame('string-value', $session->get(IntBackedKey::Second)); + } + + // ========================================================================= + // Helper methods + // ========================================================================= + + protected function getSession(string $serialization = 'php'): Store + { + $store = new Store( + 'test-session', + m::mock(SessionHandlerInterface::class), + $serialization + ); + + $store->setId('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + + return $store; + } +} From 63c417e40f5ea6d45e8d986d4140804e7ece6712 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:50:19 +0000 Subject: [PATCH 02/19] feat(session): add UnitEnum support and use enum_value() helper - Add UnitEnum support to session contract and store (BackedEnum uses ->value, UnitEnum uses ->name) - Replace Str::from()/fromAll() with enum_value() to match Laravel - Simplify enum_value() helper to match Laravel's direct implementation (remove unnecessary transform() wrapper) - Add comprehensive UnitEnum test coverage (24 new tests) - Update all session method signatures with strict UnitEnum types BREAKING CHANGE: Session contract method signatures now include UnitEnum type. Custom implementations must update their signatures. --- src/session/src/Contracts/Session.php | 15 +- src/session/src/Store.php | 80 +++--- src/support/src/Functions.php | 7 +- tests/Session/SessionStoreBackedEnumTest.php | 247 +++++++++++++++++++ 4 files changed, 292 insertions(+), 57 deletions(-) diff --git a/src/session/src/Contracts/Session.php b/src/session/src/Contracts/Session.php index 51b0058e3..05ab1d772 100644 --- a/src/session/src/Contracts/Session.php +++ b/src/session/src/Contracts/Session.php @@ -6,6 +6,7 @@ use BackedEnum; use SessionHandlerInterface; +use UnitEnum; interface Session { @@ -47,27 +48,27 @@ public function all(): array; /** * Checks if a key exists. */ - public function exists(array|BackedEnum|string $key): bool; + public function exists(array|BackedEnum|UnitEnum|string $key): bool; /** * Checks if a key is present and not null. */ - public function has(array|BackedEnum|string $key): bool; + public function has(array|BackedEnum|UnitEnum|string $key): bool; /** * Get an item from the session. */ - public function get(BackedEnum|string $key, mixed $default = null): mixed; + public function get(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed; /** * Get the value of a given key and then forget it. */ - public function pull(BackedEnum|string $key, mixed $default = null): mixed; + public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed; /** * Put a key / value pair or array of key / value pairs in the session. */ - public function put(array|BackedEnum|string $key, mixed $value = null): void; + public function put(array|BackedEnum|UnitEnum|string $key, mixed $value = null): void; /** * Get the CSRF token value. @@ -82,12 +83,12 @@ public function regenerateToken(): void; /** * Remove an item from the session, returning its value. */ - public function remove(BackedEnum|string $key): mixed; + public function remove(BackedEnum|UnitEnum|string $key): mixed; /** * Remove one or many items from the session. */ - public function forget(array|BackedEnum|string $keys): void; + public function forget(array|BackedEnum|UnitEnum|string $keys): void; /** * Remove all of the items from the session. diff --git a/src/session/src/Store.php b/src/session/src/Store.php index daf852599..16c6b1d9b 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -7,7 +7,6 @@ use BackedEnum; use Closure; use Hyperf\Collection\Arr; -use Hyperf\Collection\Collection; use Hyperf\Context\Context; use Hyperf\Macroable\Macroable; use Hyperf\Support\MessageBag; @@ -16,6 +15,9 @@ use Hypervel\Support\Str; use SessionHandlerInterface; use stdClass; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Store implements Session { @@ -204,10 +206,7 @@ public function all(): array */ public function only(array $keys): array { - $keys = Str::fromAll($keys); - $attributes = $this->getAttributes(); - - return Arr::only($attributes, $keys); + return Arr::only($this->getAttributes(), array_map(enum_value(...), $keys)); } /** @@ -215,20 +214,17 @@ public function only(array $keys): array */ public function except(array $keys): array { - $keys = Str::fromAll($keys); - $attributes = $this->getAttributes(); - - return Arr::except($attributes, $keys); + return Arr::except($this->getAttributes(), array_map(enum_value(...), $keys)); } /** * Checks if a key exists. */ - public function exists(array|BackedEnum|string $key): bool + public function exists(array|BackedEnum|UnitEnum|string $key): bool { $placeholder = new stdClass(); - return ! (new Collection(is_array($key) ? $key : func_get_args()))->contains(function ($key) use ($placeholder) { + return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) use ($placeholder) { return $this->get($key, $placeholder) === $placeholder; }); } @@ -236,7 +232,7 @@ public function exists(array|BackedEnum|string $key): bool /** * Determine if the given key is missing from the session data. */ - public function missing(array|BackedEnum|string $key): bool + public function missing(array|BackedEnum|UnitEnum|string $key): bool { return ! $this->exists($key); } @@ -244,9 +240,9 @@ public function missing(array|BackedEnum|string $key): bool /** * Determine if a key is present and not null. */ - public function has(array|BackedEnum|string $key): bool + public function has(array|BackedEnum|UnitEnum|string $key): bool { - return ! (new Collection(is_array($key) ? $key : func_get_args()))->contains(function ($key) { + return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) { return is_null($this->get($key)); }); } @@ -254,9 +250,9 @@ public function has(array|BackedEnum|string $key): bool /** * Determine if any of the given keys are present and not null. */ - public function hasAny(array|BackedEnum|string $key): bool + public function hasAny(array|BackedEnum|UnitEnum|string $key): bool { - return (new Collection(is_array($key) ? $key : func_get_args()))->filter(function ($key) { + return collect(is_array($key) ? $key : func_get_args())->filter(function ($key) { return ! is_null($this->get($key)); })->count() >= 1; } @@ -264,22 +260,18 @@ public function hasAny(array|BackedEnum|string $key): bool /** * Get an item from the session. */ - public function get(BackedEnum|string $key, mixed $default = null): mixed + public function get(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed { - $key = Str::from($key); - $attributes = $this->getAttributes(); - - return Arr::get($attributes, $key, $default); + return Arr::get($this->getAttributes(), enum_value($key), $default); } /** * Get the value of a given key and then forget it. */ - public function pull(BackedEnum|string $key, mixed $default = null): mixed + public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed { - $key = Str::from($key); $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, $key, $default); + $result = Arr::pull($attributes, enum_value($key), $default); $this->setAttributes($attributes); @@ -289,7 +281,7 @@ public function pull(BackedEnum|string $key, mixed $default = null): mixed /** * Determine if the session contains old input. */ - public function hasOldInput(BackedEnum|string|null $key = null): bool + public function hasOldInput(BackedEnum|UnitEnum|string|null $key = null): bool { $old = $this->getOldInput($key); @@ -299,11 +291,9 @@ public function hasOldInput(BackedEnum|string|null $key = null): bool /** * Get the requested item from the flashed input array. */ - public function getOldInput(BackedEnum|string|null $key = null, mixed $default = null): mixed + public function getOldInput(BackedEnum|UnitEnum|string|null $key = null, mixed $default = null): mixed { - $key = $key === null ? null : Str::from($key); - - return Arr::get($this->get('_old_input', []), $key, $default); + return Arr::get($this->get('_old_input', []), enum_value($key), $default); } /** @@ -317,15 +307,15 @@ public function replace(array $attributes): void /** * Put a key / value pair or array of key / value pairs in the session. */ - public function put(array|BackedEnum|string $key, mixed $value = null): void + public function put(array|BackedEnum|UnitEnum|string $key, mixed $value = null): void { if (! is_array($key)) { - $key = [Str::from($key) => $value]; + $key = [enum_value($key) => $value]; } $attributes = $this->getAttributes(); foreach ($key as $arrayKey => $arrayValue) { - Arr::set($attributes, Str::from($arrayKey), $arrayValue); + Arr::set($attributes, enum_value($arrayKey), $arrayValue); } $this->setAttributes($attributes); @@ -334,7 +324,7 @@ public function put(array|BackedEnum|string $key, mixed $value = null): void /** * Get an item from the session, or store the default value. */ - public function remember(BackedEnum|string $key, Closure $callback): mixed + public function remember(BackedEnum|UnitEnum|string $key, Closure $callback): mixed { if (! is_null($value = $this->get($key))) { return $value; @@ -348,7 +338,7 @@ public function remember(BackedEnum|string $key, Closure $callback): mixed /** * Push a value onto a session array. */ - public function push(BackedEnum|string $key, mixed $value): void + public function push(BackedEnum|UnitEnum|string $key, mixed $value): void { $array = $this->get($key, []); @@ -360,7 +350,7 @@ public function push(BackedEnum|string $key, mixed $value): void /** * Increment the value of an item in the session. */ - public function increment(BackedEnum|string $key, int $amount = 1): mixed + public function increment(BackedEnum|UnitEnum|string $key, int $amount = 1): mixed { $this->put($key, $value = $this->get($key, 0) + $amount); @@ -370,7 +360,7 @@ public function increment(BackedEnum|string $key, int $amount = 1): mixed /** * Decrement the value of an item in the session. */ - public function decrement(BackedEnum|string $key, int $amount = 1): int + public function decrement(BackedEnum|UnitEnum|string $key, int $amount = 1): int { return $this->increment($key, $amount * -1); } @@ -378,9 +368,9 @@ public function decrement(BackedEnum|string $key, int $amount = 1): int /** * Flash a key / value pair to the session. */ - public function flash(BackedEnum|string $key, mixed $value = true): void + public function flash(BackedEnum|UnitEnum|string $key, mixed $value = true): void { - $key = Str::from($key); + $key = enum_value($key); $this->put($key, $value); @@ -392,9 +382,9 @@ public function flash(BackedEnum|string $key, mixed $value = true): void /** * Flash a key / value pair to the session for immediate use. */ - public function now(BackedEnum|string $key, mixed $value): void + public function now(BackedEnum|UnitEnum|string $key, mixed $value): void { - $key = Str::from($key); + $key = enum_value($key); $this->put($key, $value); @@ -452,11 +442,10 @@ public function flashInput(array $value): void /** * Remove an item from the session, returning its value. */ - public function remove(BackedEnum|string $key): mixed + public function remove(BackedEnum|UnitEnum|string $key): mixed { - $key = Str::from($key); $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, $key); + $result = Arr::pull($attributes, enum_value($key)); $this->setAttributes($attributes); @@ -466,11 +455,10 @@ public function remove(BackedEnum|string $key): mixed /** * Remove one or many items from the session. */ - public function forget(array|BackedEnum|string $keys): void + public function forget(array|BackedEnum|UnitEnum|string $keys): void { - $keys = is_array($keys) ? Str::fromAll($keys) : Str::from($keys); $attributes = $this->getAttributes(); - Arr::forget($attributes, $keys); + Arr::forget($attributes, collect((array) $keys)->map(fn ($key) => enum_value($key))->all()); $this->setAttributes($attributes); } diff --git a/src/support/src/Functions.php b/src/support/src/Functions.php index 2550b5225..1141400dd 100644 --- a/src/support/src/Functions.php +++ b/src/support/src/Functions.php @@ -36,12 +36,11 @@ function value(mixed $value, ...$args) */ function enum_value($value, $default = null) { - return transform($value, fn ($value) => match (true) { + return match (true) { $value instanceof BackedEnum => $value->value, $value instanceof UnitEnum => $value->name, - - default => $value, - }, $default ?? $value); + default => $value ?? value($default), + }; } /** diff --git a/tests/Session/SessionStoreBackedEnumTest.php b/tests/Session/SessionStoreBackedEnumTest.php index 8b4d9511c..1b60768f0 100644 --- a/tests/Session/SessionStoreBackedEnumTest.php +++ b/tests/Session/SessionStoreBackedEnumTest.php @@ -25,6 +25,13 @@ enum IntBackedKey: int case Second = 2; } +enum SessionUnitKey +{ + case User; + case Token; + case Settings; +} + /** * @internal * @coversNothing @@ -589,6 +596,246 @@ public function testIntBackedEnumInteroperability(): void $this->assertSame('string-value', $session->get(IntBackedKey::Second)); } + // ========================================================================= + // UnitEnum tests - uses enum name as key + // ========================================================================= + + public function testGetWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('User', 'john'); + + $this->assertSame('john', $session->get(SessionUnitKey::User)); + } + + public function testPutWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put(SessionUnitKey::User, 'jane'); + + // UnitEnum uses ->name, so key is 'User' not 'user' + $this->assertSame('jane', $session->get('User')); + $this->assertSame('jane', $session->get(SessionUnitKey::User)); + } + + public function testExistsWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('User', 'john'); + + $this->assertTrue($session->exists(SessionUnitKey::User)); + $this->assertFalse($session->exists(SessionUnitKey::Token)); + } + + public function testHasWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put(SessionUnitKey::User, 'john'); + $session->put(SessionUnitKey::Token, null); + + $this->assertTrue($session->has(SessionUnitKey::User)); + $this->assertFalse($session->has(SessionUnitKey::Token)); // null value + $this->assertFalse($session->has(SessionUnitKey::Settings)); // doesn't exist + } + + public function testHasAnyWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put(SessionUnitKey::User, 'john'); + + $this->assertTrue($session->hasAny([SessionUnitKey::User, SessionUnitKey::Token])); + $this->assertFalse($session->hasAny([SessionUnitKey::Token, SessionUnitKey::Settings])); + } + + public function testPullWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('User', 'john'); + + $this->assertSame('john', $session->pull(SessionUnitKey::User)); + $this->assertFalse($session->has('User')); + } + + public function testForgetWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put(SessionUnitKey::User, 'john'); + $session->put(SessionUnitKey::Token, 'abc'); + + $session->forget(SessionUnitKey::User); + + $this->assertFalse($session->has('User')); + $this->assertTrue($session->has('Token')); + } + + public function testForgetWithArrayOfUnitEnums(): void + { + $session = $this->getSession(); + $session->put(SessionUnitKey::User, 'john'); + $session->put(SessionUnitKey::Token, 'abc'); + $session->put(SessionUnitKey::Settings, ['dark' => true]); + + $session->forget([SessionUnitKey::User, SessionUnitKey::Token]); + + $this->assertFalse($session->has('User')); + $this->assertFalse($session->has('Token')); + $this->assertTrue($session->has('Settings')); + } + + public function testOnlyWithUnitEnums(): void + { + $session = $this->getSession(); + $session->put('User', 'john'); + $session->put('Token', 'abc'); + $session->put('Settings', ['dark' => true]); + + $result = $session->only([SessionUnitKey::User, SessionUnitKey::Token]); + + $this->assertSame(['User' => 'john', 'Token' => 'abc'], $result); + } + + public function testExceptWithUnitEnums(): void + { + $session = $this->getSession(); + $session->put('User', 'john'); + $session->put('Token', 'abc'); + $session->put('Settings', ['dark' => true]); + + $result = $session->except([SessionUnitKey::User, SessionUnitKey::Token]); + + $this->assertSame(['Settings' => ['dark' => true]], $result); + } + + public function testRemoveWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('User', 'john'); + + $value = $session->remove(SessionUnitKey::User); + + $this->assertSame('john', $value); + $this->assertFalse($session->has('User')); + } + + public function testRememberWithUnitEnum(): void + { + $session = $this->getSession(); + + $result = $session->remember(SessionUnitKey::User, fn () => 'computed'); + + $this->assertSame('computed', $result); + $this->assertSame('computed', $session->get('User')); + } + + public function testPushWithUnitEnum(): void + { + $session = $this->getSession(); + + $session->push(SessionUnitKey::User, 'item1'); + $session->push(SessionUnitKey::User, 'item2'); + + $this->assertSame(['item1', 'item2'], $session->get('User')); + } + + public function testIncrementWithUnitEnum(): void + { + $session = $this->getSession(); + + $session->increment(SessionUnitKey::User); + $this->assertSame(1, $session->get('User')); + + $session->increment(SessionUnitKey::User, 5); + $this->assertSame(6, $session->get('User')); + } + + public function testDecrementWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('User', 10); + + $session->decrement(SessionUnitKey::User); + $this->assertSame(9, $session->get('User')); + } + + public function testFlashWithUnitEnum(): void + { + $session = $this->getSession(); + $session->getHandler()->shouldReceive('read')->once()->andReturn(serialize([])); + $session->start(); + + $session->flash(SessionUnitKey::User, 'flash-value'); + + $this->assertTrue($session->has('User')); + $this->assertSame('flash-value', $session->get('User')); + + // Verify key is stored as string in _flash.new + $flashNew = $session->get('_flash.new'); + $this->assertContains('User', $flashNew); + } + + public function testNowWithUnitEnum(): void + { + $session = $this->getSession(); + $session->getHandler()->shouldReceive('read')->once()->andReturn(serialize([])); + $session->start(); + + $session->now(SessionUnitKey::User, 'now-value'); + + $this->assertTrue($session->has('User')); + $this->assertSame('now-value', $session->get('User')); + + // Verify key is stored as string in _flash.old + $flashOld = $session->get('_flash.old'); + $this->assertContains('User', $flashOld); + } + + public function testHasOldInputWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('_old_input', ['User' => 'john', 'email' => 'john@example.com']); + + $this->assertTrue($session->hasOldInput(SessionUnitKey::User)); + $this->assertFalse($session->hasOldInput(SessionUnitKey::Token)); + } + + public function testGetOldInputWithUnitEnum(): void + { + $session = $this->getSession(); + $session->put('_old_input', ['User' => 'john', 'email' => 'john@example.com']); + + $this->assertSame('john', $session->getOldInput(SessionUnitKey::User)); + $this->assertNull($session->getOldInput(SessionUnitKey::Token)); + $this->assertSame('default', $session->getOldInput(SessionUnitKey::Token, 'default')); + } + + public function testUnitEnumInteroperability(): void + { + $session = $this->getSession(); + + // Set with UnitEnum, get with string + $session->put(SessionUnitKey::User, 'value1'); + $this->assertSame('value1', $session->get('User')); + + // Set with string, get with UnitEnum + $session->put('Token', 'value2'); + $this->assertSame('value2', $session->get(SessionUnitKey::Token)); + } + + public function testMixedBackedAndUnitEnums(): void + { + $session = $this->getSession(); + + // BackedEnum uses ->value ('user'), UnitEnum uses ->name ('User') + $session->put(SessionKey::User, 'backed-value'); + $session->put(SessionUnitKey::User, 'unit-value'); + + // These are different keys + $this->assertSame('backed-value', $session->get('user')); + $this->assertSame('unit-value', $session->get('User')); + $this->assertSame('backed-value', $session->get(SessionKey::User)); + $this->assertSame('unit-value', $session->get(SessionUnitKey::User)); + } + // ========================================================================= // Helper methods // ========================================================================= From d07c240c9a9e9a7a3a449ef190aeae2b9d4a61c2 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:01:24 +0000 Subject: [PATCH 03/19] feat(cache): add BackedEnum and UnitEnum support to RateLimiter - Add enum support to for() and limiter() methods for named rate limiters - Add resolveLimiterName() helper using enum_value() - Add comprehensive enum tests covering BackedEnum, UnitEnum, and string interoperability Following Laravel's approach where only named limiter methods support enums (for/limiter), not key-based methods (attempt/hit/etc) since those typically use dynamic concatenated keys. --- src/cache/src/RateLimiter.php | 20 +++- tests/Cache/RateLimiterEnumTest.php | 138 ++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 tests/Cache/RateLimiterEnumTest.php diff --git a/src/cache/src/RateLimiter.php b/src/cache/src/RateLimiter.php index 8a61238b9..6400a798d 100644 --- a/src/cache/src/RateLimiter.php +++ b/src/cache/src/RateLimiter.php @@ -4,9 +4,13 @@ namespace Hypervel\Cache; +use BackedEnum; use Closure; use Hyperf\Support\Traits\InteractsWithTime; use Hypervel\Cache\Contracts\Factory as Cache; +use UnitEnum; + +use function Hypervel\Support\enum_value; class RateLimiter { @@ -33,9 +37,9 @@ public function __construct(Cache $cache) /** * Register a named limiter configuration. */ - public function for(string $name, Closure $callback): static + public function for(BackedEnum|UnitEnum|string $name, Closure $callback): static { - $this->limiters[$name] = $callback; + $this->limiters[$this->resolveLimiterName($name)] = $callback; return $this; } @@ -43,9 +47,17 @@ public function for(string $name, Closure $callback): static /** * Get the given named rate limiter. */ - public function limiter(string $name): ?Closure + public function limiter(BackedEnum|UnitEnum|string $name): ?Closure + { + return $this->limiters[$this->resolveLimiterName($name)] ?? null; + } + + /** + * Resolve the rate limiter name. + */ + private function resolveLimiterName(BackedEnum|UnitEnum|string $name): string { - return $this->limiters[$name] ?? null; + return (string) enum_value($name); } /** diff --git a/tests/Cache/RateLimiterEnumTest.php b/tests/Cache/RateLimiterEnumTest.php new file mode 100644 index 000000000..2942ebadf --- /dev/null +++ b/tests/Cache/RateLimiterEnumTest.php @@ -0,0 +1,138 @@ +for($name, fn () => 'limit'); + + $limiters = $reflectedLimitersProperty->getValue($rateLimiter); + + $this->assertArrayHasKey($expected, $limiters); + + $limiterClosure = $rateLimiter->limiter($name); + + $this->assertNotNull($limiterClosure); + } + + public static function registerNamedRateLimiterDataProvider(): array + { + return [ + 'uses BackedEnum' => [BackedEnumNamedRateLimiter::API, 'api'], + 'uses UnitEnum' => [UnitEnumNamedRateLimiter::ThirdParty, 'ThirdParty'], + 'uses normal string' => ['yolo', 'yolo'], + ]; + } + + public function testForWithBackedEnumStoresUnderValue(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + $rateLimiter->for(BackedEnumNamedRateLimiter::API, fn () => 'api-limit'); + + // Can retrieve with enum + $this->assertNotNull($rateLimiter->limiter(BackedEnumNamedRateLimiter::API)); + + // Can also retrieve with string value + $this->assertNotNull($rateLimiter->limiter('api')); + + // Closure returns expected value + $this->assertSame('api-limit', $rateLimiter->limiter(BackedEnumNamedRateLimiter::API)()); + } + + public function testForWithUnitEnumStoresUnderName(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + $rateLimiter->for(UnitEnumNamedRateLimiter::ThirdParty, fn () => 'third-party-limit'); + + // Can retrieve with enum + $this->assertNotNull($rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty)); + + // Can also retrieve with string name (PascalCase) + $this->assertNotNull($rateLimiter->limiter('ThirdParty')); + + // Closure returns expected value + $this->assertSame('third-party-limit', $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty)()); + } + + public function testLimiterReturnsNullForNonExistentEnum(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + + $this->assertNull($rateLimiter->limiter(BackedEnumNamedRateLimiter::Web)); + $this->assertNull($rateLimiter->limiter(UnitEnumNamedRateLimiter::Internal)); + } + + public function testBackedEnumAndStringInteroperability(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + + // Register with string + $rateLimiter->for('api', fn () => 'string-registered'); + + // Retrieve with BackedEnum that has same value + $limiter = $rateLimiter->limiter(BackedEnumNamedRateLimiter::API); + + $this->assertNotNull($limiter); + $this->assertSame('string-registered', $limiter()); + } + + public function testUnitEnumAndStringInteroperability(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + + // Register with string (matching UnitEnum name) + $rateLimiter->for('ThirdParty', fn () => 'string-registered'); + + // Retrieve with UnitEnum + $limiter = $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty); + + $this->assertNotNull($limiter); + $this->assertSame('string-registered', $limiter()); + } + + public function testMultipleEnumLimitersCanCoexist(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + + $rateLimiter->for(BackedEnumNamedRateLimiter::API, fn () => 'api-limit'); + $rateLimiter->for(BackedEnumNamedRateLimiter::Web, fn () => 'web-limit'); + $rateLimiter->for(UnitEnumNamedRateLimiter::ThirdParty, fn () => 'third-party-limit'); + $rateLimiter->for('custom', fn () => 'custom-limit'); + + $this->assertSame('api-limit', $rateLimiter->limiter(BackedEnumNamedRateLimiter::API)()); + $this->assertSame('web-limit', $rateLimiter->limiter(BackedEnumNamedRateLimiter::Web)()); + $this->assertSame('third-party-limit', $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty)()); + $this->assertSame('custom-limit', $rateLimiter->limiter('custom')()); + } +} From 3e12e730d6f3d2c83affa7b2a290c93773d2975a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:22:03 +0000 Subject: [PATCH 04/19] Add BackedEnum and UnitEnum support to Gate - Update Gate contract with enum type hints for has(), define(), allows(), denies(), check(), any(), none(), authorize(), and inspect() methods - Update Gate implementation to use enum_value() for ability resolution - Update Authorize middleware to support enums in using() method - Update AuthorizesRequests trait to use enum_value() in parseAbilityAndArguments() - Add comprehensive GateEnumTest with tests for all enum scenarios --- src/auth/src/Access/AuthorizesRequests.php | 4 + src/auth/src/Access/Gate.php | 28 +- src/auth/src/Contracts/Gate.php | 23 +- src/auth/src/Middleware/Authorize.php | 8 +- tests/Auth/Access/GateEnumTest.php | 341 +++++++++++++++++++++ 5 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 tests/Auth/Access/GateEnumTest.php diff --git a/src/auth/src/Access/AuthorizesRequests.php b/src/auth/src/Access/AuthorizesRequests.php index 5951827f9..d032c0039 100644 --- a/src/auth/src/Access/AuthorizesRequests.php +++ b/src/auth/src/Access/AuthorizesRequests.php @@ -8,6 +8,8 @@ use Hypervel\Auth\Contracts\Authenticatable; use Hypervel\Auth\Contracts\Gate; +use function Hypervel\Support\enum_value; + trait AuthorizesRequests { /** @@ -39,6 +41,8 @@ public function authorizeForUser(?Authenticatable $user, mixed $ability, mixed $ */ protected function parseAbilityAndArguments(mixed $ability, mixed $arguments = []): array { + $ability = enum_value($ability); + if (is_string($ability) && ! str_contains($ability, '\\')) { return [$ability, $arguments]; } diff --git a/src/auth/src/Access/Gate.php b/src/auth/src/Access/Gate.php index 765cb7a6b..0feb0bb06 100644 --- a/src/auth/src/Access/Gate.php +++ b/src/auth/src/Access/Gate.php @@ -4,6 +4,7 @@ namespace Hypervel\Auth\Access; +use BackedEnum; use Closure; use Exception; use Hyperf\Collection\Arr; @@ -20,6 +21,9 @@ use ReflectionException; use ReflectionFunction; use ReflectionParameter; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Gate implements GateContract { @@ -58,12 +62,12 @@ public function __construct( /** * Determine if a given ability has been defined. */ - public function has(array|string $ability): bool + public function has(array|BackedEnum|UnitEnum|string $ability): bool { $abilities = is_array($ability) ? $ability : func_get_args(); foreach ($abilities as $ability) { - if (! isset($this->abilities[$ability])) { + if (! isset($this->abilities[enum_value($ability)])) { return false; } } @@ -120,8 +124,10 @@ protected function authorizeOnDemand(bool|Closure|Response $condition, ?string $ * * @throws InvalidArgumentException */ - public function define(string $ability, array|callable|string $callback): static + public function define(BackedEnum|UnitEnum|string $ability, array|callable|string $callback): static { + $ability = enum_value($ability); + if (is_array($callback) && isset($callback[0]) && is_string($callback[0])) { $callback = $callback[0] . '@' . $callback[1]; } @@ -227,7 +233,7 @@ public function after(callable $callback): static /** * Determine if the given ability should be granted for the current user. */ - public function allows(string $ability, mixed $arguments = []): bool + public function allows(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool { return $this->check($ability, $arguments); } @@ -235,7 +241,7 @@ public function allows(string $ability, mixed $arguments = []): bool /** * Determine if the given ability should be denied for the current user. */ - public function denies(string $ability, mixed $arguments = []): bool + public function denies(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool { return ! $this->allows($ability, $arguments); } @@ -243,7 +249,7 @@ public function denies(string $ability, mixed $arguments = []): bool /** * Determine if all of the given abilities should be granted for the current user. */ - public function check(iterable|string $abilities, mixed $arguments = []): bool + public function check(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool { return collect($abilities)->every( fn ($ability) => $this->inspect($ability, $arguments)->allowed() @@ -253,7 +259,7 @@ public function check(iterable|string $abilities, mixed $arguments = []): bool /** * Determine if any one of the given abilities should be granted for the current user. */ - public function any(iterable|string $abilities, mixed $arguments = []): bool + public function any(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool { return collect($abilities)->contains(fn ($ability) => $this->check($ability, $arguments)); } @@ -261,7 +267,7 @@ public function any(iterable|string $abilities, mixed $arguments = []): bool /** * Determine if all of the given abilities should be denied for the current user. */ - public function none(iterable|string $abilities, mixed $arguments = []): bool + public function none(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool { return ! $this->any($abilities, $arguments); } @@ -271,7 +277,7 @@ public function none(iterable|string $abilities, mixed $arguments = []): bool * * @throws AuthorizationException */ - public function authorize(string $ability, mixed $arguments = []): Response + public function authorize(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response { return $this->inspect($ability, $arguments)->authorize(); } @@ -279,10 +285,10 @@ public function authorize(string $ability, mixed $arguments = []): Response /** * Inspect the user for the given ability. */ - public function inspect(string $ability, mixed $arguments = []): Response + public function inspect(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response { try { - $result = $this->raw($ability, $arguments); + $result = $this->raw(enum_value($ability), $arguments); if ($result instanceof Response) { return $result; diff --git a/src/auth/src/Contracts/Gate.php b/src/auth/src/Contracts/Gate.php index 28a86fa4c..d3c674bbb 100644 --- a/src/auth/src/Contracts/Gate.php +++ b/src/auth/src/Contracts/Gate.php @@ -4,21 +4,23 @@ namespace Hypervel\Auth\Contracts; +use BackedEnum; use Hypervel\Auth\Access\AuthorizationException; use Hypervel\Auth\Access\Response; use InvalidArgumentException; +use UnitEnum; interface Gate { /** * Determine if a given ability has been defined. */ - public function has(string $ability): bool; + public function has(array|BackedEnum|UnitEnum|string $ability): bool; /** * Define a new ability. */ - public function define(string $ability, callable|string $callback): static; + public function define(BackedEnum|UnitEnum|string $ability, callable|string $callback): static; /** * Define abilities for a resource. @@ -43,34 +45,39 @@ public function after(callable $callback): static; /** * Determine if the given ability should be granted for the current user. */ - public function allows(string $ability, mixed $arguments = []): bool; + public function allows(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool; /** * Determine if the given ability should be denied for the current user. */ - public function denies(string $ability, mixed $arguments = []): bool; + public function denies(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool; /** * Determine if all of the given abilities should be granted for the current user. */ - public function check(iterable|string $abilities, mixed $arguments = []): bool; + public function check(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool; /** * Determine if any one of the given abilities should be granted for the current user. */ - public function any(iterable|string $abilities, mixed $arguments = []): bool; + public function any(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool; + + /** + * Determine if all of the given abilities should be denied for the current user. + */ + public function none(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool; /** * Determine if the given ability should be granted for the current user. * * @throws AuthorizationException */ - public function authorize(string $ability, mixed $arguments = []): Response; + public function authorize(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response; /** * Inspect the user for the given ability. */ - public function inspect(string $ability, mixed $arguments = []): Response; + public function inspect(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response; /** * Get the raw result from the authorization callback. diff --git a/src/auth/src/Middleware/Authorize.php b/src/auth/src/Middleware/Authorize.php index ec842d893..fbececac0 100644 --- a/src/auth/src/Middleware/Authorize.php +++ b/src/auth/src/Middleware/Authorize.php @@ -4,6 +4,7 @@ namespace Hypervel\Auth\Middleware; +use BackedEnum; use Hyperf\Collection\Collection; use Hyperf\Database\Model\Model; use Hyperf\HttpServer\Router\Dispatched; @@ -13,6 +14,9 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Authorize implements MiddlewareInterface { @@ -28,9 +32,9 @@ public function __construct(protected Gate $gate) /** * Specify the ability and models for the middleware. */ - public static function using(string $ability, string ...$models): string + public static function using(BackedEnum|UnitEnum|string $ability, string ...$models): string { - return static::class . ':' . implode(',', [$ability, ...$models]); + return static::class . ':' . implode(',', [enum_value($ability), ...$models]); } /** diff --git a/tests/Auth/Access/GateEnumTest.php b/tests/Auth/Access/GateEnumTest.php new file mode 100644 index 000000000..97698c674 --- /dev/null +++ b/tests/Auth/Access/GateEnumTest.php @@ -0,0 +1,341 @@ +getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + // Can check with string (the enum value) + $this->assertTrue($gate->allows('view-dashboard')); + } + + public function testDefineWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + // UnitEnum uses ->name, so key is 'ManageUsers' + $this->assertTrue($gate->allows('ManageUsers')); + } + + // ========================================================================= + // allows() with enums + // ========================================================================= + + public function testAllowsWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $this->assertTrue($gate->allows(AbilitiesBackedEnum::ViewDashboard)); + } + + public function testAllowsWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->allows(AbilitiesUnitEnum::ManageUsers)); + } + + // ========================================================================= + // denies() with enums + // ========================================================================= + + public function testDeniesWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => false); + + $this->assertTrue($gate->denies(AbilitiesBackedEnum::ViewDashboard)); + } + + public function testDeniesWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => false); + + $this->assertTrue($gate->denies(AbilitiesUnitEnum::ManageUsers)); + } + + // ========================================================================= + // check() with enums (array of abilities) + // ========================================================================= + + public function testCheckWithArrayContainingBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('allow_1', fn ($user) => true); + $gate->define('allow_2', fn ($user) => true); + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $this->assertTrue($gate->check(['allow_1', 'allow_2', AbilitiesBackedEnum::ViewDashboard])); + } + + public function testCheckWithArrayContainingUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('allow_1', fn ($user) => true); + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->check(['allow_1', AbilitiesUnitEnum::ManageUsers])); + } + + // ========================================================================= + // any() with enums + // ========================================================================= + + public function testAnyWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->policy(AccessGateTestDummy::class, AccessGateTestPolicyWithAllPermissions::class); + + $this->assertTrue($gate->any(['edit', AbilitiesBackedEnum::Update], new AccessGateTestDummy())); + } + + public function testAnyWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('deny', fn ($user) => false); + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->any(['deny', AbilitiesUnitEnum::ManageUsers])); + } + + // ========================================================================= + // none() with enums + // ========================================================================= + + public function testNoneWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->policy(AccessGateTestDummy::class, AccessGateTestPolicyWithNoPermissions::class); + + $this->assertTrue($gate->none(['edit', AbilitiesBackedEnum::Update], new AccessGateTestDummy())); + } + + public function testNoneWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('deny_1', fn ($user) => false); + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => false); + + $this->assertTrue($gate->none(['deny_1', AbilitiesUnitEnum::ManageUsers])); + } + + public function testNoneReturnsFalseWhenAnyAbilityAllows(): void + { + $gate = $this->getBasicGate(); + + $gate->define('deny', fn ($user) => false); + $gate->define('allow', fn ($user) => true); + + $this->assertFalse($gate->none(['deny', 'allow'])); + } + + // ========================================================================= + // has() with enums + // ========================================================================= + + public function testHasWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $this->assertTrue($gate->has(AbilitiesBackedEnum::ViewDashboard)); + $this->assertFalse($gate->has(AbilitiesBackedEnum::Update)); + } + + public function testHasWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->has(AbilitiesUnitEnum::ManageUsers)); + $this->assertFalse($gate->has(AbilitiesUnitEnum::ViewReports)); + } + + public function testHasWithArrayContainingEnums(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->has([AbilitiesBackedEnum::ViewDashboard, AbilitiesUnitEnum::ManageUsers])); + $this->assertFalse($gate->has([AbilitiesBackedEnum::ViewDashboard, AbilitiesBackedEnum::Update])); + } + + // ========================================================================= + // authorize() with enums + // ========================================================================= + + public function testAuthorizeWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $response = $gate->authorize(AbilitiesBackedEnum::ViewDashboard); + + $this->assertTrue($response->allowed()); + } + + public function testAuthorizeWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $response = $gate->authorize(AbilitiesUnitEnum::ManageUsers); + + $this->assertTrue($response->allowed()); + } + + // ========================================================================= + // inspect() with enums + // ========================================================================= + + public function testInspectWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $response = $gate->inspect(AbilitiesBackedEnum::ViewDashboard); + + $this->assertTrue($response->allowed()); + } + + public function testInspectWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => false); + + $response = $gate->inspect(AbilitiesUnitEnum::ManageUsers); + + $this->assertFalse($response->allowed()); + } + + // ========================================================================= + // Interoperability tests + // ========================================================================= + + public function testBackedEnumAndStringInteroperability(): void + { + $gate = $this->getBasicGate(); + + // Define with enum + $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + // Check with string (the enum value) + $this->assertTrue($gate->allows('view-dashboard')); + + // Define with string + $gate->define('update', fn ($user) => true); + + // Check with enum that has same value + $this->assertTrue($gate->allows(AbilitiesBackedEnum::Update)); + } + + public function testUnitEnumAndStringInteroperability(): void + { + $gate = $this->getBasicGate(); + + // Define with enum + $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + // Check with string (the enum name) + $this->assertTrue($gate->allows('ManageUsers')); + + // Define with string + $gate->define('ViewReports', fn ($user) => true); + + // Check with enum + $this->assertTrue($gate->allows(AbilitiesUnitEnum::ViewReports)); + } + + // ========================================================================= + // Authorize middleware + // ========================================================================= + + public function testAuthorizeMiddlewareUsingWithBackedEnum(): void + { + $result = Authorize::using(AbilitiesBackedEnum::ViewDashboard, 'App\\Models\\Post'); + + $this->assertSame(Authorize::class . ':view-dashboard,App\\Models\\Post', $result); + } + + public function testAuthorizeMiddlewareUsingWithUnitEnum(): void + { + $result = Authorize::using(AbilitiesUnitEnum::ManageUsers); + + $this->assertSame(Authorize::class . ':ManageUsers', $result); + } + + // ========================================================================= + // Helper methods + // ========================================================================= + + protected function getBasicGate(bool $isGuest = false): Gate + { + $container = new Container(new DefinitionSource([])); + + return new Gate( + $container, + fn () => $isGuest ? null : new AccessGateTestAuthenticatable() + ); + } +} From b58356009cb39ba47e6512004964ffb956adc009 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:38:12 +0000 Subject: [PATCH 05/19] Add BackedEnum and UnitEnum support to Collection - Override groupBy() to convert UnitEnum/Stringable keys via enum_value() - Override keyBy() to convert UnitEnum keys via enum_value() - Override getArrayableItems() to wrap UnitEnum as [$enum] instead of casting - Override operatorForWhere() to use enum_value() for comparisons - Add Collection.php to phpstan ignored paths (same as Eloquent Collection) - Add comprehensive tests for both enum and base functionality --- phpstan.neon.dist | 1 + src/support/src/Collection.php | 146 +++++++++++++ tests/Auth/Access/GateEnumTest.php | 4 +- tests/Support/CollectionEnumTest.php | 290 ++++++++++++++++++++++++++ tests/Support/CollectionTest.php | 299 +++++++++++++++++++++++++++ 5 files changed, 738 insertions(+), 2 deletions(-) create mode 100644 tests/Support/CollectionEnumTest.php create mode 100644 tests/Support/CollectionTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1ca809110..d86b0c032 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -45,6 +45,7 @@ parameters: - '#Call to an undefined method Hyperf\\Tappable\\HigherOrderTapProxy#' - message: '#.*#' paths: + - src/support/src/Collection.php - src/core/src/Database/Eloquent/Builder.php - src/core/src/Database/Eloquent/Collection.php - src/core/src/Database/Eloquent/Concerns/HasRelationships.php diff --git a/src/support/src/Collection.php b/src/support/src/Collection.php index e23980f94..1418f1ad7 100644 --- a/src/support/src/Collection.php +++ b/src/support/src/Collection.php @@ -4,8 +4,12 @@ namespace Hypervel\Support; +use Closure; use Hyperf\Collection\Collection as BaseCollection; +use Hyperf\Collection\Enumerable; use Hypervel\Support\Traits\TransformsToResourceCollection; +use Stringable; +use UnitEnum; /** * @template TKey of array-key @@ -16,4 +20,146 @@ class Collection extends BaseCollection { use TransformsToResourceCollection; + + /** + * Group an associative array by a field or using a callback. + * + * Supports UnitEnum and Stringable keys, converting them to array keys. + */ + public function groupBy(mixed $groupBy, bool $preserveKeys = false): Enumerable + { + if (is_array($groupBy)) { + $nextGroups = $groupBy; + $groupBy = array_shift($nextGroups); + } + + $groupBy = $this->valueRetriever($groupBy); + $results = []; + + foreach ($this->items as $key => $value) { + $groupKeys = $groupBy($value, $key); + + if (! is_array($groupKeys)) { + $groupKeys = [$groupKeys]; + } + + foreach ($groupKeys as $groupKey) { + $groupKey = match (true) { + is_bool($groupKey) => (int) $groupKey, + $groupKey instanceof UnitEnum => enum_value($groupKey), + $groupKey instanceof Stringable => (string) $groupKey, + is_null($groupKey) => (string) $groupKey, + default => $groupKey, + }; + + if (! array_key_exists($groupKey, $results)) { + $results[$groupKey] = new static(); + } + + $results[$groupKey]->offsetSet($preserveKeys ? $key : null, $value); + } + } + + $result = new static($results); + + if (! empty($nextGroups)) { + return $result->map->groupBy($nextGroups, $preserveKeys); + } + + return $result; + } + + /** + * Key an associative array by a field or using a callback. + * + * Supports UnitEnum keys, converting them to array keys via enum_value(). + */ + public function keyBy(mixed $keyBy): static + { + $keyBy = $this->valueRetriever($keyBy); + $results = []; + + foreach ($this->items as $key => $item) { + $resolvedKey = $keyBy($item, $key); + + if ($resolvedKey instanceof UnitEnum) { + $resolvedKey = enum_value($resolvedKey); + } + + if (is_object($resolvedKey)) { + $resolvedKey = (string) $resolvedKey; + } + + $results[$resolvedKey] = $item; + } + + return new static($results); + } + + /** + * Results array of items from Collection or Arrayable. + * + * @return array + */ + protected function getArrayableItems(mixed $items): array + { + if ($items instanceof UnitEnum) { + return [$items]; + } + + return parent::getArrayableItems($items); + } + + /** + * Get an operator checker callback. + * + * @param callable|string $key + * @param null|string $operator + */ + protected function operatorForWhere(mixed $key, mixed $operator = null, mixed $value = null): callable|Closure + { + if ($this->useAsCallable($key)) { + return $key; + } + + if (func_num_args() === 1) { + $value = true; + $operator = '='; + } + + if (func_num_args() === 2) { + $value = $operator; + $operator = '='; + } + + return function ($item) use ($key, $operator, $value) { + $retrieved = enum_value(data_get($item, $key)); + $value = enum_value($value); + + $strings = array_filter([$retrieved, $value], function ($value) { + return match (true) { + is_string($value) => true, + $value instanceof Stringable => true, + default => false, + }; + }); + + if (count($strings) < 2 && count(array_filter([$retrieved, $value], 'is_object')) == 1) { + return in_array($operator, ['!=', '<>', '!==']); + } + + return match ($operator) { + '=', '==' => $retrieved == $value, + '!=', '<>' => $retrieved != $value, + '<' => $retrieved < $value, + '>' => $retrieved > $value, + '<=' => $retrieved <= $value, + '>=' => $retrieved >= $value, + '===' => $retrieved === $value, + '!==' => $retrieved !== $value, + '<=>' => $retrieved <=> $value, + default => $retrieved == $value, + }; + }; + } } diff --git a/tests/Auth/Access/GateEnumTest.php b/tests/Auth/Access/GateEnumTest.php index 97698c674..e759df0d0 100644 --- a/tests/Auth/Access/GateEnumTest.php +++ b/tests/Auth/Access/GateEnumTest.php @@ -313,9 +313,9 @@ public function testUnitEnumAndStringInteroperability(): void public function testAuthorizeMiddlewareUsingWithBackedEnum(): void { - $result = Authorize::using(AbilitiesBackedEnum::ViewDashboard, 'App\\Models\\Post'); + $result = Authorize::using(AbilitiesBackedEnum::ViewDashboard, 'App\Models\Post'); - $this->assertSame(Authorize::class . ':view-dashboard,App\\Models\\Post', $result); + $this->assertSame(Authorize::class . ':view-dashboard,App\Models\Post', $result); } public function testAuthorizeMiddlewareUsingWithUnitEnum(): void diff --git a/tests/Support/CollectionEnumTest.php b/tests/Support/CollectionEnumTest.php new file mode 100644 index 000000000..cede0f8a5 --- /dev/null +++ b/tests/Support/CollectionEnumTest.php @@ -0,0 +1,290 @@ +assertEquals([TestUnitEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testCollectionFromBackedEnum(): void + { + $data = new Collection(TestBackedEnum::Foo); + + $this->assertEquals([TestBackedEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testCollectionFromStringBackedEnum(): void + { + $data = new Collection(TestStringBackedEnum::Foo); + + $this->assertEquals([TestStringBackedEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testGroupByWithUnitEnumKey(): void + { + $data = new Collection([ + ['name' => TestUnitEnum::Foo, 'value' => 1], + ['name' => TestUnitEnum::Foo, 'value' => 2], + ['name' => TestUnitEnum::Bar, 'value' => 3], + ]); + + $result = $data->groupBy('name'); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + $this->assertCount(2, $result->get('Foo')); + $this->assertCount(1, $result->get('Bar')); + } + + public function testGroupByWithBackedEnumKey(): void + { + $data = new Collection([ + ['rating' => TestBackedEnum::Foo, 'url' => '1'], + ['rating' => TestBackedEnum::Bar, 'url' => '2'], + ]); + + $result = $data->groupBy('rating'); + + $expected = [ + TestBackedEnum::Foo->value => [['rating' => TestBackedEnum::Foo, 'url' => '1']], + TestBackedEnum::Bar->value => [['rating' => TestBackedEnum::Bar, 'url' => '2']], + ]; + + $this->assertEquals($expected, $result->toArray()); + } + + public function testGroupByWithStringBackedEnumKey(): void + { + $data = new Collection([ + ['category' => TestStringBackedEnum::Foo, 'value' => 1], + ['category' => TestStringBackedEnum::Foo, 'value' => 2], + ['category' => TestStringBackedEnum::Bar, 'value' => 3], + ]); + + $result = $data->groupBy('category'); + + $this->assertArrayHasKey(TestStringBackedEnum::Foo->value, $result->toArray()); + $this->assertArrayHasKey(TestStringBackedEnum::Bar->value, $result->toArray()); + } + + public function testGroupByWithCallableReturningEnum(): void + { + $data = new Collection([ + ['value' => 1], + ['value' => 2], + ['value' => 3], + ]); + + $result = $data->groupBy(fn ($item) => $item['value'] <= 2 ? TestUnitEnum::Foo : TestUnitEnum::Bar); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + $this->assertCount(2, $result->get('Foo')); + $this->assertCount(1, $result->get('Bar')); + } + + public function testKeyByWithUnitEnumKey(): void + { + $data = new Collection([ + ['name' => TestUnitEnum::Foo, 'value' => 1], + ['name' => TestUnitEnum::Bar, 'value' => 2], + ]); + + $result = $data->keyBy('name'); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + $this->assertEquals(1, $result->get('Foo')['value']); + $this->assertEquals(2, $result->get('Bar')['value']); + } + + public function testKeyByWithBackedEnumKey(): void + { + $data = new Collection([ + ['rating' => TestBackedEnum::Foo, 'value' => 'first'], + ['rating' => TestBackedEnum::Bar, 'value' => 'second'], + ]); + + $result = $data->keyBy('rating'); + + $this->assertArrayHasKey(TestBackedEnum::Foo->value, $result->toArray()); + $this->assertArrayHasKey(TestBackedEnum::Bar->value, $result->toArray()); + } + + public function testKeyByWithCallableReturningEnum(): void + { + $data = new Collection([ + ['id' => 1, 'value' => 'first'], + ['id' => 2, 'value' => 'second'], + ]); + + $result = $data->keyBy(fn ($item) => $item['id'] === 1 ? TestUnitEnum::Foo : TestUnitEnum::Bar); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + } + + public function testWhereWithEnumValue(): void + { + $data = new Collection([ + ['id' => 1, 'status' => TestBackedEnum::Foo], + ['id' => 2, 'status' => TestBackedEnum::Bar], + ['id' => 3, 'status' => TestBackedEnum::Foo], + ]); + + $result = $data->where('status', TestBackedEnum::Foo); + + $this->assertCount(2, $result); + $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); + } + + public function testWhereWithUnitEnumValue(): void + { + $data = new Collection([ + ['id' => 1, 'type' => TestUnitEnum::Foo], + ['id' => 2, 'type' => TestUnitEnum::Bar], + ['id' => 3, 'type' => TestUnitEnum::Foo], + ]); + + $result = $data->where('type', TestUnitEnum::Foo); + + $this->assertCount(2, $result); + $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); + } + + public function testFirstWhereWithEnum(): void + { + $data = new Collection([ + ['id' => 1, 'name' => TestUnitEnum::Foo], + ['id' => 2, 'name' => TestUnitEnum::Bar], + ['id' => 3, 'name' => TestUnitEnum::Baz], + ]); + + $this->assertSame(2, $data->firstWhere('name', TestUnitEnum::Bar)['id']); + $this->assertSame(3, $data->firstWhere('name', TestUnitEnum::Baz)['id']); + } + + public function testMapIntoWithIntBackedEnum(): void + { + $data = new Collection([1, 2]); + + $result = $data->mapInto(TestBackedEnum::class); + + $this->assertSame(TestBackedEnum::Foo, $result->get(0)); + $this->assertSame(TestBackedEnum::Bar, $result->get(1)); + } + + public function testMapIntoWithStringBackedEnum(): void + { + $data = new Collection(['foo', 'bar']); + + $result = $data->mapInto(TestStringBackedEnum::class); + + $this->assertSame(TestStringBackedEnum::Foo, $result->get(0)); + $this->assertSame(TestStringBackedEnum::Bar, $result->get(1)); + } + + public function testCollectHelperWithUnitEnum(): void + { + $data = collect(TestUnitEnum::Foo); + + $this->assertEquals([TestUnitEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testCollectHelperWithBackedEnum(): void + { + $data = collect(TestBackedEnum::Bar); + + $this->assertEquals([TestBackedEnum::Bar], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testWhereStrictWithEnums(): void + { + $data = new Collection([ + ['id' => 1, 'status' => TestBackedEnum::Foo], + ['id' => 2, 'status' => TestBackedEnum::Bar], + ]); + + $result = $data->whereStrict('status', TestBackedEnum::Foo); + + $this->assertCount(1, $result); + $this->assertEquals(1, $result->first()['id']); + } + + public function testEnumValuesArePreservedInCollection(): void + { + $data = new Collection([TestUnitEnum::Foo, TestBackedEnum::Bar, TestStringBackedEnum::Baz]); + + $this->assertSame(TestUnitEnum::Foo, $data->get(0)); + $this->assertSame(TestBackedEnum::Bar, $data->get(1)); + $this->assertSame(TestStringBackedEnum::Baz, $data->get(2)); + } + + public function testContainsWithEnum(): void + { + $data = new Collection([TestUnitEnum::Foo, TestUnitEnum::Bar]); + + $this->assertTrue($data->contains(TestUnitEnum::Foo)); + $this->assertTrue($data->contains(TestUnitEnum::Bar)); + $this->assertFalse($data->contains(TestUnitEnum::Baz)); + } + + public function testGroupByMixedEnumTypes(): void + { + $payload = [ + ['name' => TestUnitEnum::Foo, 'url' => '1'], + ['name' => TestBackedEnum::Foo, 'url' => '1'], + ['name' => TestStringBackedEnum::Foo, 'url' => '2'], + ]; + + $data = new Collection($payload); + $result = $data->groupBy('name'); + + // UnitEnum uses name ('Foo'), IntBackedEnum uses value (1), StringBackedEnum uses value ('foo') + $this->assertEquals([ + 'Foo' => [$payload[0]], + 1 => [$payload[1]], + 'foo' => [$payload[2]], + ], $result->toArray()); + } +} + +enum TestUnitEnum +{ + case Foo; + case Bar; + case Baz; +} + +enum TestBackedEnum: int +{ + case Foo = 1; + case Bar = 2; + case Baz = 3; +} + +enum TestStringBackedEnum: string +{ + case Foo = 'foo'; + case Bar = 'bar'; + case Baz = 'baz'; +} diff --git a/tests/Support/CollectionTest.php b/tests/Support/CollectionTest.php new file mode 100644 index 000000000..0e3ec43bd --- /dev/null +++ b/tests/Support/CollectionTest.php @@ -0,0 +1,299 @@ + 'fruit', 'name' => 'apple'], + ['category' => 'fruit', 'name' => 'banana'], + ['category' => 'vegetable', 'name' => 'carrot'], + ]); + + $result = $data->groupBy('category'); + + $this->assertArrayHasKey('fruit', $result->toArray()); + $this->assertArrayHasKey('vegetable', $result->toArray()); + $this->assertCount(2, $result->get('fruit')); + $this->assertCount(1, $result->get('vegetable')); + } + + public function testGroupByWithIntKey(): void + { + $data = new Collection([ + ['rating' => 5, 'name' => 'excellent'], + ['rating' => 5, 'name' => 'great'], + ['rating' => 3, 'name' => 'average'], + ]); + + $result = $data->groupBy('rating'); + + $this->assertArrayHasKey(5, $result->toArray()); + $this->assertArrayHasKey(3, $result->toArray()); + $this->assertCount(2, $result->get(5)); + $this->assertCount(1, $result->get(3)); + } + + public function testGroupByWithCallback(): void + { + $data = new Collection([ + ['name' => 'Alice', 'age' => 25], + ['name' => 'Bob', 'age' => 30], + ['name' => 'Charlie', 'age' => 25], + ]); + + $result = $data->groupBy(fn ($item) => $item['age']); + + $this->assertArrayHasKey(25, $result->toArray()); + $this->assertArrayHasKey(30, $result->toArray()); + $this->assertCount(2, $result->get(25)); + } + + public function testGroupByWithBoolKey(): void + { + $data = new Collection([ + ['active' => true, 'name' => 'Alice'], + ['active' => false, 'name' => 'Bob'], + ['active' => true, 'name' => 'Charlie'], + ]); + + $result = $data->groupBy('active'); + + // Bool keys are converted to int (true => 1, false => 0) + $this->assertArrayHasKey(1, $result->toArray()); + $this->assertArrayHasKey(0, $result->toArray()); + $this->assertCount(2, $result->get(1)); + $this->assertCount(1, $result->get(0)); + } + + public function testGroupByWithNullKey(): void + { + $data = new Collection([ + ['category' => 'fruit', 'name' => 'apple'], + ['category' => null, 'name' => 'unknown'], + ]); + + $result = $data->groupBy('category'); + + $this->assertArrayHasKey('fruit', $result->toArray()); + $this->assertArrayHasKey('', $result->toArray()); // null becomes empty string + } + + public function testGroupByWithStringableKey(): void + { + $data = new Collection([ + ['id' => new CollectionTestStringable('group-a'), 'value' => 1], + ['id' => new CollectionTestStringable('group-a'), 'value' => 2], + ['id' => new CollectionTestStringable('group-b'), 'value' => 3], + ]); + + $result = $data->groupBy('id'); + + $this->assertArrayHasKey('group-a', $result->toArray()); + $this->assertArrayHasKey('group-b', $result->toArray()); + $this->assertCount(2, $result->get('group-a')); + } + + public function testGroupByPreservesKeys(): void + { + $data = new Collection([ + 10 => ['category' => 'a', 'value' => 1], + 20 => ['category' => 'a', 'value' => 2], + 30 => ['category' => 'b', 'value' => 3], + ]); + + $result = $data->groupBy('category', true); + + $this->assertEquals([10, 20], array_keys($result->get('a')->toArray())); + $this->assertEquals([30], array_keys($result->get('b')->toArray())); + } + + public function testGroupByWithNestedGroups(): void + { + $data = new Collection([ + ['type' => 'fruit', 'color' => 'red', 'name' => 'apple'], + ['type' => 'fruit', 'color' => 'yellow', 'name' => 'banana'], + ['type' => 'vegetable', 'color' => 'red', 'name' => 'tomato'], + ]); + + $result = $data->groupBy(['type', 'color']); + + $this->assertArrayHasKey('fruit', $result->toArray()); + $this->assertArrayHasKey('red', $result->get('fruit')->toArray()); + $this->assertArrayHasKey('yellow', $result->get('fruit')->toArray()); + } + + public function testKeyByWithStringKey(): void + { + $data = new Collection([ + ['id' => 'user-1', 'name' => 'Alice'], + ['id' => 'user-2', 'name' => 'Bob'], + ]); + + $result = $data->keyBy('id'); + + $this->assertArrayHasKey('user-1', $result->toArray()); + $this->assertArrayHasKey('user-2', $result->toArray()); + $this->assertEquals('Alice', $result->get('user-1')['name']); + } + + public function testKeyByWithIntKey(): void + { + $data = new Collection([ + ['id' => 100, 'name' => 'Alice'], + ['id' => 200, 'name' => 'Bob'], + ]); + + $result = $data->keyBy('id'); + + $this->assertArrayHasKey(100, $result->toArray()); + $this->assertArrayHasKey(200, $result->toArray()); + } + + public function testKeyByWithCallback(): void + { + $data = new Collection([ + ['first' => 'Alice', 'last' => 'Smith'], + ['first' => 'Bob', 'last' => 'Jones'], + ]); + + $result = $data->keyBy(fn ($item) => $item['first'] . '_' . $item['last']); + + $this->assertArrayHasKey('Alice_Smith', $result->toArray()); + $this->assertArrayHasKey('Bob_Jones', $result->toArray()); + } + + public function testKeyByWithStringableKey(): void + { + $data = new Collection([ + ['id' => new CollectionTestStringable('key-1'), 'value' => 'first'], + ['id' => new CollectionTestStringable('key-2'), 'value' => 'second'], + ]); + + $result = $data->keyBy('id'); + + $this->assertArrayHasKey('key-1', $result->toArray()); + $this->assertArrayHasKey('key-2', $result->toArray()); + } + + public function testWhereWithStringValue(): void + { + $data = new Collection([ + ['id' => 1, 'status' => 'active'], + ['id' => 2, 'status' => 'inactive'], + ['id' => 3, 'status' => 'active'], + ]); + + $result = $data->where('status', 'active'); + + $this->assertCount(2, $result); + $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); + } + + public function testWhereWithIntValue(): void + { + $data = new Collection([ + ['id' => 1, 'count' => 10], + ['id' => 2, 'count' => 20], + ['id' => 3, 'count' => 10], + ]); + + $result = $data->where('count', 10); + + $this->assertCount(2, $result); + } + + public function testWhereWithOperator(): void + { + $data = new Collection([ + ['id' => 1, 'price' => 100], + ['id' => 2, 'price' => 200], + ['id' => 3, 'price' => 300], + ]); + + $this->assertCount(2, $data->where('price', '>', 100)); + $this->assertCount(2, $data->where('price', '>=', 200)); + $this->assertCount(1, $data->where('price', '<', 200)); + $this->assertCount(2, $data->where('price', '!=', 200)); + } + + public function testWhereStrictWithTypes(): void + { + $data = new Collection([ + ['id' => 1, 'value' => '10'], + ['id' => 2, 'value' => 10], + ]); + + // Strict comparison - string '10' !== int 10 + $result = $data->whereStrict('value', 10); + + $this->assertCount(1, $result); + $this->assertEquals(2, $result->first()['id']); + } + + public function testGetArrayableItemsWithNull(): void + { + $data = new Collection(null); + + $this->assertEquals([], $data->toArray()); + } + + public function testGetArrayableItemsWithScalar(): void + { + // String + $data = new Collection('hello'); + $this->assertEquals(['hello'], $data->toArray()); + + // Int + $data = new Collection(42); + $this->assertEquals([42], $data->toArray()); + + // Bool + $data = new Collection(true); + $this->assertEquals([true], $data->toArray()); + } + + public function testGetArrayableItemsWithArray(): void + { + $data = new Collection(['a', 'b', 'c']); + + $this->assertEquals(['a', 'b', 'c'], $data->toArray()); + } + + public function testOperatorForWhereWithNestedData(): void + { + $data = new Collection([ + ['user' => ['name' => 'Alice', 'age' => 25]], + ['user' => ['name' => 'Bob', 'age' => 30]], + ]); + + $result = $data->where('user.name', 'Alice'); + + $this->assertCount(1, $result); + $this->assertEquals(25, $result->first()['user']['age']); + } +} + +class CollectionTestStringable implements Stringable +{ + public function __construct(private string $value) + { + } + + public function __toString(): string + { + return $this->value; + } +} From e7b5ee65a43a078fb573dd86fbf07a8b32866325 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:01:51 +0000 Subject: [PATCH 06/19] Add BackedEnum and UnitEnum support to Cache Repository Add enum support to Cache Repository key methods (get, put, add, increment, decrement, forever, forget, remember, etc.) and their child classes (TaggedCache, RedisTaggedCache). Also adds enum support for cache tags, allowing enums to be used as tag names - a feature Laravel doesn't have yet. --- src/cache/src/RedisTaggedCache.php | 24 +- src/cache/src/Repository.php | 61 ++-- src/cache/src/TaggedCache.php | 12 +- tests/Cache/CacheRepositoryEnumTest.php | 403 ++++++++++++++++++++++++ 4 files changed, 469 insertions(+), 31 deletions(-) create mode 100644 tests/Cache/CacheRepositoryEnumTest.php diff --git a/src/cache/src/RedisTaggedCache.php b/src/cache/src/RedisTaggedCache.php index ae678f86b..398c7bbca 100644 --- a/src/cache/src/RedisTaggedCache.php +++ b/src/cache/src/RedisTaggedCache.php @@ -4,9 +4,13 @@ namespace Hypervel\Cache; +use BackedEnum; use DateInterval; use DateTimeInterface; use Hypervel\Cache\Contracts\Store; +use UnitEnum; + +use function Hypervel\Support\enum_value; class RedisTaggedCache extends TaggedCache { @@ -27,8 +31,10 @@ class RedisTaggedCache extends TaggedCache /** * Store an item in the cache if the key does not exist. */ - public function add(string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function add(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { + $key = enum_value($key); + $this->tags->addEntry( $this->itemKey($key), ! is_null($ttl) ? $this->getSeconds($ttl) : 0 @@ -40,12 +46,14 @@ public function add(string $key, mixed $value, DateInterval|DateTimeInterface|in /** * Store an item in the cache. */ - public function put(array|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function put(array|BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { if (is_array($key)) { return $this->putMany($key, $value); } + $key = enum_value($key); + if (is_null($ttl)) { return $this->forever($key, $value); } @@ -61,8 +69,10 @@ public function put(array|string $key, mixed $value, DateInterval|DateTimeInterf /** * Increment the value of an item in the cache. */ - public function increment(string $key, int $value = 1): bool|int + public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int { + $key = enum_value($key); + $this->tags->addEntry($this->itemKey($key), updateWhen: 'NX'); return parent::increment($key, $value); @@ -71,8 +81,10 @@ public function increment(string $key, int $value = 1): bool|int /** * Decrement the value of an item in the cache. */ - public function decrement(string $key, int $value = 1): bool|int + public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int { + $key = enum_value($key); + $this->tags->addEntry($this->itemKey($key), updateWhen: 'NX'); return parent::decrement($key, $value); @@ -81,8 +93,10 @@ public function decrement(string $key, int $value = 1): bool|int /** * Store an item in the cache indefinitely. */ - public function forever(string $key, mixed $value): bool + public function forever(BackedEnum|UnitEnum|string $key, mixed $value): bool { + $key = enum_value($key); + $this->tags->addEntry($this->itemKey($key)); return parent::forever($key, $value); diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 8a772f2da..0d41a1088 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -5,6 +5,7 @@ namespace Hypervel\Cache; use ArrayAccess; +use BackedEnum; use BadMethodCallException; use Carbon\Carbon; use Closure; @@ -29,6 +30,9 @@ use Hypervel\Cache\Events\WritingKey; use Hypervel\Cache\Events\WritingManyKeys; use Psr\EventDispatcher\EventDispatcherInterface; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @mixin \Hypervel\Cache\Contracts\Store @@ -92,7 +96,7 @@ public function __clone() /** * Determine if an item exists in the cache. */ - public function has(array|string $key): bool + public function has(array|BackedEnum|UnitEnum|string $key): bool { return ! is_null($this->get($key)); } @@ -100,7 +104,7 @@ public function has(array|string $key): bool /** * Determine if an item doesn't exist in the cache. */ - public function missing(string $key): bool + public function missing(BackedEnum|UnitEnum|string $key): bool { return ! $this->has($key); } @@ -114,12 +118,14 @@ public function missing(string $key): bool * * @return (TCacheValue is null ? mixed : TCacheValue) */ - public function get(array|string $key, mixed $default = null): mixed + public function get(array|BackedEnum|UnitEnum|string $key, mixed $default = null): mixed { if (is_array($key)) { return $this->many($key); } + $key = enum_value($key); + $this->event(new RetrievingKey($this->getName(), $key)); $value = $this->store->get($this->itemKey($key)); @@ -177,7 +183,7 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable * * @return (TCacheValue is null ? mixed : TCacheValue) */ - public function pull(string $key, mixed $default = null): mixed + public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed { return tap($this->get($key, $default), function () use ($key) { $this->forget($key); @@ -187,12 +193,14 @@ public function pull(string $key, mixed $default = null): mixed /** * Store an item in the cache. */ - public function put(array|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function put(array|BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { if (is_array($key)) { return $this->putMany($key, $value); } + $key = enum_value($key); + if ($ttl === null) { return $this->forever($key, $value); } @@ -218,7 +226,7 @@ public function put(array|string $key, mixed $value, DateInterval|DateTimeInterf /** * Store an item in the cache. */ - public function set(string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function set(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { return $this->put($key, $value, $ttl); } @@ -261,8 +269,10 @@ public function setMultiple(iterable $values, DateInterval|DateTimeInterface|int /** * Store an item in the cache if the key does not exist. */ - public function add(string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function add(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { + $key = enum_value($key); + $seconds = null; if ($ttl !== null) { @@ -297,24 +307,26 @@ public function add(string $key, mixed $value, DateInterval|DateTimeInterface|in /** * Increment the value of an item in the cache. */ - public function increment(string $key, int $value = 1): bool|int + public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->increment($key, $value); + return $this->store->increment(enum_value($key), $value); } /** * Decrement the value of an item in the cache. */ - public function decrement(string $key, int $value = 1): bool|int + public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->decrement($key, $value); + return $this->store->decrement(enum_value($key), $value); } /** * Store an item in the cache indefinitely. */ - public function forever(string $key, mixed $value): bool + public function forever(BackedEnum|UnitEnum|string $key, mixed $value): bool { + $key = enum_value($key); + $this->event(new WritingKey($this->getName(), $key, $value)); $result = $this->store->forever($this->itemKey($key), $value); @@ -337,7 +349,7 @@ public function forever(string $key, mixed $value): bool * * @return TCacheValue */ - public function remember(string $key, DateInterval|DateTimeInterface|int|null $ttl, Closure $callback): mixed + public function remember(BackedEnum|UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl, Closure $callback): mixed { $value = $this->get($key); @@ -362,7 +374,7 @@ public function remember(string $key, DateInterval|DateTimeInterface|int|null $t * * @return TCacheValue */ - public function sear(string $key, Closure $callback): mixed + public function sear(BackedEnum|UnitEnum|string $key, Closure $callback): mixed { return $this->rememberForever($key, $callback); } @@ -376,7 +388,7 @@ public function sear(string $key, Closure $callback): mixed * * @return TCacheValue */ - public function rememberForever(string $key, Closure $callback): mixed + public function rememberForever(BackedEnum|UnitEnum|string $key, Closure $callback): mixed { $value = $this->get($key); @@ -395,8 +407,10 @@ public function rememberForever(string $key, Closure $callback): mixed /** * Remove an item from the cache. */ - public function forget(string $key): bool + public function forget(BackedEnum|UnitEnum|string $key): bool { + $key = enum_value($key); + $this->event(new ForgettingKey($this->getName(), $key)); return tap($this->store->forget($this->itemKey($key)), function ($result) use ($key) { @@ -408,7 +422,7 @@ public function forget(string $key): bool }); } - public function delete(string $key): bool + public function delete(BackedEnum|UnitEnum|string $key): bool { return $this->forget($key); } @@ -452,8 +466,11 @@ public function tags(mixed $names): TaggedCache throw new BadMethodCallException('This cache store does not support tagging.'); } + $names = is_array($names) ? $names : func_get_args(); + $names = array_map(fn ($name) => enum_value($name), $names); + /* @phpstan-ignore-next-line */ - $cache = $this->store->tags(is_array($names) ? $names : func_get_args()); + $cache = $this->store->tags($names); if (! is_null($this->events)) { $cache->setEventDispatcher($this->events); @@ -523,7 +540,7 @@ public function getName(): ?string /** * Determine if a cached value exists. * - * @param string $key + * @param BackedEnum|string|UnitEnum $key */ public function offsetExists($key): bool { @@ -533,7 +550,7 @@ public function offsetExists($key): bool /** * Retrieve an item from the cache by key. * - * @param string $key + * @param BackedEnum|string|UnitEnum $key */ public function offsetGet($key): mixed { @@ -543,7 +560,7 @@ public function offsetGet($key): mixed /** * Store an item in the cache for the default time. * - * @param string $key + * @param BackedEnum|string|UnitEnum $key * @param mixed $value */ public function offsetSet($key, $value): void @@ -554,7 +571,7 @@ public function offsetSet($key, $value): void /** * Remove an item from the cache. * - * @param string $key + * @param BackedEnum|string|UnitEnum $key */ public function offsetUnset($key): void { diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php index ba70fb5df..c42e00a97 100644 --- a/src/cache/src/TaggedCache.php +++ b/src/cache/src/TaggedCache.php @@ -4,11 +4,15 @@ namespace Hypervel\Cache; +use BackedEnum; use DateInterval; use DateTimeInterface; use Hypervel\Cache\Contracts\Store; use Hypervel\Cache\Events\CacheFlushed; use Hypervel\Cache\Events\CacheFlushing; +use UnitEnum; + +use function Hypervel\Support\enum_value; class TaggedCache extends Repository { @@ -46,17 +50,17 @@ public function putMany(array $values, DateInterval|DateTimeInterface|int|null $ /** * Increment the value of an item in the cache. */ - public function increment(string $key, int $value = 1): bool|int + public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->increment($this->itemKey($key), $value); + return $this->store->increment($this->itemKey(enum_value($key)), $value); } /** * Decrement the value of an item in the cache. */ - public function decrement(string $key, int $value = 1): bool|int + public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->decrement($this->itemKey($key), $value); + return $this->store->decrement($this->itemKey(enum_value($key)), $value); } /** diff --git a/tests/Cache/CacheRepositoryEnumTest.php b/tests/Cache/CacheRepositoryEnumTest.php new file mode 100644 index 000000000..45e5dbe34 --- /dev/null +++ b/tests/Cache/CacheRepositoryEnumTest.php @@ -0,0 +1,403 @@ +getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('cached-value'); + + $this->assertSame('cached-value', $repo->get(CacheKeyBackedEnum::UserProfile)); + } + + public function testGetWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn('dashboard-data'); + + $this->assertSame('dashboard-data', $repo->get(CacheKeyUnitEnum::Dashboard)); + } + + public function testHasWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('value'); + + $this->assertTrue($repo->has(CacheKeyBackedEnum::UserProfile)); + } + + public function testHasWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn(null); + + $this->assertFalse($repo->has(CacheKeyUnitEnum::Dashboard)); + } + + public function testMissingWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('settings')->andReturn(null); + + $this->assertTrue($repo->missing(CacheKeyBackedEnum::Settings)); + } + + public function testPutWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('put')->once()->with('user-profile', 'value', 60)->andReturn(true); + + $this->assertTrue($repo->put(CacheKeyBackedEnum::UserProfile, 'value', 60)); + } + + public function testPutWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('put')->once()->with('Dashboard', 'data', 120)->andReturn(true); + + $this->assertTrue($repo->put(CacheKeyUnitEnum::Dashboard, 'data', 120)); + } + + public function testSetWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('put')->once()->with('settings', 'config', 300)->andReturn(true); + + $this->assertTrue($repo->set(CacheKeyBackedEnum::Settings, 'config', 300)); + } + + public function testAddWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn(null); + $repo->getStore()->shouldReceive('put')->once()->with('user-profile', 'new-value', 60)->andReturn(true); + + $this->assertTrue($repo->add(CacheKeyBackedEnum::UserProfile, 'new-value', 60)); + } + + public function testAddWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Analytics')->andReturn(null); + $repo->getStore()->shouldReceive('put')->once()->with('Analytics', 'data', 60)->andReturn(true); + + $this->assertTrue($repo->add(CacheKeyUnitEnum::Analytics, 'data', 60)); + } + + public function testIncrementWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('increment')->once()->with('user-profile', 1)->andReturn(2); + + $this->assertSame(2, $repo->increment(CacheKeyBackedEnum::UserProfile)); + } + + public function testIncrementWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('increment')->once()->with('Dashboard', 5)->andReturn(10); + + $this->assertSame(10, $repo->increment(CacheKeyUnitEnum::Dashboard, 5)); + } + + public function testDecrementWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('decrement')->once()->with('settings', 1)->andReturn(4); + + $this->assertSame(4, $repo->decrement(CacheKeyBackedEnum::Settings)); + } + + public function testDecrementWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('decrement')->once()->with('Analytics', 3)->andReturn(7); + + $this->assertSame(7, $repo->decrement(CacheKeyUnitEnum::Analytics, 3)); + } + + public function testForeverWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forever')->once()->with('user-profile', 'permanent')->andReturn(true); + + $this->assertTrue($repo->forever(CacheKeyBackedEnum::UserProfile, 'permanent')); + } + + public function testForeverWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forever')->once()->with('Dashboard', 'forever-data')->andReturn(true); + + $this->assertTrue($repo->forever(CacheKeyUnitEnum::Dashboard, 'forever-data')); + } + + public function testRememberWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('settings')->andReturn(null); + $repo->getStore()->shouldReceive('put')->once()->with('settings', 'computed', 60)->andReturn(true); + + $result = $repo->remember(CacheKeyBackedEnum::Settings, 60, fn () => 'computed'); + + $this->assertSame('computed', $result); + } + + public function testRememberWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Analytics')->andReturn(null); + $repo->getStore()->shouldReceive('put')->once()->with('Analytics', 'analytics-data', 120)->andReturn(true); + + $result = $repo->remember(CacheKeyUnitEnum::Analytics, 120, fn () => 'analytics-data'); + + $this->assertSame('analytics-data', $result); + } + + public function testRememberForeverWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn(null); + $repo->getStore()->shouldReceive('forever')->once()->with('user-profile', 'forever-value')->andReturn(true); + + $result = $repo->rememberForever(CacheKeyBackedEnum::UserProfile, fn () => 'forever-value'); + + $this->assertSame('forever-value', $result); + } + + public function testSearWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn(null); + $repo->getStore()->shouldReceive('forever')->once()->with('Dashboard', 'seared')->andReturn(true); + + $result = $repo->sear(CacheKeyUnitEnum::Dashboard, fn () => 'seared'); + + $this->assertSame('seared', $result); + } + + public function testForgetWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forget')->once()->with('user-profile')->andReturn(true); + + $this->assertTrue($repo->forget(CacheKeyBackedEnum::UserProfile)); + } + + public function testForgetWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forget')->once()->with('Dashboard')->andReturn(true); + + $this->assertTrue($repo->forget(CacheKeyUnitEnum::Dashboard)); + } + + public function testDeleteWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forget')->once()->with('settings')->andReturn(true); + + $this->assertTrue($repo->delete(CacheKeyBackedEnum::Settings)); + } + + public function testPullWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('pulled-value'); + $repo->getStore()->shouldReceive('forget')->once()->with('user-profile')->andReturn(true); + + $this->assertSame('pulled-value', $repo->pull(CacheKeyBackedEnum::UserProfile)); + } + + public function testPullWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Analytics')->andReturn('analytics'); + $repo->getStore()->shouldReceive('forget')->once()->with('Analytics')->andReturn(true); + + $this->assertSame('analytics', $repo->pull(CacheKeyUnitEnum::Analytics)); + } + + public function testBackedEnumAndStringInteroperability(): void + { + $repo = new Repository(new ArrayStore()); + + // Store with enum + $repo->put(CacheKeyBackedEnum::UserProfile, 'enum-stored', 60); + + // Retrieve with string (the enum value) + $this->assertSame('enum-stored', $repo->get('user-profile')); + + // Store with string + $repo->put('settings', 'string-stored', 60); + + // Retrieve with enum + $this->assertSame('string-stored', $repo->get(CacheKeyBackedEnum::Settings)); + } + + public function testUnitEnumAndStringInteroperability(): void + { + $repo = new Repository(new ArrayStore()); + + // Store with enum + $repo->put(CacheKeyUnitEnum::Dashboard, 'enum-stored', 60); + + // Retrieve with string (the enum name) + $this->assertSame('enum-stored', $repo->get('Dashboard')); + + // Store with string + $repo->put('Analytics', 'string-stored', 60); + + // Retrieve with enum + $this->assertSame('string-stored', $repo->get(CacheKeyUnitEnum::Analytics)); + } + + public function testTagsWithBackedEnumArray(): void + { + $repo = new Repository(new ArrayStore()); + + $tagged = $repo->tags([CacheTagBackedEnum::Users, CacheTagBackedEnum::Posts]); + + $this->assertInstanceOf(TaggedCache::class, $tagged); + $this->assertEquals(['users', 'posts'], $tagged->getTags()->getNames()); + } + + public function testTagsWithUnitEnumArray(): void + { + $repo = new Repository(new ArrayStore()); + + $tagged = $repo->tags([CacheTagUnitEnum::Reports, CacheTagUnitEnum::Exports]); + + $this->assertInstanceOf(TaggedCache::class, $tagged); + $this->assertEquals(['Reports', 'Exports'], $tagged->getTags()->getNames()); + } + + public function testTagsWithMixedEnumsAndStrings(): void + { + $repo = new Repository(new ArrayStore()); + + $tagged = $repo->tags([CacheTagBackedEnum::Users, 'custom-tag', CacheTagUnitEnum::Reports]); + + $this->assertInstanceOf(TaggedCache::class, $tagged); + $this->assertEquals(['users', 'custom-tag', 'Reports'], $tagged->getTags()->getNames()); + } + + public function testTagsWithBackedEnumVariadicArgs(): void + { + $store = m::mock(ArrayStore::class); + $repo = new Repository($store); + + $taggedCache = m::mock(TaggedCache::class); + $taggedCache->shouldReceive('setDefaultCacheTime')->andReturnSelf(); + $store->shouldReceive('tags')->once()->with(['users', 'posts'])->andReturn($taggedCache); + + $repo->tags(CacheTagBackedEnum::Users, CacheTagBackedEnum::Posts); + } + + public function testTagsWithUnitEnumVariadicArgs(): void + { + $store = m::mock(ArrayStore::class); + $repo = new Repository($store); + + $taggedCache = m::mock(TaggedCache::class); + $taggedCache->shouldReceive('setDefaultCacheTime')->andReturnSelf(); + $store->shouldReceive('tags')->once()->with(['Reports', 'Exports'])->andReturn($taggedCache); + + $repo->tags(CacheTagUnitEnum::Reports, CacheTagUnitEnum::Exports); + } + + public function testTaggedCacheOperationsWithEnumKeys(): void + { + $repo = new Repository(new ArrayStore()); + + $tagged = $repo->tags([CacheTagBackedEnum::Users]); + + // Put with enum key + $tagged->put(CacheKeyBackedEnum::UserProfile, 'tagged-value', 60); + + // Get with enum key + $this->assertSame('tagged-value', $tagged->get(CacheKeyBackedEnum::UserProfile)); + + // Get with string key (interoperability) + $this->assertSame('tagged-value', $tagged->get('user-profile')); + } + + public function testOffsetAccessWithBackedEnum(): void + { + $repo = new Repository(new ArrayStore()); + + // offsetSet with enum + $repo[CacheKeyBackedEnum::UserProfile] = 'offset-value'; + + // offsetGet with enum + $this->assertSame('offset-value', $repo[CacheKeyBackedEnum::UserProfile]); + + // offsetExists with enum + $this->assertTrue(isset($repo[CacheKeyBackedEnum::UserProfile])); + + // offsetUnset with enum + unset($repo[CacheKeyBackedEnum::UserProfile]); + $this->assertFalse(isset($repo[CacheKeyBackedEnum::UserProfile])); + } + + public function testOffsetAccessWithUnitEnum(): void + { + $repo = new Repository(new ArrayStore()); + + $repo[CacheKeyUnitEnum::Dashboard] = 'dashboard-data'; + + $this->assertSame('dashboard-data', $repo[CacheKeyUnitEnum::Dashboard]); + $this->assertTrue(isset($repo[CacheKeyUnitEnum::Dashboard])); + } + + protected function getRepository(): Repository + { + $dispatcher = m::mock(Dispatcher::class); + $dispatcher->shouldReceive('dispatch')->with(m::any())->andReturnNull(); + $repository = new Repository(m::mock(Store::class)); + + $repository->setEventDispatcher($dispatcher); + + return $repository; + } +} From 568584e6ea3173e560e7044430edc6968b6183fe Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:34:16 +0000 Subject: [PATCH 07/19] Add BackedEnum and UnitEnum support to Context Allow using enums as context keys for set, get, has, destroy, override, and getOrSet methods. Follows the same pattern as Session enum support. --- src/core/src/Context/Context.php | 53 ++++++++ tests/Core/ContextEnumTest.php | 221 +++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 tests/Core/ContextEnumTest.php diff --git a/src/core/src/Context/Context.php b/src/core/src/Context/Context.php index 9489ecdee..e48c3a82b 100644 --- a/src/core/src/Context/Context.php +++ b/src/core/src/Context/Context.php @@ -4,8 +4,13 @@ namespace Hypervel\Context; +use BackedEnum; +use Closure; use Hyperf\Context\Context as HyperfContext; use Hyperf\Engine\Coroutine; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Context extends HyperfContext { @@ -16,6 +21,54 @@ public function __call(string $method, array $arguments): mixed return static::{$method}(...$arguments); } + /** + * Set a value in the context. + */ + public static function set(BackedEnum|UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed + { + return parent::set(enum_value($id), $value, $coroutineId); + } + + /** + * Get a value from the context. + */ + public static function get(BackedEnum|UnitEnum|string $id, mixed $default = null, ?int $coroutineId = null): mixed + { + return parent::get(enum_value($id), $default, $coroutineId); + } + + /** + * Determine if a value exists in the context. + */ + public static function has(BackedEnum|UnitEnum|string $id, ?int $coroutineId = null): bool + { + return parent::has(enum_value($id), $coroutineId); + } + + /** + * Remove a value from the context. + */ + public static function destroy(BackedEnum|UnitEnum|string $id, ?int $coroutineId = null): void + { + parent::destroy(enum_value($id), $coroutineId); + } + + /** + * Retrieve the value and override it by closure. + */ + public static function override(BackedEnum|UnitEnum|string $id, Closure $closure, ?int $coroutineId = null): mixed + { + return parent::override(enum_value($id), $closure, $coroutineId); + } + + /** + * Retrieve the value and store it if not exists. + */ + public static function getOrSet(BackedEnum|UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed + { + return parent::getOrSet(enum_value($id), $value, $coroutineId); + } + /** * Set multiple key-value pairs in the context. */ diff --git a/tests/Core/ContextEnumTest.php b/tests/Core/ContextEnumTest.php new file mode 100644 index 000000000..533feea4d --- /dev/null +++ b/tests/Core/ContextEnumTest.php @@ -0,0 +1,221 @@ +assertSame('user-123', Context::get(ContextKeyBackedEnum::CurrentUser)); + } + + public function testSetAndGetWithUnitEnum(): void + { + Context::set(ContextKeyUnitEnum::Locale, 'en-US'); + + $this->assertSame('en-US', Context::get(ContextKeyUnitEnum::Locale)); + } + + public function testHasWithBackedEnum(): void + { + $this->assertFalse(Context::has(ContextKeyBackedEnum::CurrentUser)); + + Context::set(ContextKeyBackedEnum::CurrentUser, 'user-123'); + + $this->assertTrue(Context::has(ContextKeyBackedEnum::CurrentUser)); + } + + public function testHasWithUnitEnum(): void + { + $this->assertFalse(Context::has(ContextKeyUnitEnum::Locale)); + + Context::set(ContextKeyUnitEnum::Locale, 'en-US'); + + $this->assertTrue(Context::has(ContextKeyUnitEnum::Locale)); + } + + public function testDestroyWithBackedEnum(): void + { + Context::set(ContextKeyBackedEnum::CurrentUser, 'user-123'); + $this->assertTrue(Context::has(ContextKeyBackedEnum::CurrentUser)); + + Context::destroy(ContextKeyBackedEnum::CurrentUser); + + $this->assertFalse(Context::has(ContextKeyBackedEnum::CurrentUser)); + } + + public function testDestroyWithUnitEnum(): void + { + Context::set(ContextKeyUnitEnum::Locale, 'en-US'); + $this->assertTrue(Context::has(ContextKeyUnitEnum::Locale)); + + Context::destroy(ContextKeyUnitEnum::Locale); + + $this->assertFalse(Context::has(ContextKeyUnitEnum::Locale)); + } + + public function testOverrideWithBackedEnum(): void + { + Context::set(ContextKeyBackedEnum::CurrentUser, 'user-123'); + + $result = Context::override(ContextKeyBackedEnum::CurrentUser, fn ($value) => $value . '-modified'); + + $this->assertSame('user-123-modified', $result); + $this->assertSame('user-123-modified', Context::get(ContextKeyBackedEnum::CurrentUser)); + } + + public function testOverrideWithUnitEnum(): void + { + Context::set(ContextKeyUnitEnum::Locale, 'en'); + + $result = Context::override(ContextKeyUnitEnum::Locale, fn ($value) => $value . '-US'); + + $this->assertSame('en-US', $result); + $this->assertSame('en-US', Context::get(ContextKeyUnitEnum::Locale)); + } + + public function testGetOrSetWithBackedEnum(): void + { + // First call should set and return the value + $result = Context::getOrSet(ContextKeyBackedEnum::RequestId, 'req-001'); + $this->assertSame('req-001', $result); + + // Second call should return existing value, not set new one + $result = Context::getOrSet(ContextKeyBackedEnum::RequestId, 'req-002'); + $this->assertSame('req-001', $result); + } + + public function testGetOrSetWithUnitEnum(): void + { + $result = Context::getOrSet(ContextKeyUnitEnum::Theme, 'dark'); + $this->assertSame('dark', $result); + + $result = Context::getOrSet(ContextKeyUnitEnum::Theme, 'light'); + $this->assertSame('dark', $result); + } + + public function testGetOrSetWithClosure(): void + { + $callCount = 0; + $callback = function () use (&$callCount) { + ++$callCount; + return 'computed-value'; + }; + + $result = Context::getOrSet(ContextKeyBackedEnum::Tenant, $callback); + $this->assertSame('computed-value', $result); + $this->assertSame(1, $callCount); + + // Closure should not be called again + $result = Context::getOrSet(ContextKeyBackedEnum::Tenant, $callback); + $this->assertSame('computed-value', $result); + $this->assertSame(1, $callCount); + } + + public function testSetManyWithEnumKeys(): void + { + Context::setMany([ + ContextKeyBackedEnum::CurrentUser->value => 'user-123', + ContextKeyUnitEnum::Locale->name => 'en-US', + ]); + + $this->assertSame('user-123', Context::get(ContextKeyBackedEnum::CurrentUser)); + $this->assertSame('en-US', Context::get(ContextKeyUnitEnum::Locale)); + } + + public function testBackedEnumAndStringInteroperability(): void + { + // Set with enum + Context::set(ContextKeyBackedEnum::CurrentUser, 'user-123'); + + // Get with string (the enum value) + $this->assertSame('user-123', Context::get('current-user')); + + // Set with string + Context::set('request-id', 'req-456'); + + // Get with enum + $this->assertSame('req-456', Context::get(ContextKeyBackedEnum::RequestId)); + } + + public function testUnitEnumAndStringInteroperability(): void + { + // Set with enum + Context::set(ContextKeyUnitEnum::Locale, 'en-US'); + + // Get with string (the enum name) + $this->assertSame('en-US', Context::get('Locale')); + + // Set with string + Context::set('Theme', 'dark'); + + // Get with enum + $this->assertSame('dark', Context::get(ContextKeyUnitEnum::Theme)); + } + + public function testGetWithDefaultAndBackedEnum(): void + { + $result = Context::get(ContextKeyBackedEnum::CurrentUser, 'default-user'); + + $this->assertSame('default-user', $result); + } + + public function testGetWithDefaultAndUnitEnum(): void + { + $result = Context::get(ContextKeyUnitEnum::Locale, 'en'); + + $this->assertSame('en', $result); + } + + public function testMultipleEnumKeysCanCoexist(): void + { + Context::set(ContextKeyBackedEnum::CurrentUser, 'user-123'); + Context::set(ContextKeyBackedEnum::RequestId, 'req-456'); + Context::set(ContextKeyBackedEnum::Tenant, 'tenant-789'); + Context::set(ContextKeyUnitEnum::Locale, 'en-US'); + Context::set(ContextKeyUnitEnum::Theme, 'dark'); + + $this->assertSame('user-123', Context::get(ContextKeyBackedEnum::CurrentUser)); + $this->assertSame('req-456', Context::get(ContextKeyBackedEnum::RequestId)); + $this->assertSame('tenant-789', Context::get(ContextKeyBackedEnum::Tenant)); + $this->assertSame('en-US', Context::get(ContextKeyUnitEnum::Locale)); + $this->assertSame('dark', Context::get(ContextKeyUnitEnum::Theme)); + } +} From 39ec1a5511eb843b922397428081755efbd639d7 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:55:31 +0000 Subject: [PATCH 08/19] Add BackedEnum and UnitEnum support to Translator replacement values Allow using enums as replacement values in translations. The enum's value (for BackedEnum) or name (for UnitEnum) is used as the replacement string. --- src/translation/src/Translator.php | 8 +++-- tests/Translation/TranslatorTest.php | 44 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/translation/src/Translator.php b/src/translation/src/Translator.php index f1f2e2b09..fefd83e88 100644 --- a/src/translation/src/Translator.php +++ b/src/translation/src/Translator.php @@ -16,6 +16,8 @@ use Hypervel\Translation\Contracts\Translator as TranslatorContract; use InvalidArgumentException; +use function Hypervel\Support\enum_value; + class Translator extends NamespacedItemResolver implements TranslatorContract { use Macroable; @@ -243,8 +245,10 @@ protected function makeReplacements(string $line, array $replace): string continue; } - if (is_object($value) && isset($this->stringableHandlers[get_class($value)])) { - $value = call_user_func($this->stringableHandlers[get_class($value)], $value); + if (is_object($value)) { + $value = isset($this->stringableHandlers[get_class($value)]) + ? call_user_func($this->stringableHandlers[get_class($value)], $value) + : enum_value($value); } $key = (string) $key; diff --git a/tests/Translation/TranslatorTest.php b/tests/Translation/TranslatorTest.php index 3c699d11b..d421166a5 100644 --- a/tests/Translation/TranslatorTest.php +++ b/tests/Translation/TranslatorTest.php @@ -15,6 +15,21 @@ use function Hypervel\Coroutine\run; +enum TranslatorTestStringBackedEnum: string +{ + case February = 'February'; +} + +enum TranslatorTestIntBackedEnum: int +{ + case Thirteen = 13; +} + +enum TranslatorTestUnitEnum +{ + case Hosni; +} + /** * @internal * @coversNothing @@ -292,6 +307,35 @@ public function testGetJsonReplacesWithStringable() ); } + public function testGetJsonReplacesWithEnums() + { + $translator = new Translator($this->getLoader(), 'en'); + $translator->getLoader() + ->shouldReceive('load') + ->once() + ->with('en', '*', '*') + ->andReturn([ + 'string_backed_enum' => 'Laravel 12 was released in :month 2025', + 'int_backed_enum' => 'Stay tuned for Laravel v:version', + 'unit_enum' => ':person gets excited about every new Laravel release', + ]); + + $this->assertSame( + 'Laravel 12 was released in February 2025', + $translator->get('string_backed_enum', ['month' => TranslatorTestStringBackedEnum::February]) + ); + + $this->assertSame( + 'Stay tuned for Laravel v13', + $translator->get('int_backed_enum', ['version' => TranslatorTestIntBackedEnum::Thirteen]) + ); + + $this->assertSame( + 'Hosni gets excited about every new Laravel release', + $translator->get('unit_enum', ['person' => TranslatorTestUnitEnum::Hosni]) + ); + } + public function testTagReplacements() { $translator = new Translator($this->getLoader(), 'en'); From 9f78bba6b68195681449df4b677ff224d0b035f4 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 04:40:34 +0000 Subject: [PATCH 09/19] Add BackedEnum and UnitEnum support to Redis, Filesystem, Eloquent, and Queue - Redis::connection() now accepts UnitEnum for connection names - FilesystemManager::disk()/drive() now accepts UnitEnum for disk names - Model::setConnection() now accepts UnitEnum for connection names - Factory::connection() now accepts UnitEnum for connection names - Pivot::setConnection() now accepts UnitEnum for connection names - MorphPivot::setConnection() now accepts UnitEnum for connection names - Queueable::onConnection/onQueue() changed from BackedEnum to UnitEnum for consistency --- src/bus/src/Queueable.php | 26 ++++--- .../Database/Eloquent/Factories/Factory.php | 15 ++-- src/core/src/Database/Eloquent/Model.php | 22 ++++++ .../Eloquent/Relations/MorphPivot.php | 17 ++++ .../src/Database/Eloquent/Relations/Pivot.php | 17 ++++ src/filesystem/src/FilesystemManager.php | 9 ++- src/redis/src/Redis.php | 7 +- tests/Bus/QueueableTest.php | 34 +++++++- .../Eloquent/Factories/FactoryTest.php | 53 +++++++++++++ .../Core/Database/Eloquent/ModelEnumTest.php | 78 +++++++++++++++++++ .../Eloquent/Relations/MorphPivotEnumTest.php | 73 +++++++++++++++++ .../Eloquent/Relations/PivotEnumTest.php | 73 +++++++++++++++++ tests/Filesystem/FilesystemManagerTest.php | 66 ++++++++++++++++ tests/Redis/RedisTest.php | 62 +++++++++++++++ 14 files changed, 529 insertions(+), 23 deletions(-) create mode 100644 tests/Core/Database/Eloquent/ModelEnumTest.php create mode 100644 tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php create mode 100644 tests/Core/Database/Eloquent/Relations/PivotEnumTest.php diff --git a/src/bus/src/Queueable.php b/src/bus/src/Queueable.php index 8b2693cd4..56560506b 100644 --- a/src/bus/src/Queueable.php +++ b/src/bus/src/Queueable.php @@ -4,7 +4,6 @@ namespace Hypervel\Bus; -use BackedEnum; use Closure; use DateInterval; use DateTimeInterface; @@ -14,6 +13,7 @@ use PHPUnit\Framework\Assert as PHPUnit; use RuntimeException; use Throwable; +use UnitEnum; use function Hypervel\Support\enum_value; @@ -67,9 +67,11 @@ trait Queueable /** * Set the desired connection for the job. */ - public function onConnection(BackedEnum|string|null $connection): static + public function onConnection(UnitEnum|string|null $connection): static { - $this->connection = enum_value($connection); + $value = enum_value($connection); + + $this->connection = is_null($value) ? null : (string) $value; return $this; } @@ -77,9 +79,11 @@ public function onConnection(BackedEnum|string|null $connection): static /** * Set the desired queue for the job. */ - public function onQueue(BackedEnum|string|null $queue): static + public function onQueue(UnitEnum|string|null $queue): static { - $this->queue = enum_value($queue); + $value = enum_value($queue); + + $this->queue = is_null($value) ? null : (string) $value; return $this; } @@ -87,9 +91,11 @@ public function onQueue(BackedEnum|string|null $queue): static /** * Set the desired connection for the chain. */ - public function allOnConnection(BackedEnum|string|null $connection): static + public function allOnConnection(UnitEnum|string|null $connection): static { - $resolvedConnection = enum_value($connection); + $value = enum_value($connection); + + $resolvedConnection = is_null($value) ? null : (string) $value; $this->chainConnection = $resolvedConnection; $this->connection = $resolvedConnection; @@ -100,9 +106,11 @@ public function allOnConnection(BackedEnum|string|null $connection): static /** * Set the desired queue for the chain. */ - public function allOnQueue(BackedEnum|string|null $queue): static + public function allOnQueue(UnitEnum|string|null $queue): static { - $resolvedQueue = enum_value($queue); + $value = enum_value($queue); + + $resolvedQueue = is_null($value) ? null : (string) $value; $this->chainQueue = $resolvedQueue; $this->queue = $resolvedQueue; diff --git a/src/core/src/Database/Eloquent/Factories/Factory.php b/src/core/src/Database/Eloquent/Factories/Factory.php index 959fe449f..ae00b9731 100644 --- a/src/core/src/Database/Eloquent/Factories/Factory.php +++ b/src/core/src/Database/Eloquent/Factories/Factory.php @@ -21,6 +21,9 @@ use Hypervel\Support\Traits\Conditionable; use Hypervel\Support\Traits\Macroable; use Throwable; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @template TModel of Model @@ -81,7 +84,7 @@ abstract class Factory /** * The name of the database connection that will be used to create the models. */ - protected ?string $connection; + protected UnitEnum|string|null $connection; /** * The current Faker instance. @@ -117,7 +120,7 @@ public function __construct( ?Collection $for = null, ?Collection $afterMaking = null, ?Collection $afterCreating = null, - ?string $connection = null, + UnitEnum|string|null $connection = null, ?Collection $recycle = null, bool $expandRelationships = true ) { @@ -662,15 +665,17 @@ public function withoutParents(): self /** * Get the name of the database connection that is used to generate models. */ - public function getConnectionName(): string + public function getConnectionName(): ?string { - return $this->connection; + $value = enum_value($this->connection); + + return is_null($value) ? null : (string) $value; } /** * Specify the database connection that should be used to generate models. */ - public function connection(string $connection): self + public function connection(UnitEnum|string $connection): self { return $this->newInstance(['connection' => $connection]); } diff --git a/src/core/src/Database/Eloquent/Model.php b/src/core/src/Database/Eloquent/Model.php index fb6c06e37..614d9acbe 100644 --- a/src/core/src/Database/Eloquent/Model.php +++ b/src/core/src/Database/Eloquent/Model.php @@ -23,6 +23,9 @@ use Hypervel\Router\Contracts\UrlRoutable; use Psr\EventDispatcher\EventDispatcherInterface; use ReflectionClass; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @method static \Hypervel\Database\Eloquent\Collection all(array|string $columns = ['*']) @@ -91,8 +94,27 @@ abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChann */ protected static array $resolvedBuilderClasses = []; + /** + * The connection name for the model. + * + * Overrides Hyperf's default of 'default' to null. + */ protected ?string $connection = null; + /** + * Set the connection associated with the model. + * + * @param null|string|UnitEnum $name + */ + public function setConnection($name): static + { + $value = enum_value($name); + + $this->connection = is_null($value) ? null : (string) $value; + + return $this; + } + public function resolveRouteBinding($value) { return $this->where($this->getRouteKeyName(), $value)->firstOrFail(); diff --git a/src/core/src/Database/Eloquent/Relations/MorphPivot.php b/src/core/src/Database/Eloquent/Relations/MorphPivot.php index cdfa47a3c..735d0c68d 100644 --- a/src/core/src/Database/Eloquent/Relations/MorphPivot.php +++ b/src/core/src/Database/Eloquent/Relations/MorphPivot.php @@ -11,6 +11,9 @@ use Hypervel\Database\Eloquent\Concerns\HasObservers; use Hypervel\Database\Eloquent\Concerns\HasTimestamps; use Psr\EventDispatcher\StoppableEventInterface; +use UnitEnum; + +use function Hypervel\Support\enum_value; class MorphPivot extends BaseMorphPivot { @@ -20,6 +23,20 @@ class MorphPivot extends BaseMorphPivot use HasObservers; use HasTimestamps; + /** + * Set the connection associated with the model. + * + * @param null|string|UnitEnum $name + */ + public function setConnection($name): static + { + $value = enum_value($name); + + $this->connection = is_null($value) ? null : (string) $value; + + return $this; + } + /** * Delete the pivot model record from the database. * diff --git a/src/core/src/Database/Eloquent/Relations/Pivot.php b/src/core/src/Database/Eloquent/Relations/Pivot.php index 085b717ef..8cb0bc541 100644 --- a/src/core/src/Database/Eloquent/Relations/Pivot.php +++ b/src/core/src/Database/Eloquent/Relations/Pivot.php @@ -11,6 +11,9 @@ use Hypervel\Database\Eloquent\Concerns\HasObservers; use Hypervel\Database\Eloquent\Concerns\HasTimestamps; use Psr\EventDispatcher\StoppableEventInterface; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Pivot extends BasePivot { @@ -20,6 +23,20 @@ class Pivot extends BasePivot use HasObservers; use HasTimestamps; + /** + * Set the connection associated with the model. + * + * @param null|string|UnitEnum $name + */ + public function setConnection($name): static + { + $value = enum_value($name); + + $this->connection = is_null($value) ? null : (string) $value; + + return $this; + } + /** * Delete the pivot model record from the database. * diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php index 011773682..9109b214e 100644 --- a/src/filesystem/src/FilesystemManager.php +++ b/src/filesystem/src/FilesystemManager.php @@ -31,6 +31,9 @@ use League\Flysystem\UnixVisibility\PortableVisibilityConverter; use League\Flysystem\Visibility; use Psr\Container\ContainerInterface; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @mixin \Hypervel\Filesystem\Filesystem @@ -71,7 +74,7 @@ public function __construct( /** * Get a filesystem instance. */ - public function drive(?string $name = null): Filesystem + public function drive(UnitEnum|string|null $name = null): Filesystem { return $this->disk($name); } @@ -79,9 +82,9 @@ public function drive(?string $name = null): Filesystem /** * Get a filesystem instance. */ - public function disk(?string $name = null): FileSystem + public function disk(UnitEnum|string|null $name = null): FileSystem { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->disks[$name] = $this->get($name); } diff --git a/src/redis/src/Redis.php b/src/redis/src/Redis.php index 4beeb1ef4..be6ccc695 100644 --- a/src/redis/src/Redis.php +++ b/src/redis/src/Redis.php @@ -10,6 +10,9 @@ use Hypervel\Context\ApplicationContext; use Hypervel\Context\Context; use Throwable; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @mixin \Hypervel\Redis\RedisConnection @@ -133,10 +136,10 @@ protected function getContextKey(): string /** * Get a Redis connection by name. */ - public function connection(string $name = 'default'): RedisProxy + public function connection(UnitEnum|string $name = 'default'): RedisProxy { return ApplicationContext::getContainer() ->get(RedisFactory::class) - ->get($name); + ->get(enum_value($name)); } } diff --git a/tests/Bus/QueueableTest.php b/tests/Bus/QueueableTest.php index 2481a64f5..ff3cd12a4 100644 --- a/tests/Bus/QueueableTest.php +++ b/tests/Bus/QueueableTest.php @@ -37,8 +37,9 @@ public static function connectionDataProvider(): array { return [ 'uses string' => ['redis', 'redis'], - 'uses BackedEnum #1' => [ConnectionEnum::SQS, 'sqs'], - 'uses BackedEnum #2' => [ConnectionEnum::REDIS, 'redis'], + 'uses string-backed enum' => [ConnectionEnum::SQS, 'sqs'], + 'uses int-backed enum' => [IntConnectionEnum::Redis, '2'], + 'uses unit enum' => [UnitConnectionEnum::Sync, 'Sync'], 'uses null' => [null, null], ]; } @@ -66,8 +67,9 @@ public static function queuesDataProvider(): array { return [ 'uses string' => ['high', 'high'], - 'uses BackedEnum #1' => [QueueEnum::DEFAULT, 'default'], - 'uses BackedEnum #2' => [QueueEnum::HIGH, 'high'], + 'uses string-backed enum' => [QueueEnum::HIGH, 'high'], + 'uses int-backed enum' => [IntQueueEnum::High, '2'], + 'uses unit enum' => [UnitQueueEnum::Low, 'Low'], 'uses null' => [null, null], ]; } @@ -84,8 +86,32 @@ enum ConnectionEnum: string case REDIS = 'redis'; } +enum IntConnectionEnum: int +{ + case Sqs = 1; + case Redis = 2; +} + +enum UnitConnectionEnum +{ + case Sync; + case Database; +} + enum QueueEnum: string { case HIGH = 'high'; case DEFAULT = 'default'; } + +enum IntQueueEnum: int +{ + case Default = 1; + case High = 2; +} + +enum UnitQueueEnum +{ + case Default; + case Low; +} diff --git a/tests/Core/Database/Eloquent/Factories/FactoryTest.php b/tests/Core/Database/Eloquent/Factories/FactoryTest.php index 3fea068dd..333f80a5f 100644 --- a/tests/Core/Database/Eloquent/Factories/FactoryTest.php +++ b/tests/Core/Database/Eloquent/Factories/FactoryTest.php @@ -21,6 +21,24 @@ use Mockery as m; use ReflectionClass; +enum FactoryTestStringBackedConnection: string +{ + case Default = 'default'; + case Testing = 'testing'; +} + +enum FactoryTestIntBackedConnection: int +{ + case Default = 1; + case Testing = 2; +} + +enum FactoryTestUnitConnection +{ + case default; + case testing; +} + /** * @internal * @coversNothing @@ -848,6 +866,41 @@ public function testFlushStateResetsAllResolvers() // After flush, namespace should be reset $this->assertSame('Database\Factories\\', Factory::$namespace); } + + public function testConnectionAcceptsStringBackedEnum() + { + $factory = FactoryTestUserFactory::new()->connection(FactoryTestStringBackedConnection::Testing); + + $this->assertSame('testing', $factory->getConnectionName()); + } + + public function testConnectionAcceptsIntBackedEnum() + { + $factory = FactoryTestUserFactory::new()->connection(FactoryTestIntBackedConnection::Testing); + + $this->assertSame('2', $factory->getConnectionName()); + } + + public function testConnectionAcceptsUnitEnum() + { + $factory = FactoryTestUserFactory::new()->connection(FactoryTestUnitConnection::testing); + + $this->assertSame('testing', $factory->getConnectionName()); + } + + public function testConnectionAcceptsString() + { + $factory = FactoryTestUserFactory::new()->connection('mysql'); + + $this->assertSame('mysql', $factory->getConnectionName()); + } + + public function testGetConnectionNameReturnsNullByDefault() + { + $factory = FactoryTestUserFactory::new(); + + $this->assertNull($factory->getConnectionName()); + } } class FactoryTestUserFactory extends Factory diff --git a/tests/Core/Database/Eloquent/ModelEnumTest.php b/tests/Core/Database/Eloquent/ModelEnumTest.php new file mode 100644 index 000000000..db35750f4 --- /dev/null +++ b/tests/Core/Database/Eloquent/ModelEnumTest.php @@ -0,0 +1,78 @@ +setConnection(ModelTestStringBackedConnection::Testing); + + $this->assertSame('testing', $model->getConnectionName()); + } + + public function testSetConnectionAcceptsIntBackedEnum(): void + { + $model = new ModelEnumTestModel(); + $model->setConnection(ModelTestIntBackedConnection::Testing); + + $this->assertSame('2', $model->getConnectionName()); + } + + public function testSetConnectionAcceptsUnitEnum(): void + { + $model = new ModelEnumTestModel(); + $model->setConnection(ModelTestUnitConnection::testing); + + $this->assertSame('testing', $model->getConnectionName()); + } + + public function testSetConnectionAcceptsString(): void + { + $model = new ModelEnumTestModel(); + $model->setConnection('mysql'); + + $this->assertSame('mysql', $model->getConnectionName()); + } + + public function testSetConnectionAcceptsNull(): void + { + $model = new ModelEnumTestModel(); + $model->setConnection(null); + + $this->assertNull($model->getConnectionName()); + } +} + +class ModelEnumTestModel extends Model +{ + protected ?string $table = 'test_models'; +} diff --git a/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php new file mode 100644 index 000000000..95732b11b --- /dev/null +++ b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php @@ -0,0 +1,73 @@ +setConnection(MorphPivotTestStringBackedConnection::Testing); + + $this->assertSame('testing', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsIntBackedEnum(): void + { + $pivot = new MorphPivot(); + $pivot->setConnection(MorphPivotTestIntBackedConnection::Testing); + + $this->assertSame('2', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsUnitEnum(): void + { + $pivot = new MorphPivot(); + $pivot->setConnection(MorphPivotTestUnitConnection::testing); + + $this->assertSame('testing', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsString(): void + { + $pivot = new MorphPivot(); + $pivot->setConnection('mysql'); + + $this->assertSame('mysql', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsNull(): void + { + $pivot = new MorphPivot(); + $pivot->setConnection(null); + + $this->assertNull($pivot->getConnectionName()); + } +} diff --git a/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php new file mode 100644 index 000000000..f7d61525d --- /dev/null +++ b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php @@ -0,0 +1,73 @@ +setConnection(PivotTestStringBackedConnection::Testing); + + $this->assertSame('testing', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsIntBackedEnum(): void + { + $pivot = new Pivot(); + $pivot->setConnection(PivotTestIntBackedConnection::Testing); + + $this->assertSame('2', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsUnitEnum(): void + { + $pivot = new Pivot(); + $pivot->setConnection(PivotTestUnitConnection::testing); + + $this->assertSame('testing', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsString(): void + { + $pivot = new Pivot(); + $pivot->setConnection('mysql'); + + $this->assertSame('mysql', $pivot->getConnectionName()); + } + + public function testSetConnectionAcceptsNull(): void + { + $pivot = new Pivot(); + $pivot->setConnection(null); + + $this->assertNull($pivot->getConnectionName()); + } +} diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index b5a686b5b..2b51b77e3 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -19,6 +19,21 @@ use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; +enum FilesystemTestStringBackedDisk: string +{ + case Local = 'local'; +} + +enum FilesystemTestIntBackedDisk: int +{ + case Local = 1; +} + +enum FilesystemTestUnitDisk +{ + case local; +} + /** * @internal * @coversNothing @@ -192,6 +207,57 @@ public function testPoolableDriver() $this->assertInstanceOf(FilesystemPoolProxy::class, $filesystem->disk('local')); } + public function testDiskAcceptsStringBackedEnum(): void + { + $container = $this->getContainer([ + 'disks' => [ + 'local' => [ + 'driver' => 'local', + 'root' => __DIR__ . '/tmp', + ], + ], + ]); + $filesystem = new FilesystemManager($container); + + $disk = $filesystem->disk(FilesystemTestStringBackedDisk::Local); + + $this->assertInstanceOf(Filesystem::class, $disk); + } + + public function testDiskAcceptsUnitEnum(): void + { + $container = $this->getContainer([ + 'disks' => [ + 'local' => [ + 'driver' => 'local', + 'root' => __DIR__ . '/tmp', + ], + ], + ]); + $filesystem = new FilesystemManager($container); + + $disk = $filesystem->disk(FilesystemTestUnitDisk::local); + + $this->assertInstanceOf(Filesystem::class, $disk); + } + + public function testDriveAcceptsStringBackedEnum(): void + { + $container = $this->getContainer([ + 'disks' => [ + 'local' => [ + 'driver' => 'local', + 'root' => __DIR__ . '/tmp', + ], + ], + ]); + $filesystem = new FilesystemManager($container); + + $disk = $filesystem->drive(FilesystemTestStringBackedDisk::Local); + + $this->assertInstanceOf(Filesystem::class, $disk); + } + protected function getContainer(array $config = []): ContainerInterface { $config = new Config(['filesystems' => $config]); diff --git a/tests/Redis/RedisTest.php b/tests/Redis/RedisTest.php index 46e88ee74..e8d2995fe 100644 --- a/tests/Redis/RedisTest.php +++ b/tests/Redis/RedisTest.php @@ -13,12 +13,26 @@ use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; use Hypervel\Redis\Redis; use Hypervel\Redis\RedisConnection; +use Hypervel\Redis\RedisFactory; +use Hypervel\Redis\RedisProxy; use Hypervel\Tests\TestCase; use Mockery; use Psr\EventDispatcher\EventDispatcherInterface; use Redis as PhpRedis; use Throwable; +enum RedisTestStringBackedConnection: string +{ + case Default = 'default'; + case Cache = 'cache'; +} + +enum RedisTestUnitConnection +{ + case default; + case cache; +} + /** * @internal * @coversNothing @@ -217,6 +231,54 @@ public function testRegularCommandDoesNotStoreConnectionInContext(): void $this->assertNull(Context::get('redis.connection.default')); } + public function testConnectionAcceptsStringBackedEnum(): void + { + $mockRedisProxy = Mockery::mock(RedisProxy::class); + + $mockRedisFactory = Mockery::mock(RedisFactory::class); + $mockRedisFactory->shouldReceive('get') + ->with('default') + ->once() + ->andReturn($mockRedisProxy); + + $mockContainer = Mockery::mock(\Hypervel\Container\Contracts\Container::class); + $mockContainer->shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($mockRedisFactory); + + \Hypervel\Context\ApplicationContext::setContainer($mockContainer); + + $redis = new Redis(Mockery::mock(PoolFactory::class)); + + $result = $redis->connection(RedisTestStringBackedConnection::Default); + + $this->assertSame($mockRedisProxy, $result); + } + + public function testConnectionAcceptsUnitEnum(): void + { + $mockRedisProxy = Mockery::mock(RedisProxy::class); + + $mockRedisFactory = Mockery::mock(RedisFactory::class); + $mockRedisFactory->shouldReceive('get') + ->with('default') + ->once() + ->andReturn($mockRedisProxy); + + $mockContainer = Mockery::mock(\Hypervel\Container\Contracts\Container::class); + $mockContainer->shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($mockRedisFactory); + + \Hypervel\Context\ApplicationContext::setContainer($mockContainer); + + $redis = new Redis(Mockery::mock(PoolFactory::class)); + + $result = $redis->connection(RedisTestUnitConnection::default); + + $this->assertSame($mockRedisProxy, $result); + } + /** * Create a mock Redis connection with configurable behavior. */ From 74ad61179aa7c6dee02201d600de6dd1f89e2a1e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 04:48:20 +0000 Subject: [PATCH 10/19] Remove redundant BackedEnum from union types with UnitEnum BackedEnum extends UnitEnum, so including both in union types is redundant. Simplified all BackedEnum|UnitEnum unions to just UnitEnum. --- src/auth/src/Access/Gate.php | 19 +++++----- src/auth/src/Contracts/Gate.php | 19 +++++----- src/auth/src/Middleware/Authorize.php | 3 +- src/cache/src/RateLimiter.php | 7 ++-- src/cache/src/RedisTaggedCache.php | 11 +++--- src/cache/src/Repository.php | 39 ++++++++++----------- src/cache/src/TaggedCache.php | 5 ++- src/core/src/Context/Context.php | 13 ++++--- src/foundation/src/Http/Traits/HasCasts.php | 3 +- src/session/src/Contracts/Session.php | 15 ++++---- src/session/src/Store.php | 35 +++++++++--------- 11 files changed, 79 insertions(+), 90 deletions(-) diff --git a/src/auth/src/Access/Gate.php b/src/auth/src/Access/Gate.php index 0feb0bb06..e8741f0c9 100644 --- a/src/auth/src/Access/Gate.php +++ b/src/auth/src/Access/Gate.php @@ -4,7 +4,6 @@ namespace Hypervel\Auth\Access; -use BackedEnum; use Closure; use Exception; use Hyperf\Collection\Arr; @@ -62,7 +61,7 @@ public function __construct( /** * Determine if a given ability has been defined. */ - public function has(array|BackedEnum|UnitEnum|string $ability): bool + public function has(array|UnitEnum|string $ability): bool { $abilities = is_array($ability) ? $ability : func_get_args(); @@ -124,7 +123,7 @@ protected function authorizeOnDemand(bool|Closure|Response $condition, ?string $ * * @throws InvalidArgumentException */ - public function define(BackedEnum|UnitEnum|string $ability, array|callable|string $callback): static + public function define(UnitEnum|string $ability, array|callable|string $callback): static { $ability = enum_value($ability); @@ -233,7 +232,7 @@ public function after(callable $callback): static /** * Determine if the given ability should be granted for the current user. */ - public function allows(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool + public function allows(UnitEnum|string $ability, mixed $arguments = []): bool { return $this->check($ability, $arguments); } @@ -241,7 +240,7 @@ public function allows(BackedEnum|UnitEnum|string $ability, mixed $arguments = [ /** * Determine if the given ability should be denied for the current user. */ - public function denies(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool + public function denies(UnitEnum|string $ability, mixed $arguments = []): bool { return ! $this->allows($ability, $arguments); } @@ -249,7 +248,7 @@ public function denies(BackedEnum|UnitEnum|string $ability, mixed $arguments = [ /** * Determine if all of the given abilities should be granted for the current user. */ - public function check(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool + public function check(iterable|UnitEnum|string $abilities, mixed $arguments = []): bool { return collect($abilities)->every( fn ($ability) => $this->inspect($ability, $arguments)->allowed() @@ -259,7 +258,7 @@ public function check(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arg /** * Determine if any one of the given abilities should be granted for the current user. */ - public function any(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool + public function any(iterable|UnitEnum|string $abilities, mixed $arguments = []): bool { return collect($abilities)->contains(fn ($ability) => $this->check($ability, $arguments)); } @@ -267,7 +266,7 @@ public function any(iterable|BackedEnum|UnitEnum|string $abilities, mixed $argum /** * Determine if all of the given abilities should be denied for the current user. */ - public function none(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool + public function none(iterable|UnitEnum|string $abilities, mixed $arguments = []): bool { return ! $this->any($abilities, $arguments); } @@ -277,7 +276,7 @@ public function none(iterable|BackedEnum|UnitEnum|string $abilities, mixed $argu * * @throws AuthorizationException */ - public function authorize(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response + public function authorize(UnitEnum|string $ability, mixed $arguments = []): Response { return $this->inspect($ability, $arguments)->authorize(); } @@ -285,7 +284,7 @@ public function authorize(BackedEnum|UnitEnum|string $ability, mixed $arguments /** * Inspect the user for the given ability. */ - public function inspect(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response + public function inspect(UnitEnum|string $ability, mixed $arguments = []): Response { try { $result = $this->raw(enum_value($ability), $arguments); diff --git a/src/auth/src/Contracts/Gate.php b/src/auth/src/Contracts/Gate.php index d3c674bbb..a3ea09739 100644 --- a/src/auth/src/Contracts/Gate.php +++ b/src/auth/src/Contracts/Gate.php @@ -4,7 +4,6 @@ namespace Hypervel\Auth\Contracts; -use BackedEnum; use Hypervel\Auth\Access\AuthorizationException; use Hypervel\Auth\Access\Response; use InvalidArgumentException; @@ -15,12 +14,12 @@ interface Gate /** * Determine if a given ability has been defined. */ - public function has(array|BackedEnum|UnitEnum|string $ability): bool; + public function has(array|UnitEnum|string $ability): bool; /** * Define a new ability. */ - public function define(BackedEnum|UnitEnum|string $ability, callable|string $callback): static; + public function define(UnitEnum|string $ability, callable|string $callback): static; /** * Define abilities for a resource. @@ -45,39 +44,39 @@ public function after(callable $callback): static; /** * Determine if the given ability should be granted for the current user. */ - public function allows(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool; + public function allows(UnitEnum|string $ability, mixed $arguments = []): bool; /** * Determine if the given ability should be denied for the current user. */ - public function denies(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): bool; + public function denies(UnitEnum|string $ability, mixed $arguments = []): bool; /** * Determine if all of the given abilities should be granted for the current user. */ - public function check(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool; + public function check(iterable|UnitEnum|string $abilities, mixed $arguments = []): bool; /** * Determine if any one of the given abilities should be granted for the current user. */ - public function any(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool; + public function any(iterable|UnitEnum|string $abilities, mixed $arguments = []): bool; /** * Determine if all of the given abilities should be denied for the current user. */ - public function none(iterable|BackedEnum|UnitEnum|string $abilities, mixed $arguments = []): bool; + public function none(iterable|UnitEnum|string $abilities, mixed $arguments = []): bool; /** * Determine if the given ability should be granted for the current user. * * @throws AuthorizationException */ - public function authorize(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response; + public function authorize(UnitEnum|string $ability, mixed $arguments = []): Response; /** * Inspect the user for the given ability. */ - public function inspect(BackedEnum|UnitEnum|string $ability, mixed $arguments = []): Response; + public function inspect(UnitEnum|string $ability, mixed $arguments = []): Response; /** * Get the raw result from the authorization callback. diff --git a/src/auth/src/Middleware/Authorize.php b/src/auth/src/Middleware/Authorize.php index fbececac0..fa07c1aa5 100644 --- a/src/auth/src/Middleware/Authorize.php +++ b/src/auth/src/Middleware/Authorize.php @@ -4,7 +4,6 @@ namespace Hypervel\Auth\Middleware; -use BackedEnum; use Hyperf\Collection\Collection; use Hyperf\Database\Model\Model; use Hyperf\HttpServer\Router\Dispatched; @@ -32,7 +31,7 @@ public function __construct(protected Gate $gate) /** * Specify the ability and models for the middleware. */ - public static function using(BackedEnum|UnitEnum|string $ability, string ...$models): string + public static function using(UnitEnum|string $ability, string ...$models): string { return static::class . ':' . implode(',', [enum_value($ability), ...$models]); } diff --git a/src/cache/src/RateLimiter.php b/src/cache/src/RateLimiter.php index 6400a798d..2aa69890d 100644 --- a/src/cache/src/RateLimiter.php +++ b/src/cache/src/RateLimiter.php @@ -4,7 +4,6 @@ namespace Hypervel\Cache; -use BackedEnum; use Closure; use Hyperf\Support\Traits\InteractsWithTime; use Hypervel\Cache\Contracts\Factory as Cache; @@ -37,7 +36,7 @@ public function __construct(Cache $cache) /** * Register a named limiter configuration. */ - public function for(BackedEnum|UnitEnum|string $name, Closure $callback): static + public function for(UnitEnum|string $name, Closure $callback): static { $this->limiters[$this->resolveLimiterName($name)] = $callback; @@ -47,7 +46,7 @@ public function for(BackedEnum|UnitEnum|string $name, Closure $callback): static /** * Get the given named rate limiter. */ - public function limiter(BackedEnum|UnitEnum|string $name): ?Closure + public function limiter(UnitEnum|string $name): ?Closure { return $this->limiters[$this->resolveLimiterName($name)] ?? null; } @@ -55,7 +54,7 @@ public function limiter(BackedEnum|UnitEnum|string $name): ?Closure /** * Resolve the rate limiter name. */ - private function resolveLimiterName(BackedEnum|UnitEnum|string $name): string + private function resolveLimiterName(UnitEnum|string $name): string { return (string) enum_value($name); } diff --git a/src/cache/src/RedisTaggedCache.php b/src/cache/src/RedisTaggedCache.php index 398c7bbca..89cdcc311 100644 --- a/src/cache/src/RedisTaggedCache.php +++ b/src/cache/src/RedisTaggedCache.php @@ -4,7 +4,6 @@ namespace Hypervel\Cache; -use BackedEnum; use DateInterval; use DateTimeInterface; use Hypervel\Cache\Contracts\Store; @@ -31,7 +30,7 @@ class RedisTaggedCache extends TaggedCache /** * Store an item in the cache if the key does not exist. */ - public function add(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { $key = enum_value($key); @@ -46,7 +45,7 @@ public function add(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval| /** * Store an item in the cache. */ - public function put(array|BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { if (is_array($key)) { return $this->putMany($key, $value); @@ -69,7 +68,7 @@ public function put(array|BackedEnum|UnitEnum|string $key, mixed $value, DateInt /** * Increment the value of an item in the cache. */ - public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int + public function increment(UnitEnum|string $key, int $value = 1): bool|int { $key = enum_value($key); @@ -81,7 +80,7 @@ public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool /** * Decrement the value of an item in the cache. */ - public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int + public function decrement(UnitEnum|string $key, int $value = 1): bool|int { $key = enum_value($key); @@ -93,7 +92,7 @@ public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool /** * Store an item in the cache indefinitely. */ - public function forever(BackedEnum|UnitEnum|string $key, mixed $value): bool + public function forever(UnitEnum|string $key, mixed $value): bool { $key = enum_value($key); diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 0d41a1088..7ea3fb250 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -5,7 +5,6 @@ namespace Hypervel\Cache; use ArrayAccess; -use BackedEnum; use BadMethodCallException; use Carbon\Carbon; use Closure; @@ -96,7 +95,7 @@ public function __clone() /** * Determine if an item exists in the cache. */ - public function has(array|BackedEnum|UnitEnum|string $key): bool + public function has(array|UnitEnum|string $key): bool { return ! is_null($this->get($key)); } @@ -104,7 +103,7 @@ public function has(array|BackedEnum|UnitEnum|string $key): bool /** * Determine if an item doesn't exist in the cache. */ - public function missing(BackedEnum|UnitEnum|string $key): bool + public function missing(UnitEnum|string $key): bool { return ! $this->has($key); } @@ -118,7 +117,7 @@ public function missing(BackedEnum|UnitEnum|string $key): bool * * @return (TCacheValue is null ? mixed : TCacheValue) */ - public function get(array|BackedEnum|UnitEnum|string $key, mixed $default = null): mixed + public function get(array|UnitEnum|string $key, mixed $default = null): mixed { if (is_array($key)) { return $this->many($key); @@ -183,7 +182,7 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable * * @return (TCacheValue is null ? mixed : TCacheValue) */ - public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed + public function pull(UnitEnum|string $key, mixed $default = null): mixed { return tap($this->get($key, $default), function () use ($key) { $this->forget($key); @@ -193,7 +192,7 @@ public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mi /** * Store an item in the cache. */ - public function put(array|BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { if (is_array($key)) { return $this->putMany($key, $value); @@ -226,7 +225,7 @@ public function put(array|BackedEnum|UnitEnum|string $key, mixed $value, DateInt /** * Store an item in the cache. */ - public function set(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function set(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { return $this->put($key, $value, $ttl); } @@ -269,7 +268,7 @@ public function setMultiple(iterable $values, DateInterval|DateTimeInterface|int /** * Store an item in the cache if the key does not exist. */ - public function add(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { $key = enum_value($key); @@ -307,7 +306,7 @@ public function add(BackedEnum|UnitEnum|string $key, mixed $value, DateInterval| /** * Increment the value of an item in the cache. */ - public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int + public function increment(UnitEnum|string $key, int $value = 1): bool|int { return $this->store->increment(enum_value($key), $value); } @@ -315,7 +314,7 @@ public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool /** * Decrement the value of an item in the cache. */ - public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int + public function decrement(UnitEnum|string $key, int $value = 1): bool|int { return $this->store->decrement(enum_value($key), $value); } @@ -323,7 +322,7 @@ public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool /** * Store an item in the cache indefinitely. */ - public function forever(BackedEnum|UnitEnum|string $key, mixed $value): bool + public function forever(UnitEnum|string $key, mixed $value): bool { $key = enum_value($key); @@ -349,7 +348,7 @@ public function forever(BackedEnum|UnitEnum|string $key, mixed $value): bool * * @return TCacheValue */ - public function remember(BackedEnum|UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl, Closure $callback): mixed + public function remember(UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl, Closure $callback): mixed { $value = $this->get($key); @@ -374,7 +373,7 @@ public function remember(BackedEnum|UnitEnum|string $key, DateInterval|DateTimeI * * @return TCacheValue */ - public function sear(BackedEnum|UnitEnum|string $key, Closure $callback): mixed + public function sear(UnitEnum|string $key, Closure $callback): mixed { return $this->rememberForever($key, $callback); } @@ -388,7 +387,7 @@ public function sear(BackedEnum|UnitEnum|string $key, Closure $callback): mixed * * @return TCacheValue */ - public function rememberForever(BackedEnum|UnitEnum|string $key, Closure $callback): mixed + public function rememberForever(UnitEnum|string $key, Closure $callback): mixed { $value = $this->get($key); @@ -407,7 +406,7 @@ public function rememberForever(BackedEnum|UnitEnum|string $key, Closure $callba /** * Remove an item from the cache. */ - public function forget(BackedEnum|UnitEnum|string $key): bool + public function forget(UnitEnum|string $key): bool { $key = enum_value($key); @@ -422,7 +421,7 @@ public function forget(BackedEnum|UnitEnum|string $key): bool }); } - public function delete(BackedEnum|UnitEnum|string $key): bool + public function delete(UnitEnum|string $key): bool { return $this->forget($key); } @@ -540,7 +539,7 @@ public function getName(): ?string /** * Determine if a cached value exists. * - * @param BackedEnum|string|UnitEnum $key + * @param UnitEnum|string $key */ public function offsetExists($key): bool { @@ -550,7 +549,7 @@ public function offsetExists($key): bool /** * Retrieve an item from the cache by key. * - * @param BackedEnum|string|UnitEnum $key + * @param UnitEnum|string $key */ public function offsetGet($key): mixed { @@ -560,7 +559,7 @@ public function offsetGet($key): mixed /** * Store an item in the cache for the default time. * - * @param BackedEnum|string|UnitEnum $key + * @param UnitEnum|string $key * @param mixed $value */ public function offsetSet($key, $value): void @@ -571,7 +570,7 @@ public function offsetSet($key, $value): void /** * Remove an item from the cache. * - * @param BackedEnum|string|UnitEnum $key + * @param UnitEnum|string $key */ public function offsetUnset($key): void { diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php index c42e00a97..a273136ef 100644 --- a/src/cache/src/TaggedCache.php +++ b/src/cache/src/TaggedCache.php @@ -4,7 +4,6 @@ namespace Hypervel\Cache; -use BackedEnum; use DateInterval; use DateTimeInterface; use Hypervel\Cache\Contracts\Store; @@ -50,7 +49,7 @@ public function putMany(array $values, DateInterval|DateTimeInterface|int|null $ /** * Increment the value of an item in the cache. */ - public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int + public function increment(UnitEnum|string $key, int $value = 1): bool|int { return $this->store->increment($this->itemKey(enum_value($key)), $value); } @@ -58,7 +57,7 @@ public function increment(BackedEnum|UnitEnum|string $key, int $value = 1): bool /** * Decrement the value of an item in the cache. */ - public function decrement(BackedEnum|UnitEnum|string $key, int $value = 1): bool|int + public function decrement(UnitEnum|string $key, int $value = 1): bool|int { return $this->store->decrement($this->itemKey(enum_value($key)), $value); } diff --git a/src/core/src/Context/Context.php b/src/core/src/Context/Context.php index e48c3a82b..07d0331cf 100644 --- a/src/core/src/Context/Context.php +++ b/src/core/src/Context/Context.php @@ -4,7 +4,6 @@ namespace Hypervel\Context; -use BackedEnum; use Closure; use Hyperf\Context\Context as HyperfContext; use Hyperf\Engine\Coroutine; @@ -24,7 +23,7 @@ public function __call(string $method, array $arguments): mixed /** * Set a value in the context. */ - public static function set(BackedEnum|UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed + public static function set(UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed { return parent::set(enum_value($id), $value, $coroutineId); } @@ -32,7 +31,7 @@ public static function set(BackedEnum|UnitEnum|string $id, mixed $value, ?int $c /** * Get a value from the context. */ - public static function get(BackedEnum|UnitEnum|string $id, mixed $default = null, ?int $coroutineId = null): mixed + public static function get(UnitEnum|string $id, mixed $default = null, ?int $coroutineId = null): mixed { return parent::get(enum_value($id), $default, $coroutineId); } @@ -40,7 +39,7 @@ public static function get(BackedEnum|UnitEnum|string $id, mixed $default = null /** * Determine if a value exists in the context. */ - public static function has(BackedEnum|UnitEnum|string $id, ?int $coroutineId = null): bool + public static function has(UnitEnum|string $id, ?int $coroutineId = null): bool { return parent::has(enum_value($id), $coroutineId); } @@ -48,7 +47,7 @@ public static function has(BackedEnum|UnitEnum|string $id, ?int $coroutineId = n /** * Remove a value from the context. */ - public static function destroy(BackedEnum|UnitEnum|string $id, ?int $coroutineId = null): void + public static function destroy(UnitEnum|string $id, ?int $coroutineId = null): void { parent::destroy(enum_value($id), $coroutineId); } @@ -56,7 +55,7 @@ public static function destroy(BackedEnum|UnitEnum|string $id, ?int $coroutineId /** * Retrieve the value and override it by closure. */ - public static function override(BackedEnum|UnitEnum|string $id, Closure $closure, ?int $coroutineId = null): mixed + public static function override(UnitEnum|string $id, Closure $closure, ?int $coroutineId = null): mixed { return parent::override(enum_value($id), $closure, $coroutineId); } @@ -64,7 +63,7 @@ public static function override(BackedEnum|UnitEnum|string $id, Closure $closure /** * Retrieve the value and store it if not exists. */ - public static function getOrSet(BackedEnum|UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed + public static function getOrSet(UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed { return parent::getOrSet(enum_value($id), $value, $coroutineId); } diff --git a/src/foundation/src/Http/Traits/HasCasts.php b/src/foundation/src/Http/Traits/HasCasts.php index bca1ffb37..cd83c4e37 100644 --- a/src/foundation/src/Http/Traits/HasCasts.php +++ b/src/foundation/src/Http/Traits/HasCasts.php @@ -4,7 +4,6 @@ namespace Hypervel\Foundation\Http\Traits; -use BackedEnum; use Carbon\Carbon; use Carbon\CarbonInterface; use DateTimeInterface; @@ -240,7 +239,7 @@ public function getDataObjectCastableInputValue(string $key, mixed $value): mixe /** * Get an enum case instance from a given class and value. */ - protected function getEnumCaseFromValue(string $enumClass, int|string $value): BackedEnum|UnitEnum + protected function getEnumCaseFromValue(string $enumClass, int|string $value): UnitEnum { return EnumCollector::getEnumCaseFromValue($enumClass, $value); } diff --git a/src/session/src/Contracts/Session.php b/src/session/src/Contracts/Session.php index 05ab1d772..a9fdbc160 100644 --- a/src/session/src/Contracts/Session.php +++ b/src/session/src/Contracts/Session.php @@ -4,7 +4,6 @@ namespace Hypervel\Session\Contracts; -use BackedEnum; use SessionHandlerInterface; use UnitEnum; @@ -48,27 +47,27 @@ public function all(): array; /** * Checks if a key exists. */ - public function exists(array|BackedEnum|UnitEnum|string $key): bool; + public function exists(array|UnitEnum|string $key): bool; /** * Checks if a key is present and not null. */ - public function has(array|BackedEnum|UnitEnum|string $key): bool; + public function has(array|UnitEnum|string $key): bool; /** * Get an item from the session. */ - public function get(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed; + public function get(UnitEnum|string $key, mixed $default = null): mixed; /** * Get the value of a given key and then forget it. */ - public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed; + public function pull(UnitEnum|string $key, mixed $default = null): mixed; /** * Put a key / value pair or array of key / value pairs in the session. */ - public function put(array|BackedEnum|UnitEnum|string $key, mixed $value = null): void; + public function put(array|UnitEnum|string $key, mixed $value = null): void; /** * Get the CSRF token value. @@ -83,12 +82,12 @@ public function regenerateToken(): void; /** * Remove an item from the session, returning its value. */ - public function remove(BackedEnum|UnitEnum|string $key): mixed; + public function remove(UnitEnum|string $key): mixed; /** * Remove one or many items from the session. */ - public function forget(array|BackedEnum|UnitEnum|string $keys): void; + public function forget(array|UnitEnum|string $keys): void; /** * Remove all of the items from the session. diff --git a/src/session/src/Store.php b/src/session/src/Store.php index 16c6b1d9b..13c668ed4 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -4,7 +4,6 @@ namespace Hypervel\Session; -use BackedEnum; use Closure; use Hyperf\Collection\Arr; use Hyperf\Context\Context; @@ -220,7 +219,7 @@ public function except(array $keys): array /** * Checks if a key exists. */ - public function exists(array|BackedEnum|UnitEnum|string $key): bool + public function exists(array|UnitEnum|string $key): bool { $placeholder = new stdClass(); @@ -232,7 +231,7 @@ public function exists(array|BackedEnum|UnitEnum|string $key): bool /** * Determine if the given key is missing from the session data. */ - public function missing(array|BackedEnum|UnitEnum|string $key): bool + public function missing(array|UnitEnum|string $key): bool { return ! $this->exists($key); } @@ -240,7 +239,7 @@ public function missing(array|BackedEnum|UnitEnum|string $key): bool /** * Determine if a key is present and not null. */ - public function has(array|BackedEnum|UnitEnum|string $key): bool + public function has(array|UnitEnum|string $key): bool { return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) { return is_null($this->get($key)); @@ -250,7 +249,7 @@ public function has(array|BackedEnum|UnitEnum|string $key): bool /** * Determine if any of the given keys are present and not null. */ - public function hasAny(array|BackedEnum|UnitEnum|string $key): bool + public function hasAny(array|UnitEnum|string $key): bool { return collect(is_array($key) ? $key : func_get_args())->filter(function ($key) { return ! is_null($this->get($key)); @@ -260,7 +259,7 @@ public function hasAny(array|BackedEnum|UnitEnum|string $key): bool /** * Get an item from the session. */ - public function get(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed + public function get(UnitEnum|string $key, mixed $default = null): mixed { return Arr::get($this->getAttributes(), enum_value($key), $default); } @@ -268,7 +267,7 @@ public function get(BackedEnum|UnitEnum|string $key, mixed $default = null): mix /** * Get the value of a given key and then forget it. */ - public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mixed + public function pull(UnitEnum|string $key, mixed $default = null): mixed { $attributes = $this->getAttributes(); $result = Arr::pull($attributes, enum_value($key), $default); @@ -281,7 +280,7 @@ public function pull(BackedEnum|UnitEnum|string $key, mixed $default = null): mi /** * Determine if the session contains old input. */ - public function hasOldInput(BackedEnum|UnitEnum|string|null $key = null): bool + public function hasOldInput(UnitEnum|string|null $key = null): bool { $old = $this->getOldInput($key); @@ -291,7 +290,7 @@ public function hasOldInput(BackedEnum|UnitEnum|string|null $key = null): bool /** * Get the requested item from the flashed input array. */ - public function getOldInput(BackedEnum|UnitEnum|string|null $key = null, mixed $default = null): mixed + public function getOldInput(UnitEnum|string|null $key = null, mixed $default = null): mixed { return Arr::get($this->get('_old_input', []), enum_value($key), $default); } @@ -307,7 +306,7 @@ public function replace(array $attributes): void /** * Put a key / value pair or array of key / value pairs in the session. */ - public function put(array|BackedEnum|UnitEnum|string $key, mixed $value = null): void + public function put(array|UnitEnum|string $key, mixed $value = null): void { if (! is_array($key)) { $key = [enum_value($key) => $value]; @@ -324,7 +323,7 @@ public function put(array|BackedEnum|UnitEnum|string $key, mixed $value = null): /** * Get an item from the session, or store the default value. */ - public function remember(BackedEnum|UnitEnum|string $key, Closure $callback): mixed + public function remember(UnitEnum|string $key, Closure $callback): mixed { if (! is_null($value = $this->get($key))) { return $value; @@ -338,7 +337,7 @@ public function remember(BackedEnum|UnitEnum|string $key, Closure $callback): mi /** * Push a value onto a session array. */ - public function push(BackedEnum|UnitEnum|string $key, mixed $value): void + public function push(UnitEnum|string $key, mixed $value): void { $array = $this->get($key, []); @@ -350,7 +349,7 @@ public function push(BackedEnum|UnitEnum|string $key, mixed $value): void /** * Increment the value of an item in the session. */ - public function increment(BackedEnum|UnitEnum|string $key, int $amount = 1): mixed + public function increment(UnitEnum|string $key, int $amount = 1): mixed { $this->put($key, $value = $this->get($key, 0) + $amount); @@ -360,7 +359,7 @@ public function increment(BackedEnum|UnitEnum|string $key, int $amount = 1): mix /** * Decrement the value of an item in the session. */ - public function decrement(BackedEnum|UnitEnum|string $key, int $amount = 1): int + public function decrement(UnitEnum|string $key, int $amount = 1): int { return $this->increment($key, $amount * -1); } @@ -368,7 +367,7 @@ public function decrement(BackedEnum|UnitEnum|string $key, int $amount = 1): int /** * Flash a key / value pair to the session. */ - public function flash(BackedEnum|UnitEnum|string $key, mixed $value = true): void + public function flash(UnitEnum|string $key, mixed $value = true): void { $key = enum_value($key); @@ -382,7 +381,7 @@ public function flash(BackedEnum|UnitEnum|string $key, mixed $value = true): voi /** * Flash a key / value pair to the session for immediate use. */ - public function now(BackedEnum|UnitEnum|string $key, mixed $value): void + public function now(UnitEnum|string $key, mixed $value): void { $key = enum_value($key); @@ -442,7 +441,7 @@ public function flashInput(array $value): void /** * Remove an item from the session, returning its value. */ - public function remove(BackedEnum|UnitEnum|string $key): mixed + public function remove(UnitEnum|string $key): mixed { $attributes = $this->getAttributes(); $result = Arr::pull($attributes, enum_value($key)); @@ -455,7 +454,7 @@ public function remove(BackedEnum|UnitEnum|string $key): mixed /** * Remove one or many items from the session. */ - public function forget(array|BackedEnum|UnitEnum|string $keys): void + public function forget(array|UnitEnum|string $keys): void { $attributes = $this->getAttributes(); Arr::forget($attributes, collect((array) $keys)->map(fn ($key) => enum_value($key))->all()); From 23a681504d2fee17dbe2eadc73a1b7d4e560f972 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 04:55:49 +0000 Subject: [PATCH 11/19] Remove remaining redundant BackedEnum from union types Since BackedEnum extends UnitEnum, having both in union types is redundant. This removes BackedEnum from union types in validation, permission, queue, and cache packages while keeping the import where needed for instanceof checks. --- src/cache/src/Repository.php | 8 ++-- .../src/Middlewares/PermissionMiddleware.php | 2 +- .../src/Middlewares/RoleMiddleware.php | 2 +- src/permission/src/Traits/HasPermission.php | 40 +++++++++---------- src/permission/src/Traits/HasRole.php | 24 +++++------ src/queue/src/Middleware/RateLimited.php | 3 +- src/validation/src/Rule.php | 5 +-- src/validation/src/Rules/In.php | 3 +- src/validation/src/Rules/NotIn.php | 3 +- 9 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 7ea3fb250..3a469848d 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -539,7 +539,7 @@ public function getName(): ?string /** * Determine if a cached value exists. * - * @param UnitEnum|string $key + * @param string|UnitEnum $key */ public function offsetExists($key): bool { @@ -549,7 +549,7 @@ public function offsetExists($key): bool /** * Retrieve an item from the cache by key. * - * @param UnitEnum|string $key + * @param string|UnitEnum $key */ public function offsetGet($key): mixed { @@ -559,7 +559,7 @@ public function offsetGet($key): mixed /** * Store an item in the cache for the default time. * - * @param UnitEnum|string $key + * @param string|UnitEnum $key * @param mixed $value */ public function offsetSet($key, $value): void @@ -570,7 +570,7 @@ public function offsetSet($key, $value): void /** * Remove an item from the cache. * - * @param UnitEnum|string $key + * @param string|UnitEnum $key */ public function offsetUnset($key): void { diff --git a/src/permission/src/Middlewares/PermissionMiddleware.php b/src/permission/src/Middlewares/PermissionMiddleware.php index 5ddc2f1f9..cff08432b 100644 --- a/src/permission/src/Middlewares/PermissionMiddleware.php +++ b/src/permission/src/Middlewares/PermissionMiddleware.php @@ -77,7 +77,7 @@ public function process( /** * Generate a unique identifier for the middleware based on the permissions. */ - public static function using(array|BackedEnum|int|string|UnitEnum ...$permissions): string + public static function using(array|UnitEnum|int|string ...$permissions): string { return static::class . ':' . self::parsePermissionsToString($permissions); } diff --git a/src/permission/src/Middlewares/RoleMiddleware.php b/src/permission/src/Middlewares/RoleMiddleware.php index 1bdc9c5a9..70efcdfbd 100644 --- a/src/permission/src/Middlewares/RoleMiddleware.php +++ b/src/permission/src/Middlewares/RoleMiddleware.php @@ -77,7 +77,7 @@ public function process( /** * Generate a unique identifier for the middleware based on the roles. */ - public static function using(array|BackedEnum|int|string|UnitEnum ...$roles): string + public static function using(array|UnitEnum|int|string ...$roles): string { return static::class . ':' . self::parseRolesToString($roles); } diff --git a/src/permission/src/Traits/HasPermission.php b/src/permission/src/Traits/HasPermission.php index b7378f150..6b49cab13 100644 --- a/src/permission/src/Traits/HasPermission.php +++ b/src/permission/src/Traits/HasPermission.php @@ -163,7 +163,7 @@ public function getPermissionsViaRoles(): BaseCollection /** * Check if the owner has a specific permission. */ - public function hasPermission(BackedEnum|int|string|UnitEnum $permission): bool + public function hasPermission(UnitEnum|int|string $permission): bool { // First check if there's a direct forbidden permission - this takes highest priority if ($this->hasForbiddenPermission($permission)) { @@ -181,7 +181,7 @@ public function hasPermission(BackedEnum|int|string|UnitEnum $permission): bool /** * Check if the owner has a direct permission. */ - public function hasDirectPermission(BackedEnum|int|string|UnitEnum $permission): bool + public function hasDirectPermission(UnitEnum|int|string $permission): bool { $ownerPermissions = $this->getCachedPermissions(); @@ -196,7 +196,7 @@ public function hasDirectPermission(BackedEnum|int|string|UnitEnum $permission): /** * Check if the owner has permission via roles. */ - public function hasPermissionViaRoles(BackedEnum|int|string|UnitEnum $permission): bool + public function hasPermissionViaRoles(UnitEnum|int|string $permission): bool { if (is_a($this->getOwnerType(), Role::class, true)) { return false; @@ -233,7 +233,7 @@ public function hasPermissionViaRoles(BackedEnum|int|string|UnitEnum $permission /** * Check if the owner has any of the specified permissions. */ - public function hasAnyPermissions(array|BackedEnum|int|string|UnitEnum ...$permissions): bool + public function hasAnyPermissions(array|UnitEnum|int|string ...$permissions): bool { return BaseCollection::make($permissions)->flatten()->some( fn ($permission) => $this->hasPermission($permission) @@ -243,7 +243,7 @@ public function hasAnyPermissions(array|BackedEnum|int|string|UnitEnum ...$permi /** * Check if the owner has all of the specified permissions. */ - public function hasAllPermissions(array|BackedEnum|int|string|UnitEnum ...$permissions): bool + public function hasAllPermissions(array|UnitEnum|int|string ...$permissions): bool { return BaseCollection::make($permissions)->flatten()->every( fn ($permission) => $this->hasPermission($permission) @@ -253,7 +253,7 @@ public function hasAllPermissions(array|BackedEnum|int|string|UnitEnum ...$permi /** * Check if the owner has all direct permissions. */ - public function hasAllDirectPermissions(array|BackedEnum|int|string|UnitEnum ...$permissions): bool + public function hasAllDirectPermissions(array|UnitEnum|int|string ...$permissions): bool { return BaseCollection::make($permissions)->flatten()->every( fn ($permission) => $this->hasDirectPermission($permission) @@ -263,7 +263,7 @@ public function hasAllDirectPermissions(array|BackedEnum|int|string|UnitEnum ... /** * Check if the owner has any direct permissions. */ - public function hasAnyDirectPermissions(array|BackedEnum|int|string|UnitEnum ...$permissions): bool + public function hasAnyDirectPermissions(array|UnitEnum|int|string ...$permissions): bool { return BaseCollection::make($permissions)->flatten()->some( fn ($permission) => $this->hasDirectPermission($permission) @@ -273,7 +273,7 @@ public function hasAnyDirectPermissions(array|BackedEnum|int|string|UnitEnum ... /** * Give permission to the owner. */ - public function givePermissionTo(array|BackedEnum|int|string|UnitEnum ...$permissions): static + public function givePermissionTo(array|UnitEnum|int|string ...$permissions): static { $result = $this->attachPermission($permissions); if (is_a($this->getOwnerType(), Role::class, true)) { @@ -291,7 +291,7 @@ public function givePermissionTo(array|BackedEnum|int|string|UnitEnum ...$permis /** * Give forbidden permission to the owner. */ - public function giveForbiddenTo(array|BackedEnum|int|string|UnitEnum ...$permissions): static + public function giveForbiddenTo(array|UnitEnum|int|string ...$permissions): static { $result = $this->attachPermission($permissions, true); if (is_a($this->getOwnerType(), Role::class, true)) { @@ -309,7 +309,7 @@ public function giveForbiddenTo(array|BackedEnum|int|string|UnitEnum ...$permiss /** * Revoke permission from the owner. */ - public function revokePermissionTo(array|BackedEnum|int|string|UnitEnum ...$permissions): static + public function revokePermissionTo(array|UnitEnum|int|string ...$permissions): static { $detachPermissions = $this->collectPermissions($permissions); @@ -330,8 +330,8 @@ public function revokePermissionTo(array|BackedEnum|int|string|UnitEnum ...$perm /** * Synchronize the owner's permissions with the given permission list. * - * @param array $allowPermissions - * @param array $forbiddenPermissions + * @param array $allowPermissions + * @param array $forbiddenPermissions */ public function syncPermissions(array $allowPermissions = [], array $forbiddenPermissions = []): array { @@ -365,7 +365,7 @@ public function syncPermissions(array $allowPermissions = [], array $forbiddenPe /** * Normalize permission value to field and value pair. */ - private function normalizePermissionValue(BackedEnum|int|string|UnitEnum $permission): array + private function normalizePermissionValue(UnitEnum|int|string $permission): array { $value = $this->extractPermissionValue($permission); $isId = $this->isPermissionIdType($permission); @@ -378,7 +378,7 @@ private function normalizePermissionValue(BackedEnum|int|string|UnitEnum $permis /** * Extract the actual value from a permission of any supported type. */ - private function extractPermissionValue(BackedEnum|int|string|UnitEnum $permission): int|string + private function extractPermissionValue(UnitEnum|int|string $permission): int|string { return match (true) { $permission instanceof BackedEnum => $permission->value, @@ -390,7 +390,7 @@ private function extractPermissionValue(BackedEnum|int|string|UnitEnum $permissi /** * Check if the permission should be treated as an ID (int) rather than name (string). */ - private function isPermissionIdType(BackedEnum|int|string|UnitEnum $permission): bool + private function isPermissionIdType(UnitEnum|int|string $permission): bool { return match (true) { is_int($permission) => true, @@ -403,7 +403,7 @@ private function isPermissionIdType(BackedEnum|int|string|UnitEnum $permission): /** * Separate permissions array into IDs and names collections. * - * @param array $permissions + * @param array $permissions */ private function separatePermissionsByType(array $permissions): array { @@ -426,7 +426,7 @@ private function separatePermissionsByType(array $permissions): array /** * Attach permission to the owner. * - * @param array $permissions + * @param array $permissions */ private function attachPermission(array $permissions, bool $isForbidden = false): static { @@ -454,7 +454,7 @@ private function attachPermission(array $permissions, bool $isForbidden = false) /** * Check if the owner has a forbidden permission. */ - public function hasForbiddenPermission(BackedEnum|int|string|UnitEnum $permission): bool + public function hasForbiddenPermission(UnitEnum|int|string $permission): bool { $ownerPermissions = $this->getCachedPermissions(); @@ -469,7 +469,7 @@ public function hasForbiddenPermission(BackedEnum|int|string|UnitEnum $permissio /** * Check if the owner has a forbidden permission via roles. */ - public function hasForbiddenPermissionViaRoles(BackedEnum|int|string|UnitEnum $permission): bool + public function hasForbiddenPermissionViaRoles(UnitEnum|int|string $permission): bool { // @phpstan-ignore function.alreadyNarrowedType (trait used by both Role and non-Role models) if (is_a(static::class, Role::class, true)) { @@ -506,7 +506,7 @@ public function hasForbiddenPermissionViaRoles(BackedEnum|int|string|UnitEnum $p /** * Returns array of permission ids. */ - private function collectPermissions(array|BackedEnum|int|string|UnitEnum ...$permissions): array + private function collectPermissions(array|UnitEnum|int|string ...$permissions): array { if (empty($permissions)) { return []; diff --git a/src/permission/src/Traits/HasRole.php b/src/permission/src/Traits/HasRole.php index 960906772..e99b4d604 100644 --- a/src/permission/src/Traits/HasRole.php +++ b/src/permission/src/Traits/HasRole.php @@ -98,7 +98,7 @@ public function roles(): MorphToMany /** * Check if the owner has a specific role. */ - public function hasRole(BackedEnum|int|string|UnitEnum $role): bool + public function hasRole(UnitEnum|int|string $role): bool { $roles = $this->getCachedRoles(); @@ -110,7 +110,7 @@ public function hasRole(BackedEnum|int|string|UnitEnum $role): bool /** * Normalize role value to field and value pair. */ - private function normalizeRoleValue(BackedEnum|int|string|UnitEnum $role): array + private function normalizeRoleValue(UnitEnum|int|string $role): array { $value = $this->extractRoleValue($role); $isId = $this->isRoleIdType($role); @@ -123,7 +123,7 @@ private function normalizeRoleValue(BackedEnum|int|string|UnitEnum $role): array /** * Extract the actual value from a role of any supported type. */ - private function extractRoleValue(BackedEnum|int|string|UnitEnum $role): int|string + private function extractRoleValue(UnitEnum|int|string $role): int|string { return match (true) { $role instanceof BackedEnum => $role->value, @@ -137,7 +137,7 @@ private function extractRoleValue(BackedEnum|int|string|UnitEnum $role): int|str * * @throws InvalidArgumentException if the role type is unsupported */ - private function isRoleIdType(BackedEnum|int|string|UnitEnum $role): bool + private function isRoleIdType(UnitEnum|int|string $role): bool { return match (true) { is_int($role) => true, @@ -150,7 +150,7 @@ private function isRoleIdType(BackedEnum|int|string|UnitEnum $role): bool /** * Separate roles array into IDs and names collections. * - * @param array $roles + * @param array $roles */ private function separateRolesByType(array $roles): array { @@ -173,7 +173,7 @@ private function separateRolesByType(array $roles): array /** * Check if the owner has any of the specified roles. * - * @param array $roles + * @param array $roles */ public function hasAnyRoles(array $roles): bool { @@ -189,7 +189,7 @@ public function hasAnyRoles(array $roles): bool /** * Check if the owner has all of the specified roles. * - * @param array $roles + * @param array $roles */ public function hasAllRoles(array $roles): bool { @@ -205,7 +205,7 @@ public function hasAllRoles(array $roles): bool /** * Get only the roles that match the specified roles from the owner's assigned roles. * - * @param array $roles + * @param array $roles */ public function onlyRoles(array $roles): Collection { @@ -230,7 +230,7 @@ public function onlyRoles(array $roles): Collection /** * Assign roles to the owner. */ - public function assignRole(array|BackedEnum|int|string|UnitEnum ...$roles): static + public function assignRole(array|UnitEnum|int|string ...$roles): static { $this->loadMissing('roles'); $roles = $this->collectRoles($roles); @@ -250,7 +250,7 @@ public function assignRole(array|BackedEnum|int|string|UnitEnum ...$roles): stat /** * Revoke the given role from owner. */ - public function removeRole(array|BackedEnum|int|string|UnitEnum ...$roles): static + public function removeRole(array|UnitEnum|int|string ...$roles): static { $detachRoles = $this->collectRoles($roles); @@ -265,7 +265,7 @@ public function removeRole(array|BackedEnum|int|string|UnitEnum ...$roles): stat /** * Synchronize the owner's roles with the given role list. */ - public function syncRoles(array|BackedEnum|int|string|UnitEnum ...$roles): array + public function syncRoles(array|UnitEnum|int|string ...$roles): array { $roles = $this->collectRoles($roles); @@ -280,7 +280,7 @@ public function syncRoles(array|BackedEnum|int|string|UnitEnum ...$roles): array /** * Returns array of role ids. */ - private function collectRoles(array|BackedEnum|int|string|UnitEnum ...$roles): array + private function collectRoles(array|UnitEnum|int|string ...$roles): array { $roles = BaseCollection::make($roles) ->flatten() diff --git a/src/queue/src/Middleware/RateLimited.php b/src/queue/src/Middleware/RateLimited.php index c5d96cbbe..b311da357 100644 --- a/src/queue/src/Middleware/RateLimited.php +++ b/src/queue/src/Middleware/RateLimited.php @@ -4,7 +4,6 @@ namespace Hypervel\Queue\Middleware; -use BackedEnum; use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; use Hyperf\Context\ApplicationContext; @@ -34,7 +33,7 @@ class RateLimited /** * Create a new middleware instance. */ - public function __construct(BackedEnum|string|UnitEnum $limiterName) + public function __construct(UnitEnum|string $limiterName) { $this->limiter = ApplicationContext::getContainer() ->get(RateLimiter::class); diff --git a/src/validation/src/Rule.php b/src/validation/src/Rule.php index 626cfa1aa..fc2652743 100644 --- a/src/validation/src/Rule.php +++ b/src/validation/src/Rule.php @@ -4,7 +4,6 @@ namespace Hypervel\Validation; -use BackedEnum; use Closure; use Hyperf\Contract\Arrayable; use Hypervel\Support\Arr; @@ -100,7 +99,7 @@ public static function exists(string $table, string $column = 'NULL'): Exists /** * Get an in rule builder instance. */ - public static function in(array|Arrayable|BackedEnum|string|UnitEnum $values): In + public static function in(array|Arrayable|UnitEnum|string $values): In { if ($values instanceof Arrayable) { $values = $values->toArray(); @@ -112,7 +111,7 @@ public static function in(array|Arrayable|BackedEnum|string|UnitEnum $values): I /** * Get a not_in rule builder instance. */ - public static function notIn(array|Arrayable|BackedEnum|string|UnitEnum $values): NotIn + public static function notIn(array|Arrayable|UnitEnum|string $values): NotIn { if ($values instanceof Arrayable) { $values = $values->toArray(); diff --git a/src/validation/src/Rules/In.php b/src/validation/src/Rules/In.php index 23ecbf97e..72eff8aa9 100644 --- a/src/validation/src/Rules/In.php +++ b/src/validation/src/Rules/In.php @@ -4,7 +4,6 @@ namespace Hypervel\Validation\Rules; -use BackedEnum; use Hyperf\Contract\Arrayable; use Stringable; use UnitEnum; @@ -26,7 +25,7 @@ class In implements Stringable /** * Create a new in rule instance. */ - public function __construct(array|Arrayable|BackedEnum|string|UnitEnum $values) + public function __construct(array|Arrayable|UnitEnum|string $values) { if ($values instanceof Arrayable) { $values = $values->toArray(); diff --git a/src/validation/src/Rules/NotIn.php b/src/validation/src/Rules/NotIn.php index 627404b81..f41f39c63 100644 --- a/src/validation/src/Rules/NotIn.php +++ b/src/validation/src/Rules/NotIn.php @@ -4,7 +4,6 @@ namespace Hypervel\Validation\Rules; -use BackedEnum; use Hyperf\Contract\Arrayable; use Stringable; use UnitEnum; @@ -26,7 +25,7 @@ class NotIn implements Stringable /** * Create a new "not in" rule instance. * - * @param array|Arrayable|BackedEnum|string|UnitEnum $values + * @param array|Arrayable|string|UnitEnum $values */ public function __construct($values) { From 02f275b07442aa636a7ecd7a95f18180742d8407 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:26:16 +0000 Subject: [PATCH 12/19] Add (string) casts for int-backed enum support Cast enum_value() results to string where underlying methods expect string parameters. This ensures int-backed enums (e.g., enum Foo: int) work correctly by converting their int values to strings. Fixed locations: - Context: 6 parent method calls expecting string $id - Gate: raw() call expecting string ability - FilesystemManager: disk name resolution - Redis: connection name resolution - PendingBatch/PendingChain: queue name (with null handling) - Cache Repository: increment/decrement/forever and event dispatch - TaggedCache: increment/decrement with itemKey - RedisTaggedCache: all key operations - Session Store: Arr::pull() calls requiring string keys Added int-backed enum tests for Context, Cache, Gate, Filesystem, and Redis to verify the casts work correctly. --- src/auth/src/Access/Gate.php | 2 +- src/bus/src/PendingBatch.php | 4 ++- src/bus/src/PendingChain.php | 4 ++- src/cache/src/RedisTaggedCache.php | 10 +++---- src/cache/src/Repository.php | 14 +++++----- src/cache/src/TaggedCache.php | 4 +-- src/core/src/Context/Context.php | 12 ++++----- src/filesystem/src/FilesystemManager.php | 2 +- src/redis/src/Redis.php | 2 +- src/session/src/Store.php | 4 +-- tests/Auth/Access/GateEnumTest.php | 17 ++++++++++++ tests/Cache/CacheRepositoryEnumTest.php | 15 +++++++++++ tests/Core/ContextEnumTest.php | 16 +++++++++++ tests/Filesystem/FilesystemManagerTest.php | 18 +++++++++++++ tests/Redis/RedisTest.php | 31 ++++++++++++++++++++++ 15 files changed, 128 insertions(+), 27 deletions(-) diff --git a/src/auth/src/Access/Gate.php b/src/auth/src/Access/Gate.php index e8741f0c9..eba440f8b 100644 --- a/src/auth/src/Access/Gate.php +++ b/src/auth/src/Access/Gate.php @@ -287,7 +287,7 @@ public function authorize(UnitEnum|string $ability, mixed $arguments = []): Resp public function inspect(UnitEnum|string $ability, mixed $arguments = []): Response { try { - $result = $this->raw(enum_value($ability), $arguments); + $result = $this->raw((string) enum_value($ability), $arguments); if ($result instanceof Response) { return $result; diff --git a/src/bus/src/PendingBatch.php b/src/bus/src/PendingBatch.php index 0f5524bee..00fdfb01f 100644 --- a/src/bus/src/PendingBatch.php +++ b/src/bus/src/PendingBatch.php @@ -214,7 +214,9 @@ public function connection(): ?string */ public function onQueue(BackedEnum|string|null $queue): static { - $this->options['queue'] = enum_value($queue); + $value = enum_value($queue); + + $this->options['queue'] = is_null($value) ? null : (string) $value; return $this; } diff --git a/src/bus/src/PendingChain.php b/src/bus/src/PendingChain.php index 8b86c5e30..463147a26 100644 --- a/src/bus/src/PendingChain.php +++ b/src/bus/src/PendingChain.php @@ -68,7 +68,9 @@ public function onConnection(?string $connection): static */ public function onQueue(BackedEnum|string|null $queue): static { - $this->queue = enum_value($queue); + $value = enum_value($queue); + + $this->queue = is_null($value) ? null : (string) $value; return $this; } diff --git a/src/cache/src/RedisTaggedCache.php b/src/cache/src/RedisTaggedCache.php index 89cdcc311..d0df9fd66 100644 --- a/src/cache/src/RedisTaggedCache.php +++ b/src/cache/src/RedisTaggedCache.php @@ -32,7 +32,7 @@ class RedisTaggedCache extends TaggedCache */ public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { - $key = enum_value($key); + $key = (string) enum_value($key); $this->tags->addEntry( $this->itemKey($key), @@ -51,7 +51,7 @@ public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateT return $this->putMany($key, $value); } - $key = enum_value($key); + $key = (string) enum_value($key); if (is_null($ttl)) { return $this->forever($key, $value); @@ -70,7 +70,7 @@ public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateT */ public function increment(UnitEnum|string $key, int $value = 1): bool|int { - $key = enum_value($key); + $key = (string) enum_value($key); $this->tags->addEntry($this->itemKey($key), updateWhen: 'NX'); @@ -82,7 +82,7 @@ public function increment(UnitEnum|string $key, int $value = 1): bool|int */ public function decrement(UnitEnum|string $key, int $value = 1): bool|int { - $key = enum_value($key); + $key = (string) enum_value($key); $this->tags->addEntry($this->itemKey($key), updateWhen: 'NX'); @@ -94,7 +94,7 @@ public function decrement(UnitEnum|string $key, int $value = 1): bool|int */ public function forever(UnitEnum|string $key, mixed $value): bool { - $key = enum_value($key); + $key = (string) enum_value($key); $this->tags->addEntry($this->itemKey($key)); diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 3a469848d..4a658fe98 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -123,7 +123,7 @@ public function get(array|UnitEnum|string $key, mixed $default = null): mixed return $this->many($key); } - $key = enum_value($key); + $key = (string) enum_value($key); $this->event(new RetrievingKey($this->getName(), $key)); @@ -198,7 +198,7 @@ public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateT return $this->putMany($key, $value); } - $key = enum_value($key); + $key = (string) enum_value($key); if ($ttl === null) { return $this->forever($key, $value); @@ -270,7 +270,7 @@ public function setMultiple(iterable $values, DateInterval|DateTimeInterface|int */ public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { - $key = enum_value($key); + $key = (string) enum_value($key); $seconds = null; @@ -308,7 +308,7 @@ public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInt */ public function increment(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->increment(enum_value($key), $value); + return $this->store->increment((string) enum_value($key), $value); } /** @@ -316,7 +316,7 @@ public function increment(UnitEnum|string $key, int $value = 1): bool|int */ public function decrement(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->decrement(enum_value($key), $value); + return $this->store->decrement((string) enum_value($key), $value); } /** @@ -324,7 +324,7 @@ public function decrement(UnitEnum|string $key, int $value = 1): bool|int */ public function forever(UnitEnum|string $key, mixed $value): bool { - $key = enum_value($key); + $key = (string) enum_value($key); $this->event(new WritingKey($this->getName(), $key, $value)); @@ -408,7 +408,7 @@ public function rememberForever(UnitEnum|string $key, Closure $callback): mixed */ public function forget(UnitEnum|string $key): bool { - $key = enum_value($key); + $key = (string) enum_value($key); $this->event(new ForgettingKey($this->getName(), $key)); diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php index a273136ef..ce684bd9f 100644 --- a/src/cache/src/TaggedCache.php +++ b/src/cache/src/TaggedCache.php @@ -51,7 +51,7 @@ public function putMany(array $values, DateInterval|DateTimeInterface|int|null $ */ public function increment(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->increment($this->itemKey(enum_value($key)), $value); + return $this->store->increment($this->itemKey((string) enum_value($key)), $value); } /** @@ -59,7 +59,7 @@ public function increment(UnitEnum|string $key, int $value = 1): bool|int */ public function decrement(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->decrement($this->itemKey(enum_value($key)), $value); + return $this->store->decrement($this->itemKey((string) enum_value($key)), $value); } /** diff --git a/src/core/src/Context/Context.php b/src/core/src/Context/Context.php index 07d0331cf..9b3c67257 100644 --- a/src/core/src/Context/Context.php +++ b/src/core/src/Context/Context.php @@ -25,7 +25,7 @@ public function __call(string $method, array $arguments): mixed */ public static function set(UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed { - return parent::set(enum_value($id), $value, $coroutineId); + return parent::set((string) enum_value($id), $value, $coroutineId); } /** @@ -33,7 +33,7 @@ public static function set(UnitEnum|string $id, mixed $value, ?int $coroutineId */ public static function get(UnitEnum|string $id, mixed $default = null, ?int $coroutineId = null): mixed { - return parent::get(enum_value($id), $default, $coroutineId); + return parent::get((string) enum_value($id), $default, $coroutineId); } /** @@ -41,7 +41,7 @@ public static function get(UnitEnum|string $id, mixed $default = null, ?int $cor */ public static function has(UnitEnum|string $id, ?int $coroutineId = null): bool { - return parent::has(enum_value($id), $coroutineId); + return parent::has((string) enum_value($id), $coroutineId); } /** @@ -49,7 +49,7 @@ public static function has(UnitEnum|string $id, ?int $coroutineId = null): bool */ public static function destroy(UnitEnum|string $id, ?int $coroutineId = null): void { - parent::destroy(enum_value($id), $coroutineId); + parent::destroy((string) enum_value($id), $coroutineId); } /** @@ -57,7 +57,7 @@ public static function destroy(UnitEnum|string $id, ?int $coroutineId = null): v */ public static function override(UnitEnum|string $id, Closure $closure, ?int $coroutineId = null): mixed { - return parent::override(enum_value($id), $closure, $coroutineId); + return parent::override((string) enum_value($id), $closure, $coroutineId); } /** @@ -65,7 +65,7 @@ public static function override(UnitEnum|string $id, Closure $closure, ?int $cor */ public static function getOrSet(UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed { - return parent::getOrSet(enum_value($id), $value, $coroutineId); + return parent::getOrSet((string) enum_value($id), $value, $coroutineId); } /** diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php index 9109b214e..28bce7d71 100644 --- a/src/filesystem/src/FilesystemManager.php +++ b/src/filesystem/src/FilesystemManager.php @@ -84,7 +84,7 @@ public function drive(UnitEnum|string|null $name = null): Filesystem */ public function disk(UnitEnum|string|null $name = null): FileSystem { - $name = enum_value($name) ?: $this->getDefaultDriver(); + $name = (string) enum_value($name) ?: $this->getDefaultDriver(); return $this->disks[$name] = $this->get($name); } diff --git a/src/redis/src/Redis.php b/src/redis/src/Redis.php index be6ccc695..8bc1f4fe1 100644 --- a/src/redis/src/Redis.php +++ b/src/redis/src/Redis.php @@ -140,6 +140,6 @@ public function connection(UnitEnum|string $name = 'default'): RedisProxy { return ApplicationContext::getContainer() ->get(RedisFactory::class) - ->get(enum_value($name)); + ->get((string) enum_value($name)); } } diff --git a/src/session/src/Store.php b/src/session/src/Store.php index 13c668ed4..dafca9c1d 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -270,7 +270,7 @@ public function get(UnitEnum|string $key, mixed $default = null): mixed public function pull(UnitEnum|string $key, mixed $default = null): mixed { $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, enum_value($key), $default); + $result = Arr::pull($attributes, (string) enum_value($key), $default); $this->setAttributes($attributes); @@ -444,7 +444,7 @@ public function flashInput(array $value): void public function remove(UnitEnum|string $key): mixed { $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, enum_value($key)); + $result = Arr::pull($attributes, (string) enum_value($key)); $this->setAttributes($attributes); diff --git a/tests/Auth/Access/GateEnumTest.php b/tests/Auth/Access/GateEnumTest.php index e759df0d0..516439315 100644 --- a/tests/Auth/Access/GateEnumTest.php +++ b/tests/Auth/Access/GateEnumTest.php @@ -21,6 +21,12 @@ enum AbilitiesBackedEnum: string case Edit = 'edit'; } +enum AbilitiesIntBackedEnum: int +{ + case CreatePost = 1; + case DeletePost = 2; +} + enum AbilitiesUnitEnum { case ManageUsers; @@ -57,6 +63,17 @@ public function testDefineWithUnitEnum(): void $this->assertTrue($gate->allows('ManageUsers')); } + public function testDefineWithIntBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(AbilitiesIntBackedEnum::CreatePost, fn ($user) => true); + + // Int value 1 should be cast to string '1' + $this->assertTrue($gate->allows('1')); + $this->assertTrue($gate->allows(AbilitiesIntBackedEnum::CreatePost)); + } + // ========================================================================= // allows() with enums // ========================================================================= diff --git a/tests/Cache/CacheRepositoryEnumTest.php b/tests/Cache/CacheRepositoryEnumTest.php index 45e5dbe34..b8fbb6c98 100644 --- a/tests/Cache/CacheRepositoryEnumTest.php +++ b/tests/Cache/CacheRepositoryEnumTest.php @@ -18,6 +18,12 @@ enum CacheKeyBackedEnum: string case Settings = 'settings'; } +enum CacheKeyIntBackedEnum: int +{ + case Counter = 1; + case Stats = 2; +} + enum CacheKeyUnitEnum { case Dashboard; @@ -58,6 +64,15 @@ public function testGetWithUnitEnum(): void $this->assertSame('dashboard-data', $repo->get(CacheKeyUnitEnum::Dashboard)); } + public function testGetWithIntBackedEnum(): void + { + $repo = $this->getRepository(); + // Int value 1 should be cast to string '1' + $repo->getStore()->shouldReceive('get')->once()->with('1')->andReturn('counter-value'); + + $this->assertSame('counter-value', $repo->get(CacheKeyIntBackedEnum::Counter)); + } + public function testHasWithBackedEnum(): void { $repo = $this->getRepository(); diff --git a/tests/Core/ContextEnumTest.php b/tests/Core/ContextEnumTest.php index 533feea4d..dd68c7be2 100644 --- a/tests/Core/ContextEnumTest.php +++ b/tests/Core/ContextEnumTest.php @@ -14,6 +14,12 @@ enum ContextKeyBackedEnum: string case Tenant = 'tenant'; } +enum ContextKeyIntBackedEnum: int +{ + case UserId = 1; + case SessionId = 2; +} + enum ContextKeyUnitEnum { case Locale; @@ -53,6 +59,16 @@ public function testSetAndGetWithUnitEnum(): void $this->assertSame('en-US', Context::get(ContextKeyUnitEnum::Locale)); } + public function testSetAndGetWithIntBackedEnum(): void + { + Context::set(ContextKeyIntBackedEnum::UserId, 'user-123'); + + $this->assertSame('user-123', Context::get(ContextKeyIntBackedEnum::UserId)); + + // Verify it's stored with string key (int value cast to string) + $this->assertSame('user-123', Context::get('1')); + } + public function testHasWithBackedEnum(): void { $this->assertFalse(Context::has(ContextKeyBackedEnum::CurrentUser)); diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 2b51b77e3..cbd07568a 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -241,6 +241,24 @@ public function testDiskAcceptsUnitEnum(): void $this->assertInstanceOf(Filesystem::class, $disk); } + public function testDiskAcceptsIntBackedEnum(): void + { + $container = $this->getContainer([ + 'disks' => [ + // Int value 1 should be cast to string '1' + '1' => [ + 'driver' => 'local', + 'root' => __DIR__ . '/tmp', + ], + ], + ]); + $filesystem = new FilesystemManager($container); + + $disk = $filesystem->disk(FilesystemTestIntBackedDisk::Local); + + $this->assertInstanceOf(Filesystem::class, $disk); + } + public function testDriveAcceptsStringBackedEnum(): void { $container = $this->getContainer([ diff --git a/tests/Redis/RedisTest.php b/tests/Redis/RedisTest.php index e8d2995fe..3d542386b 100644 --- a/tests/Redis/RedisTest.php +++ b/tests/Redis/RedisTest.php @@ -27,6 +27,12 @@ enum RedisTestStringBackedConnection: string case Cache = 'cache'; } +enum RedisTestIntBackedConnection: int +{ + case Primary = 1; + case Replica = 2; +} + enum RedisTestUnitConnection { case default; @@ -279,6 +285,31 @@ public function testConnectionAcceptsUnitEnum(): void $this->assertSame($mockRedisProxy, $result); } + public function testConnectionAcceptsIntBackedEnum(): void + { + $mockRedisProxy = Mockery::mock(RedisProxy::class); + + $mockRedisFactory = Mockery::mock(RedisFactory::class); + // Int value 1 should be cast to string '1' + $mockRedisFactory->shouldReceive('get') + ->with('1') + ->once() + ->andReturn($mockRedisProxy); + + $mockContainer = Mockery::mock(\Hypervel\Container\Contracts\Container::class); + $mockContainer->shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($mockRedisFactory); + + \Hypervel\Context\ApplicationContext::setContainer($mockContainer); + + $redis = new Redis(Mockery::mock(PoolFactory::class)); + + $result = $redis->connection(RedisTestIntBackedConnection::Primary); + + $this->assertSame($mockRedisProxy, $result); + } + /** * Create a mock Redis connection with configurable behavior. */ From 795df73f967332cdfb7d63e5dee8df8636e8457f Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 06:32:55 +0000 Subject: [PATCH 13/19] Add enum support to Schedule, Broadcasting, Events, and Collections - Schedule: job() accepts UnitEnum for queue/connection, useCache() for store - ManagesFrequencies: timezone() accepts UnitEnum - QueuedClosure: onConnection(), onQueue(), onGroup() accept UnitEnum - EventDispatcher: queue names support UnitEnum in queueHandler() - InteractsWithBroadcasting: broadcastVia() accepts UnitEnum - PendingBroadcast: via() accepts UnitEnum - Collection: lazy() returns Hypervel's LazyCollection for enum support - LazyCollection: countBy() supports enum group keys All use (string) enum_value() to handle int-backed enums correctly. --- .../src/InteractsWithBroadcasting.php | 7 +- src/broadcasting/src/PendingBroadcast.php | 3 +- .../src/Scheduling/ManagesFrequencies.php | 9 +- src/console/src/Scheduling/Schedule.php | 21 +- src/event/src/EventDispatcher.php | 4 + src/event/src/QueuedClosure.php | 33 +- src/support/src/Collection.php | 10 + src/support/src/LazyCollection.php | 29 ++ .../InteractsWithBroadcastingTest.php | 93 ++++++ tests/Broadcasting/PendingBroadcastTest.php | 113 +++++++ tests/Console/Scheduling/EventTest.php | 60 ++++ tests/Console/Scheduling/ScheduleTest.php | 103 +++++++ tests/Event/QueuedClosureTest.php | 171 +++++++++++ tests/Event/QueuedEventsTest.php | 145 +++++++++ tests/Support/CollectionEnumTest.php | 290 ------------------ tests/Support/CollectionTest.php | 288 +++++++++++++++++ tests/Support/LazyCollectionTest.php | 152 +++++++++ 17 files changed, 1232 insertions(+), 299 deletions(-) create mode 100644 tests/Broadcasting/InteractsWithBroadcastingTest.php create mode 100644 tests/Broadcasting/PendingBroadcastTest.php create mode 100644 tests/Event/QueuedClosureTest.php delete mode 100644 tests/Support/CollectionEnumTest.php create mode 100644 tests/Support/LazyCollectionTest.php diff --git a/src/broadcasting/src/InteractsWithBroadcasting.php b/src/broadcasting/src/InteractsWithBroadcasting.php index 418e7a591..dc9b32f24 100644 --- a/src/broadcasting/src/InteractsWithBroadcasting.php +++ b/src/broadcasting/src/InteractsWithBroadcasting.php @@ -5,6 +5,9 @@ namespace Hypervel\Broadcasting; use Hyperf\Collection\Arr; +use UnitEnum; + +use function Hypervel\Support\enum_value; trait InteractsWithBroadcasting { @@ -16,8 +19,10 @@ trait InteractsWithBroadcasting /** * Broadcast the event using a specific broadcaster. */ - public function broadcastVia(array|string|null $connection = null): static + public function broadcastVia(UnitEnum|array|string|null $connection = null): static { + $connection = is_null($connection) ? null : (string) enum_value($connection); + $this->broadcastConnection = is_null($connection) ? [null] : Arr::wrap($connection); diff --git a/src/broadcasting/src/PendingBroadcast.php b/src/broadcasting/src/PendingBroadcast.php index ee7c2dbaa..72f6e5ddb 100644 --- a/src/broadcasting/src/PendingBroadcast.php +++ b/src/broadcasting/src/PendingBroadcast.php @@ -5,6 +5,7 @@ namespace Hypervel\Broadcasting; use Psr\EventDispatcher\EventDispatcherInterface; +use UnitEnum; class PendingBroadcast { @@ -20,7 +21,7 @@ public function __construct( /** * Broadcast the event using a specific broadcaster. */ - public function via(?string $connection = null): static + public function via(UnitEnum|string|null $connection = null): static { if (method_exists($this->event, 'broadcastVia')) { $this->event->broadcastVia($connection); diff --git a/src/console/src/Scheduling/ManagesFrequencies.php b/src/console/src/Scheduling/ManagesFrequencies.php index 8c3c3c34f..56d923052 100644 --- a/src/console/src/Scheduling/ManagesFrequencies.php +++ b/src/console/src/Scheduling/ManagesFrequencies.php @@ -8,6 +8,9 @@ use DateTimeZone; use Hypervel\Support\Carbon; use InvalidArgumentException; +use UnitEnum; + +use function Hypervel\Support\enum_value; trait ManagesFrequencies { @@ -525,9 +528,11 @@ public function days(mixed $days): static /** * Set the timezone the date should be evaluated on. */ - public function timezone(DateTimeZone|string $timezone): static + public function timezone(DateTimeZone|UnitEnum|string $timezone): static { - $this->timezone = $timezone; + $this->timezone = $timezone instanceof UnitEnum + ? (string) enum_value($timezone) + : $timezone; return $this; } diff --git a/src/console/src/Scheduling/Schedule.php b/src/console/src/Scheduling/Schedule.php index 1b9493ebe..19c65fe7d 100644 --- a/src/console/src/Scheduling/Schedule.php +++ b/src/console/src/Scheduling/Schedule.php @@ -25,6 +25,9 @@ use Hypervel\Queue\Contracts\ShouldQueue; use Hypervel\Support\ProcessUtils; use RuntimeException; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @mixin \Hypervel\Console\Scheduling\PendingEventAttributes @@ -155,10 +158,16 @@ public function command(string $command, array $parameters = []): Event /** * Add a new job callback event to the schedule. */ - public function job(object|string $job, ?string $queue = null, ?string $connection = null): CallbackEvent - { + public function job( + object|string $job, + UnitEnum|string|null $queue = null, + UnitEnum|string|null $connection = null + ): CallbackEvent { $jobName = $job; + $queue = is_null($queue) ? null : (string) enum_value($queue); + $connection = is_null($connection) ? null : (string) enum_value($connection); + if (! is_string($job)) { $jobName = method_exists($job, 'displayName') ? $job->displayName() @@ -354,8 +363,14 @@ public function events(): array /** * Specify the cache store that should be used to store mutexes. */ - public function useCache(?string $store): static + public function useCache(UnitEnum|string|null $store): static { + if (is_null($store)) { + return $this; + } + + $store = (string) enum_value($store); + if ($this->eventMutex instanceof CacheAware) { $this->eventMutex->useStore($store); } diff --git a/src/event/src/EventDispatcher.php b/src/event/src/EventDispatcher.php index 70f834afa..592a22c6c 100644 --- a/src/event/src/EventDispatcher.php +++ b/src/event/src/EventDispatcher.php @@ -28,6 +28,8 @@ use Psr\Log\LoggerInterface; use ReflectionClass; +use function Hypervel\Support\enum_value; + class EventDispatcher implements EventDispatcherContract { use ReflectsClosures; @@ -468,6 +470,8 @@ protected function queueHandler(object|string $class, string $method, array $arg ? (isset($arguments[1]) ? $listener->viaQueue($arguments[1]) : $listener->viaQueue()) : $listener->queue ?? null; + $queue = is_null($queue) ? null : (string) enum_value($queue); + $delay = method_exists($listener, 'withDelay') ? (isset($arguments[1]) ? $listener->withDelay($arguments[1]) : $listener->withDelay()) : $listener->delay ?? null; diff --git a/src/event/src/QueuedClosure.php b/src/event/src/QueuedClosure.php index 4b096e408..02738855e 100644 --- a/src/event/src/QueuedClosure.php +++ b/src/event/src/QueuedClosure.php @@ -9,8 +9,10 @@ use DateTimeInterface; use Illuminate\Events\CallQueuedListener; use Laravel\SerializableClosure\SerializableClosure; +use UnitEnum; use function Hypervel\Bus\dispatch; +use function Hypervel\Support\enum_value; class QueuedClosure { @@ -24,6 +26,11 @@ class QueuedClosure */ public ?string $queue = null; + /** + * The job "group" the job should be sent to. + */ + public ?string $messageGroup = null; + /** * The number of seconds before the job should be made available. */ @@ -46,9 +53,31 @@ public function __construct(public Closure $closure) /** * Set the desired connection for the job. */ - public function onConnection(?string $connection): static + public function onConnection(UnitEnum|string|null $connection): static + { + $this->connection = is_null($connection) ? null : (string) enum_value($connection); + + return $this; + } + + /** + * Set the desired queue for the job. + */ + public function onQueue(UnitEnum|string|null $queue): static + { + $this->queue = is_null($queue) ? null : (string) enum_value($queue); + + return $this; + } + + /** + * Set the desired job "group". + * + * This feature is only supported by some queues, such as Amazon SQS. + */ + public function onGroup(UnitEnum|string $group): static { - $this->connection = $connection; + $this->messageGroup = (string) enum_value($group); return $this; } diff --git a/src/support/src/Collection.php b/src/support/src/Collection.php index 1418f1ad7..73f9285c8 100644 --- a/src/support/src/Collection.php +++ b/src/support/src/Collection.php @@ -96,6 +96,16 @@ public function keyBy(mixed $keyBy): static return new static($results); } + /** + * Get a lazy collection for the items in this collection. + * + * @return \Hypervel\Support\LazyCollection + */ + public function lazy(): LazyCollection + { + return new LazyCollection($this->items); + } + /** * Results array of items from Collection or Arrayable. * diff --git a/src/support/src/LazyCollection.php b/src/support/src/LazyCollection.php index c8c61ce5b..f105e51fb 100644 --- a/src/support/src/LazyCollection.php +++ b/src/support/src/LazyCollection.php @@ -49,4 +49,33 @@ public function chunkWhile(callable $callback): static } }); } + + /** + * Count the number of items in the collection by a field or using a callback. + * + * @param null|(callable(TValue, TKey): array-key)|string $countBy + * @return static + */ + public function countBy($countBy = null): static + { + $countBy = is_null($countBy) + ? $this->identity() + : $this->valueRetriever($countBy); + + return new static(function () use ($countBy) { + $counts = []; + + foreach ($this as $key => $value) { + $group = enum_value($countBy($value, $key)); + + if (empty($counts[$group])) { + $counts[$group] = 0; + } + + ++$counts[$group]; + } + + yield from $counts; + }); + } } diff --git a/tests/Broadcasting/InteractsWithBroadcastingTest.php b/tests/Broadcasting/InteractsWithBroadcastingTest.php new file mode 100644 index 000000000..1396fa2b8 --- /dev/null +++ b/tests/Broadcasting/InteractsWithBroadcastingTest.php @@ -0,0 +1,93 @@ +broadcastVia(InteractsWithBroadcastingTestConnectionStringEnum::Pusher); + + $this->assertSame(['pusher'], $event->broadcastConnections()); + } + + public function testBroadcastViaAcceptsUnitEnum(): void + { + $event = new TestBroadcastingEvent(); + + $event->broadcastVia(InteractsWithBroadcastingTestConnectionUnitEnum::redis); + + $this->assertSame(['redis'], $event->broadcastConnections()); + } + + public function testBroadcastViaAcceptsIntBackedEnum(): void + { + $event = new TestBroadcastingEvent(); + + $event->broadcastVia(InteractsWithBroadcastingTestConnectionIntEnum::Connection1); + + // Int value 1 should be cast to string '1' + $this->assertSame(['1'], $event->broadcastConnections()); + } + + public function testBroadcastViaAcceptsNull(): void + { + $event = new TestBroadcastingEvent(); + + $event->broadcastVia(null); + + $this->assertSame([null], $event->broadcastConnections()); + } + + public function testBroadcastViaAcceptsString(): void + { + $event = new TestBroadcastingEvent(); + + $event->broadcastVia('custom-connection'); + + $this->assertSame(['custom-connection'], $event->broadcastConnections()); + } + + public function testBroadcastViaIsChainable(): void + { + $event = new TestBroadcastingEvent(); + + $result = $event->broadcastVia('pusher'); + + $this->assertSame($event, $result); + } +} + +class TestBroadcastingEvent +{ + use InteractsWithBroadcasting; +} diff --git a/tests/Broadcasting/PendingBroadcastTest.php b/tests/Broadcasting/PendingBroadcastTest.php new file mode 100644 index 000000000..c0c2cb61c --- /dev/null +++ b/tests/Broadcasting/PendingBroadcastTest.php @@ -0,0 +1,113 @@ +shouldReceive('dispatch')->once(); + + $event = new TestPendingBroadcastEvent(); + $pending = new PendingBroadcast($dispatcher, $event); + + $result = $pending->via(PendingBroadcastTestConnectionStringEnum::Pusher); + + $this->assertSame(['pusher'], $event->broadcastConnections()); + $this->assertSame($pending, $result); + } + + public function testViaAcceptsUnitEnum(): void + { + $dispatcher = m::mock(EventDispatcherInterface::class); + $dispatcher->shouldReceive('dispatch')->once(); + + $event = new TestPendingBroadcastEvent(); + $pending = new PendingBroadcast($dispatcher, $event); + + $pending->via(PendingBroadcastTestConnectionUnitEnum::redis); + + $this->assertSame(['redis'], $event->broadcastConnections()); + } + + public function testViaAcceptsIntBackedEnum(): void + { + $dispatcher = m::mock(EventDispatcherInterface::class); + $dispatcher->shouldReceive('dispatch')->once(); + + $event = new TestPendingBroadcastEvent(); + $pending = new PendingBroadcast($dispatcher, $event); + + $pending->via(PendingBroadcastTestConnectionIntEnum::Connection1); + + // Int value 1 should be cast to string '1' + $this->assertSame(['1'], $event->broadcastConnections()); + } + + public function testViaAcceptsNull(): void + { + $dispatcher = m::mock(EventDispatcherInterface::class); + $dispatcher->shouldReceive('dispatch')->once(); + + $event = new TestPendingBroadcastEvent(); + $pending = new PendingBroadcast($dispatcher, $event); + + $pending->via(null); + + $this->assertSame([null], $event->broadcastConnections()); + } + + public function testViaAcceptsString(): void + { + $dispatcher = m::mock(EventDispatcherInterface::class); + $dispatcher->shouldReceive('dispatch')->once(); + + $event = new TestPendingBroadcastEvent(); + $pending = new PendingBroadcast($dispatcher, $event); + + $pending->via('custom-connection'); + + $this->assertSame(['custom-connection'], $event->broadcastConnections()); + } +} + +class TestPendingBroadcastEvent +{ + use InteractsWithBroadcasting; +} diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 6e354ebcc..7db196a1d 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Console\Scheduling; +use DateTimeZone; use Hyperf\Context\ApplicationContext; use Hyperf\Context\Context; use Hyperf\Stringable\Str; @@ -17,6 +18,24 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +enum EventTestTimezoneStringEnum: string +{ + case NewYork = 'America/New_York'; + case London = 'Europe/London'; +} + +enum EventTestTimezoneIntEnum: int +{ + case Zone1 = 1; + case Zone2 = 2; +} + +enum EventTestTimezoneUnitEnum +{ + case UTC; + case EST; +} + /** * @internal * @coversNothing @@ -156,4 +175,45 @@ public function testCustomMutexName() $this->assertSame('fancy-command-description', $event->mutexName()); } + + public function testTimezoneAcceptsStringBackedEnum(): void + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->timezone(EventTestTimezoneStringEnum::NewYork); + + // String-backed enum value should be used + $this->assertSame('America/New_York', $event->timezone); + } + + public function testTimezoneAcceptsUnitEnum(): void + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->timezone(EventTestTimezoneUnitEnum::UTC); + + // Unit enum name should be used + $this->assertSame('UTC', $event->timezone); + } + + public function testTimezoneAcceptsIntBackedEnum(): void + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->timezone(EventTestTimezoneIntEnum::Zone1); + + // Int value 1 should be cast to string '1' + $this->assertSame('1', $event->timezone); + } + + public function testTimezoneAcceptsDateTimeZoneObject(): void + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $tz = new DateTimeZone('UTC'); + $event->timezone($tz); + + // DateTimeZone object should be preserved + $this->assertSame($tz, $event->timezone); + } } diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index 143d98c0b..1fe8137f0 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Console\Scheduling; +use Hypervel\Console\Contracts\CacheAware; use Hypervel\Console\Contracts\EventMutex; use Hypervel\Console\Contracts\SchedulingMutex; use Hypervel\Console\Scheduling\Schedule; @@ -15,6 +16,36 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +enum ScheduleTestQueueStringEnum: string +{ + case High = 'high-priority'; + case Low = 'low-priority'; +} + +enum ScheduleTestQueueIntEnum: int +{ + case Priority1 = 1; + case Priority2 = 2; +} + +enum ScheduleTestQueueUnitEnum +{ + case default; + case emails; +} + +enum ScheduleTestCacheStoreEnum: string +{ + case Redis = 'redis'; + case File = 'file'; +} + +enum ScheduleTestCacheStoreIntEnum: int +{ + case Store1 = 1; + case Store2 = 2; +} + /** * @internal * @coversNothing @@ -72,6 +103,78 @@ public function testJobIsNotInstantiatedIfSuppliedAsClassname(): void self::assertSame(JobToTestWithSchedule::class, $scheduledJob->description); self::assertFalse($this->container->resolved(JobToTestWithSchedule::class)); } + + public function testJobAcceptsStringBackedEnumForQueueAndConnection(): void + { + $schedule = new Schedule(); + + // Should not throw - enums are accepted + $scheduledJob = $schedule->job( + JobToTestWithSchedule::class, + ScheduleTestQueueStringEnum::High, + ScheduleTestQueueStringEnum::Low + ); + + self::assertSame(JobToTestWithSchedule::class, $scheduledJob->description); + } + + public function testJobAcceptsUnitEnumForQueueAndConnection(): void + { + $schedule = new Schedule(); + + $scheduledJob = $schedule->job( + JobToTestWithSchedule::class, + ScheduleTestQueueUnitEnum::default, + ScheduleTestQueueUnitEnum::emails + ); + + self::assertSame(JobToTestWithSchedule::class, $scheduledJob->description); + } + + public function testJobAcceptsIntBackedEnumForQueueAndConnection(): void + { + $schedule = new Schedule(); + + // Int-backed enums should be cast to string + $scheduledJob = $schedule->job( + JobToTestWithSchedule::class, + ScheduleTestQueueIntEnum::Priority1, + ScheduleTestQueueIntEnum::Priority2 + ); + + self::assertSame(JobToTestWithSchedule::class, $scheduledJob->description); + } + + public function testUseCacheAcceptsStringBackedEnum(): void + { + $eventMutex = m::mock(EventMutex::class, CacheAware::class); + $eventMutex->shouldReceive('useStore')->once()->with('redis'); + + $schedulingMutex = m::mock(SchedulingMutex::class, CacheAware::class); + $schedulingMutex->shouldReceive('useStore')->once()->with('redis'); + + $this->container->instance(EventMutex::class, $eventMutex); + $this->container->instance(SchedulingMutex::class, $schedulingMutex); + + $schedule = new Schedule(); + $schedule->useCache(ScheduleTestCacheStoreEnum::Redis); + } + + public function testUseCacheAcceptsIntBackedEnum(): void + { + $eventMutex = m::mock(EventMutex::class, CacheAware::class); + // Int value 1 should be cast to string '1' + $eventMutex->shouldReceive('useStore')->once()->with('1'); + + $schedulingMutex = m::mock(SchedulingMutex::class, CacheAware::class); + $schedulingMutex->shouldReceive('useStore')->once()->with('1'); + + $this->container->instance(EventMutex::class, $eventMutex); + $this->container->instance(SchedulingMutex::class, $schedulingMutex); + + $schedule = new Schedule(); + $schedule->useCache(ScheduleTestCacheStoreIntEnum::Store1); + } } class JobToTestWithSchedule implements ShouldQueue diff --git a/tests/Event/QueuedClosureTest.php b/tests/Event/QueuedClosureTest.php new file mode 100644 index 000000000..5a835e1f1 --- /dev/null +++ b/tests/Event/QueuedClosureTest.php @@ -0,0 +1,171 @@ + null); + + $closure->onConnection(QueuedClosureTestConnectionStringEnum::Redis); + + $this->assertSame('redis', $closure->connection); + } + + public function testOnConnectionAcceptsUnitEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onConnection(QueuedClosureTestConnectionUnitEnum::sync); + + $this->assertSame('sync', $closure->connection); + } + + public function testOnConnectionAcceptsIntBackedEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onConnection(QueuedClosureTestConnectionIntEnum::Connection1); + + // Int value 1 should be cast to string '1' + $this->assertSame('1', $closure->connection); + } + + public function testOnConnectionAcceptsNull(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onConnection(null); + + $this->assertNull($closure->connection); + } + + public function testOnQueueAcceptsStringBackedEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onQueue(QueuedClosureTestConnectionStringEnum::Sqs); + + $this->assertSame('sqs', $closure->queue); + } + + public function testOnQueueAcceptsUnitEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onQueue(QueuedClosureTestConnectionUnitEnum::database); + + $this->assertSame('database', $closure->queue); + } + + public function testOnQueueAcceptsIntBackedEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onQueue(QueuedClosureTestConnectionIntEnum::Connection2); + + // Int value 2 should be cast to string '2' + $this->assertSame('2', $closure->queue); + } + + public function testOnQueueAcceptsNull(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onQueue(null); + + $this->assertNull($closure->queue); + } + + public function testOnGroupAcceptsStringBackedEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onGroup(QueuedClosureTestConnectionStringEnum::Redis); + + $this->assertSame('redis', $closure->messageGroup); + } + + public function testOnGroupAcceptsUnitEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onGroup(QueuedClosureTestConnectionUnitEnum::sync); + + $this->assertSame('sync', $closure->messageGroup); + } + + public function testOnGroupAcceptsIntBackedEnum(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure->onGroup(QueuedClosureTestConnectionIntEnum::Connection1); + + // Int value 1 should be cast to string '1' + $this->assertSame('1', $closure->messageGroup); + } + + public function testOnQueueSetsQueueProperty(): void + { + $closure = new QueuedClosure(fn () => null); + + $result = $closure->onQueue('high-priority'); + + $this->assertSame('high-priority', $closure->queue); + $this->assertSame($closure, $result); // Returns self for chaining + } + + public function testOnGroupSetsMessageGroupProperty(): void + { + $closure = new QueuedClosure(fn () => null); + + $result = $closure->onGroup('my-group'); + + $this->assertSame('my-group', $closure->messageGroup); + $this->assertSame($closure, $result); // Returns self for chaining + } + + public function testMethodsAreChainable(): void + { + $closure = new QueuedClosure(fn () => null); + + $closure + ->onConnection('redis') + ->onQueue('emails') + ->onGroup('group-1') + ->delay(60); + + $this->assertSame('redis', $closure->connection); + $this->assertSame('emails', $closure->queue); + $this->assertSame('group-1', $closure->messageGroup); + $this->assertSame(60, $closure->delay); + } +} diff --git a/tests/Event/QueuedEventsTest.php b/tests/Event/QueuedEventsTest.php index 3b4768e43..bb0d9798b 100644 --- a/tests/Event/QueuedEventsTest.php +++ b/tests/Event/QueuedEventsTest.php @@ -25,6 +25,24 @@ use function Hypervel\Event\queueable; +enum QueuedEventsTestQueueStringEnum: string +{ + case High = 'high-priority'; + case Low = 'low-priority'; +} + +enum QueuedEventsTestQueueIntEnum: int +{ + case Priority1 = 1; + case Priority2 = 2; +} + +enum QueuedEventsTestQueueUnitEnum +{ + case emails; + case notifications; +} + /** * @internal * @coversNothing @@ -234,6 +252,94 @@ public function testQueuePropagateMiddleware() }); } + public function testQueueAcceptsStringBackedEnumViaProperty(): void + { + $this->container + ->shouldReceive('get') + ->once() + ->with(TestDispatcherStringEnumQueueProperty::class) + ->andReturn(new TestDispatcherStringEnumQueueProperty()); + + $d = $this->getEventDispatcher(); + + $queue = m::mock(QueueFactoryContract::class); + $connection = m::mock(QueueContract::class); + // String-backed enum value should be used + $connection->shouldReceive('pushOn')->with('high-priority', m::type(CallQueuedListener::class))->once(); + $queue->shouldReceive('connection')->with(null)->once()->andReturn($connection); + + $d->setQueueResolver(fn () => $queue); + + $d->listen('some.event', TestDispatcherStringEnumQueueProperty::class . '@handle'); + $d->dispatch('some.event', ['foo', 'bar']); + } + + public function testQueueAcceptsUnitEnumViaProperty(): void + { + $this->container + ->shouldReceive('get') + ->once() + ->with(TestDispatcherUnitEnumQueueProperty::class) + ->andReturn(new TestDispatcherUnitEnumQueueProperty()); + + $d = $this->getEventDispatcher(); + + $queue = m::mock(QueueFactoryContract::class); + $connection = m::mock(QueueContract::class); + // Unit enum name should be used + $connection->shouldReceive('pushOn')->with('emails', m::type(CallQueuedListener::class))->once(); + $queue->shouldReceive('connection')->with(null)->once()->andReturn($connection); + + $d->setQueueResolver(fn () => $queue); + + $d->listen('some.event', TestDispatcherUnitEnumQueueProperty::class . '@handle'); + $d->dispatch('some.event', ['foo', 'bar']); + } + + public function testQueueAcceptsIntBackedEnumViaProperty(): void + { + $this->container + ->shouldReceive('get') + ->once() + ->with(TestDispatcherIntEnumQueueProperty::class) + ->andReturn(new TestDispatcherIntEnumQueueProperty()); + + $d = $this->getEventDispatcher(); + + $queue = m::mock(QueueFactoryContract::class); + $connection = m::mock(QueueContract::class); + // Int value 1 should be cast to string '1' + $connection->shouldReceive('pushOn')->with('1', m::type(CallQueuedListener::class))->once(); + $queue->shouldReceive('connection')->with(null)->once()->andReturn($connection); + + $d->setQueueResolver(fn () => $queue); + + $d->listen('some.event', TestDispatcherIntEnumQueueProperty::class . '@handle'); + $d->dispatch('some.event', ['foo', 'bar']); + } + + public function testQueueAcceptsStringBackedEnumViaMethod(): void + { + $this->container + ->shouldReceive('get') + ->once() + ->with(TestDispatcherStringEnumQueueMethod::class) + ->andReturn(new TestDispatcherStringEnumQueueMethod()); + + $d = $this->getEventDispatcher(); + + $queue = m::mock(QueueFactoryContract::class); + $connection = m::mock(QueueContract::class); + // String-backed enum value from viaQueue() should be used + $connection->shouldReceive('pushOn')->with('low-priority', m::type(CallQueuedListener::class))->once(); + $queue->shouldReceive('connection')->with(null)->once()->andReturn($connection); + + $d->setQueueResolver(fn () => $queue); + + $d->listen('some.event', TestDispatcherStringEnumQueueMethod::class . '@handle'); + $d->dispatch('some.event', ['foo', 'bar']); + } + private function getContainer(): Container { $container = new Container( @@ -373,3 +479,42 @@ public function withDelay($event) class TestDispatcherAnonymousQueuedClosureEvent { } + +class TestDispatcherStringEnumQueueProperty implements ShouldQueue +{ + public QueuedEventsTestQueueStringEnum $queue = QueuedEventsTestQueueStringEnum::High; + + public function handle(): void + { + } +} + +class TestDispatcherUnitEnumQueueProperty implements ShouldQueue +{ + public QueuedEventsTestQueueUnitEnum $queue = QueuedEventsTestQueueUnitEnum::emails; + + public function handle(): void + { + } +} + +class TestDispatcherIntEnumQueueProperty implements ShouldQueue +{ + public QueuedEventsTestQueueIntEnum $queue = QueuedEventsTestQueueIntEnum::Priority1; + + public function handle(): void + { + } +} + +class TestDispatcherStringEnumQueueMethod implements ShouldQueue +{ + public function handle(): void + { + } + + public function viaQueue(): QueuedEventsTestQueueStringEnum + { + return QueuedEventsTestQueueStringEnum::Low; + } +} diff --git a/tests/Support/CollectionEnumTest.php b/tests/Support/CollectionEnumTest.php deleted file mode 100644 index cede0f8a5..000000000 --- a/tests/Support/CollectionEnumTest.php +++ /dev/null @@ -1,290 +0,0 @@ -assertEquals([TestUnitEnum::Foo], $data->toArray()); - $this->assertCount(1, $data); - } - - public function testCollectionFromBackedEnum(): void - { - $data = new Collection(TestBackedEnum::Foo); - - $this->assertEquals([TestBackedEnum::Foo], $data->toArray()); - $this->assertCount(1, $data); - } - - public function testCollectionFromStringBackedEnum(): void - { - $data = new Collection(TestStringBackedEnum::Foo); - - $this->assertEquals([TestStringBackedEnum::Foo], $data->toArray()); - $this->assertCount(1, $data); - } - - public function testGroupByWithUnitEnumKey(): void - { - $data = new Collection([ - ['name' => TestUnitEnum::Foo, 'value' => 1], - ['name' => TestUnitEnum::Foo, 'value' => 2], - ['name' => TestUnitEnum::Bar, 'value' => 3], - ]); - - $result = $data->groupBy('name'); - - $this->assertArrayHasKey('Foo', $result->toArray()); - $this->assertArrayHasKey('Bar', $result->toArray()); - $this->assertCount(2, $result->get('Foo')); - $this->assertCount(1, $result->get('Bar')); - } - - public function testGroupByWithBackedEnumKey(): void - { - $data = new Collection([ - ['rating' => TestBackedEnum::Foo, 'url' => '1'], - ['rating' => TestBackedEnum::Bar, 'url' => '2'], - ]); - - $result = $data->groupBy('rating'); - - $expected = [ - TestBackedEnum::Foo->value => [['rating' => TestBackedEnum::Foo, 'url' => '1']], - TestBackedEnum::Bar->value => [['rating' => TestBackedEnum::Bar, 'url' => '2']], - ]; - - $this->assertEquals($expected, $result->toArray()); - } - - public function testGroupByWithStringBackedEnumKey(): void - { - $data = new Collection([ - ['category' => TestStringBackedEnum::Foo, 'value' => 1], - ['category' => TestStringBackedEnum::Foo, 'value' => 2], - ['category' => TestStringBackedEnum::Bar, 'value' => 3], - ]); - - $result = $data->groupBy('category'); - - $this->assertArrayHasKey(TestStringBackedEnum::Foo->value, $result->toArray()); - $this->assertArrayHasKey(TestStringBackedEnum::Bar->value, $result->toArray()); - } - - public function testGroupByWithCallableReturningEnum(): void - { - $data = new Collection([ - ['value' => 1], - ['value' => 2], - ['value' => 3], - ]); - - $result = $data->groupBy(fn ($item) => $item['value'] <= 2 ? TestUnitEnum::Foo : TestUnitEnum::Bar); - - $this->assertArrayHasKey('Foo', $result->toArray()); - $this->assertArrayHasKey('Bar', $result->toArray()); - $this->assertCount(2, $result->get('Foo')); - $this->assertCount(1, $result->get('Bar')); - } - - public function testKeyByWithUnitEnumKey(): void - { - $data = new Collection([ - ['name' => TestUnitEnum::Foo, 'value' => 1], - ['name' => TestUnitEnum::Bar, 'value' => 2], - ]); - - $result = $data->keyBy('name'); - - $this->assertArrayHasKey('Foo', $result->toArray()); - $this->assertArrayHasKey('Bar', $result->toArray()); - $this->assertEquals(1, $result->get('Foo')['value']); - $this->assertEquals(2, $result->get('Bar')['value']); - } - - public function testKeyByWithBackedEnumKey(): void - { - $data = new Collection([ - ['rating' => TestBackedEnum::Foo, 'value' => 'first'], - ['rating' => TestBackedEnum::Bar, 'value' => 'second'], - ]); - - $result = $data->keyBy('rating'); - - $this->assertArrayHasKey(TestBackedEnum::Foo->value, $result->toArray()); - $this->assertArrayHasKey(TestBackedEnum::Bar->value, $result->toArray()); - } - - public function testKeyByWithCallableReturningEnum(): void - { - $data = new Collection([ - ['id' => 1, 'value' => 'first'], - ['id' => 2, 'value' => 'second'], - ]); - - $result = $data->keyBy(fn ($item) => $item['id'] === 1 ? TestUnitEnum::Foo : TestUnitEnum::Bar); - - $this->assertArrayHasKey('Foo', $result->toArray()); - $this->assertArrayHasKey('Bar', $result->toArray()); - } - - public function testWhereWithEnumValue(): void - { - $data = new Collection([ - ['id' => 1, 'status' => TestBackedEnum::Foo], - ['id' => 2, 'status' => TestBackedEnum::Bar], - ['id' => 3, 'status' => TestBackedEnum::Foo], - ]); - - $result = $data->where('status', TestBackedEnum::Foo); - - $this->assertCount(2, $result); - $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); - } - - public function testWhereWithUnitEnumValue(): void - { - $data = new Collection([ - ['id' => 1, 'type' => TestUnitEnum::Foo], - ['id' => 2, 'type' => TestUnitEnum::Bar], - ['id' => 3, 'type' => TestUnitEnum::Foo], - ]); - - $result = $data->where('type', TestUnitEnum::Foo); - - $this->assertCount(2, $result); - $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); - } - - public function testFirstWhereWithEnum(): void - { - $data = new Collection([ - ['id' => 1, 'name' => TestUnitEnum::Foo], - ['id' => 2, 'name' => TestUnitEnum::Bar], - ['id' => 3, 'name' => TestUnitEnum::Baz], - ]); - - $this->assertSame(2, $data->firstWhere('name', TestUnitEnum::Bar)['id']); - $this->assertSame(3, $data->firstWhere('name', TestUnitEnum::Baz)['id']); - } - - public function testMapIntoWithIntBackedEnum(): void - { - $data = new Collection([1, 2]); - - $result = $data->mapInto(TestBackedEnum::class); - - $this->assertSame(TestBackedEnum::Foo, $result->get(0)); - $this->assertSame(TestBackedEnum::Bar, $result->get(1)); - } - - public function testMapIntoWithStringBackedEnum(): void - { - $data = new Collection(['foo', 'bar']); - - $result = $data->mapInto(TestStringBackedEnum::class); - - $this->assertSame(TestStringBackedEnum::Foo, $result->get(0)); - $this->assertSame(TestStringBackedEnum::Bar, $result->get(1)); - } - - public function testCollectHelperWithUnitEnum(): void - { - $data = collect(TestUnitEnum::Foo); - - $this->assertEquals([TestUnitEnum::Foo], $data->toArray()); - $this->assertCount(1, $data); - } - - public function testCollectHelperWithBackedEnum(): void - { - $data = collect(TestBackedEnum::Bar); - - $this->assertEquals([TestBackedEnum::Bar], $data->toArray()); - $this->assertCount(1, $data); - } - - public function testWhereStrictWithEnums(): void - { - $data = new Collection([ - ['id' => 1, 'status' => TestBackedEnum::Foo], - ['id' => 2, 'status' => TestBackedEnum::Bar], - ]); - - $result = $data->whereStrict('status', TestBackedEnum::Foo); - - $this->assertCount(1, $result); - $this->assertEquals(1, $result->first()['id']); - } - - public function testEnumValuesArePreservedInCollection(): void - { - $data = new Collection([TestUnitEnum::Foo, TestBackedEnum::Bar, TestStringBackedEnum::Baz]); - - $this->assertSame(TestUnitEnum::Foo, $data->get(0)); - $this->assertSame(TestBackedEnum::Bar, $data->get(1)); - $this->assertSame(TestStringBackedEnum::Baz, $data->get(2)); - } - - public function testContainsWithEnum(): void - { - $data = new Collection([TestUnitEnum::Foo, TestUnitEnum::Bar]); - - $this->assertTrue($data->contains(TestUnitEnum::Foo)); - $this->assertTrue($data->contains(TestUnitEnum::Bar)); - $this->assertFalse($data->contains(TestUnitEnum::Baz)); - } - - public function testGroupByMixedEnumTypes(): void - { - $payload = [ - ['name' => TestUnitEnum::Foo, 'url' => '1'], - ['name' => TestBackedEnum::Foo, 'url' => '1'], - ['name' => TestStringBackedEnum::Foo, 'url' => '2'], - ]; - - $data = new Collection($payload); - $result = $data->groupBy('name'); - - // UnitEnum uses name ('Foo'), IntBackedEnum uses value (1), StringBackedEnum uses value ('foo') - $this->assertEquals([ - 'Foo' => [$payload[0]], - 1 => [$payload[1]], - 'foo' => [$payload[2]], - ], $result->toArray()); - } -} - -enum TestUnitEnum -{ - case Foo; - case Bar; - case Baz; -} - -enum TestBackedEnum: int -{ - case Foo = 1; - case Bar = 2; - case Baz = 3; -} - -enum TestStringBackedEnum: string -{ - case Foo = 'foo'; - case Bar = 'bar'; - case Baz = 'baz'; -} diff --git a/tests/Support/CollectionTest.php b/tests/Support/CollectionTest.php index 0e3ec43bd..865253f8b 100644 --- a/tests/Support/CollectionTest.php +++ b/tests/Support/CollectionTest.php @@ -284,6 +284,273 @@ public function testOperatorForWhereWithNestedData(): void $this->assertCount(1, $result); $this->assertEquals(25, $result->first()['user']['age']); } + + public function testCollectionFromUnitEnum(): void + { + $data = new Collection(CollectionTestUnitEnum::Foo); + + $this->assertEquals([CollectionTestUnitEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testCollectionFromBackedEnum(): void + { + $data = new Collection(CollectionTestIntEnum::Foo); + + $this->assertEquals([CollectionTestIntEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testCollectionFromStringBackedEnum(): void + { + $data = new Collection(CollectionTestStringEnum::Foo); + + $this->assertEquals([CollectionTestStringEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testGroupByWithUnitEnumKey(): void + { + $data = new Collection([ + ['name' => CollectionTestUnitEnum::Foo, 'value' => 1], + ['name' => CollectionTestUnitEnum::Foo, 'value' => 2], + ['name' => CollectionTestUnitEnum::Bar, 'value' => 3], + ]); + + $result = $data->groupBy('name'); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + $this->assertCount(2, $result->get('Foo')); + $this->assertCount(1, $result->get('Bar')); + } + + public function testGroupByWithIntBackedEnumKey(): void + { + $data = new Collection([ + ['rating' => CollectionTestIntEnum::Foo, 'url' => '1'], + ['rating' => CollectionTestIntEnum::Bar, 'url' => '2'], + ]); + + $result = $data->groupBy('rating'); + + $expected = [ + CollectionTestIntEnum::Foo->value => [['rating' => CollectionTestIntEnum::Foo, 'url' => '1']], + CollectionTestIntEnum::Bar->value => [['rating' => CollectionTestIntEnum::Bar, 'url' => '2']], + ]; + + $this->assertEquals($expected, $result->toArray()); + } + + public function testGroupByWithStringBackedEnumKey(): void + { + $data = new Collection([ + ['category' => CollectionTestStringEnum::Foo, 'value' => 1], + ['category' => CollectionTestStringEnum::Foo, 'value' => 2], + ['category' => CollectionTestStringEnum::Bar, 'value' => 3], + ]); + + $result = $data->groupBy('category'); + + $this->assertArrayHasKey(CollectionTestStringEnum::Foo->value, $result->toArray()); + $this->assertArrayHasKey(CollectionTestStringEnum::Bar->value, $result->toArray()); + } + + public function testGroupByWithCallableReturningEnum(): void + { + $data = new Collection([ + ['value' => 1], + ['value' => 2], + ['value' => 3], + ]); + + $result = $data->groupBy(fn ($item) => $item['value'] <= 2 ? CollectionTestUnitEnum::Foo : CollectionTestUnitEnum::Bar); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + $this->assertCount(2, $result->get('Foo')); + $this->assertCount(1, $result->get('Bar')); + } + + public function testKeyByWithUnitEnumKey(): void + { + $data = new Collection([ + ['name' => CollectionTestUnitEnum::Foo, 'value' => 1], + ['name' => CollectionTestUnitEnum::Bar, 'value' => 2], + ]); + + $result = $data->keyBy('name'); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + $this->assertEquals(1, $result->get('Foo')['value']); + $this->assertEquals(2, $result->get('Bar')['value']); + } + + public function testKeyByWithIntBackedEnumKey(): void + { + $data = new Collection([ + ['rating' => CollectionTestIntEnum::Foo, 'value' => 'first'], + ['rating' => CollectionTestIntEnum::Bar, 'value' => 'second'], + ]); + + $result = $data->keyBy('rating'); + + $this->assertArrayHasKey(CollectionTestIntEnum::Foo->value, $result->toArray()); + $this->assertArrayHasKey(CollectionTestIntEnum::Bar->value, $result->toArray()); + } + + public function testKeyByWithCallableReturningEnum(): void + { + $data = new Collection([ + ['id' => 1, 'value' => 'first'], + ['id' => 2, 'value' => 'second'], + ]); + + $result = $data->keyBy(fn ($item) => $item['id'] === 1 ? CollectionTestUnitEnum::Foo : CollectionTestUnitEnum::Bar); + + $this->assertArrayHasKey('Foo', $result->toArray()); + $this->assertArrayHasKey('Bar', $result->toArray()); + } + + public function testWhereWithIntBackedEnumValue(): void + { + $data = new Collection([ + ['id' => 1, 'status' => CollectionTestIntEnum::Foo], + ['id' => 2, 'status' => CollectionTestIntEnum::Bar], + ['id' => 3, 'status' => CollectionTestIntEnum::Foo], + ]); + + $result = $data->where('status', CollectionTestIntEnum::Foo); + + $this->assertCount(2, $result); + $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); + } + + public function testWhereWithUnitEnumValue(): void + { + $data = new Collection([ + ['id' => 1, 'type' => CollectionTestUnitEnum::Foo], + ['id' => 2, 'type' => CollectionTestUnitEnum::Bar], + ['id' => 3, 'type' => CollectionTestUnitEnum::Foo], + ]); + + $result = $data->where('type', CollectionTestUnitEnum::Foo); + + $this->assertCount(2, $result); + $this->assertEquals([1, 3], $result->pluck('id')->values()->toArray()); + } + + public function testFirstWhereWithEnum(): void + { + $data = new Collection([ + ['id' => 1, 'name' => CollectionTestUnitEnum::Foo], + ['id' => 2, 'name' => CollectionTestUnitEnum::Bar], + ['id' => 3, 'name' => CollectionTestUnitEnum::Baz], + ]); + + $this->assertSame(2, $data->firstWhere('name', CollectionTestUnitEnum::Bar)['id']); + $this->assertSame(3, $data->firstWhere('name', CollectionTestUnitEnum::Baz)['id']); + } + + public function testMapIntoWithIntBackedEnum(): void + { + $data = new Collection([1, 2]); + + $result = $data->mapInto(CollectionTestIntEnum::class); + + $this->assertSame(CollectionTestIntEnum::Foo, $result->get(0)); + $this->assertSame(CollectionTestIntEnum::Bar, $result->get(1)); + } + + public function testMapIntoWithStringBackedEnum(): void + { + $data = new Collection(['foo', 'bar']); + + $result = $data->mapInto(CollectionTestStringEnum::class); + + $this->assertSame(CollectionTestStringEnum::Foo, $result->get(0)); + $this->assertSame(CollectionTestStringEnum::Bar, $result->get(1)); + } + + public function testCollectHelperWithUnitEnum(): void + { + $data = collect(CollectionTestUnitEnum::Foo); + + $this->assertEquals([CollectionTestUnitEnum::Foo], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testCollectHelperWithBackedEnum(): void + { + $data = collect(CollectionTestIntEnum::Bar); + + $this->assertEquals([CollectionTestIntEnum::Bar], $data->toArray()); + $this->assertCount(1, $data); + } + + public function testWhereStrictWithEnums(): void + { + $data = new Collection([ + ['id' => 1, 'status' => CollectionTestIntEnum::Foo], + ['id' => 2, 'status' => CollectionTestIntEnum::Bar], + ]); + + $result = $data->whereStrict('status', CollectionTestIntEnum::Foo); + + $this->assertCount(1, $result); + $this->assertEquals(1, $result->first()['id']); + } + + public function testEnumValuesArePreservedInCollection(): void + { + $data = new Collection([CollectionTestUnitEnum::Foo, CollectionTestIntEnum::Bar, CollectionTestStringEnum::Baz]); + + $this->assertSame(CollectionTestUnitEnum::Foo, $data->get(0)); + $this->assertSame(CollectionTestIntEnum::Bar, $data->get(1)); + $this->assertSame(CollectionTestStringEnum::Baz, $data->get(2)); + } + + public function testContainsWithEnum(): void + { + $data = new Collection([CollectionTestUnitEnum::Foo, CollectionTestUnitEnum::Bar]); + + $this->assertTrue($data->contains(CollectionTestUnitEnum::Foo)); + $this->assertTrue($data->contains(CollectionTestUnitEnum::Bar)); + $this->assertFalse($data->contains(CollectionTestUnitEnum::Baz)); + } + + public function testGroupByMixedEnumTypes(): void + { + $payload = [ + ['name' => CollectionTestUnitEnum::Foo, 'url' => '1'], + ['name' => CollectionTestIntEnum::Foo, 'url' => '1'], + ['name' => CollectionTestStringEnum::Foo, 'url' => '2'], + ]; + + $data = new Collection($payload); + $result = $data->groupBy('name'); + + // UnitEnum uses name ('Foo'), IntBackedEnum uses value (1), StringBackedEnum uses value ('foo') + $this->assertEquals([ + 'Foo' => [$payload[0]], + 1 => [$payload[1]], + 'foo' => [$payload[2]], + ], $result->toArray()); + } + + public function testCountByWithUnitEnum(): void + { + $data = new Collection([ + ['type' => CollectionTestUnitEnum::Foo], + ['type' => CollectionTestUnitEnum::Foo], + ['type' => CollectionTestUnitEnum::Bar], + ]); + + $result = $data->countBy('type'); + + $this->assertEquals(['Foo' => 2, 'Bar' => 1], $result->all()); + } } class CollectionTestStringable implements Stringable @@ -297,3 +564,24 @@ public function __toString(): string return $this->value; } } + +enum CollectionTestUnitEnum +{ + case Foo; + case Bar; + case Baz; +} + +enum CollectionTestIntEnum: int +{ + case Foo = 1; + case Bar = 2; + case Baz = 3; +} + +enum CollectionTestStringEnum: string +{ + case Foo = 'foo'; + case Bar = 'bar'; + case Baz = 'baz'; +} diff --git a/tests/Support/LazyCollectionTest.php b/tests/Support/LazyCollectionTest.php new file mode 100644 index 000000000..b1404d9c9 --- /dev/null +++ b/tests/Support/LazyCollectionTest.php @@ -0,0 +1,152 @@ + 'electronics'], + ['category' => 'electronics'], + ['category' => 'clothing'], + ]); + + $result = $data->countBy('category'); + + $this->assertEquals(['electronics' => 2, 'clothing' => 1], $result->all()); + } + + public function testCountByWithCallback(): void + { + $data = new LazyCollection([1, 2, 3, 4, 5]); + + $result = $data->countBy(fn ($value) => $value % 2 === 0 ? 'even' : 'odd'); + + $this->assertEquals(['odd' => 3, 'even' => 2], $result->all()); + } + + public function testCountByWithNullCallback(): void + { + $data = new LazyCollection(['a', 'b', 'a', 'c', 'a']); + + $result = $data->countBy(); + + $this->assertEquals(['a' => 3, 'b' => 1, 'c' => 1], $result->all()); + } + + public function testCountByWithIntegerKeys(): void + { + $data = new LazyCollection([ + ['rating' => 5], + ['rating' => 3], + ['rating' => 5], + ['rating' => 5], + ]); + + $result = $data->countBy('rating'); + + $this->assertEquals([5 => 3, 3 => 1], $result->all()); + } + + public function testCountByIsLazy(): void + { + $called = 0; + + $data = new LazyCollection(function () use (&$called) { + for ($i = 0; $i < 5; ++$i) { + ++$called; + yield ['type' => $i % 2 === 0 ? 'even' : 'odd']; + } + }); + + $result = $data->countBy('type'); + + // Generator not yet consumed + $this->assertEquals(0, $called); + + // Now consume + $result->all(); + $this->assertEquals(5, $called); + } + + public function testCountByWithUnitEnum(): void + { + $data = new LazyCollection([ + ['type' => LazyCollectionTestUnitEnum::Foo], + ['type' => LazyCollectionTestUnitEnum::Foo], + ['type' => LazyCollectionTestUnitEnum::Bar], + ]); + + $result = $data->countBy('type'); + + $this->assertEquals(['Foo' => 2, 'Bar' => 1], $result->all()); + } + + public function testCountByWithStringBackedEnum(): void + { + $data = new LazyCollection([ + ['category' => LazyCollectionTestStringEnum::Foo], + ['category' => LazyCollectionTestStringEnum::Bar], + ['category' => LazyCollectionTestStringEnum::Foo], + ]); + + $result = $data->countBy('category'); + + $this->assertEquals(['foo' => 2, 'bar' => 1], $result->all()); + } + + public function testCountByWithIntBackedEnum(): void + { + $data = new LazyCollection([ + ['rating' => LazyCollectionTestIntEnum::Foo], + ['rating' => LazyCollectionTestIntEnum::Bar], + ['rating' => LazyCollectionTestIntEnum::Foo], + ]); + + $result = $data->countBy('rating'); + + // Int-backed enum values should be used as keys + $this->assertEquals([1 => 2, 2 => 1], $result->all()); + } + + public function testCountByWithCallableReturningEnum(): void + { + $data = new LazyCollection([ + ['value' => 1], + ['value' => 2], + ['value' => 3], + ]); + + $result = $data->countBy(fn ($item) => $item['value'] <= 2 ? LazyCollectionTestUnitEnum::Foo : LazyCollectionTestUnitEnum::Bar); + + $this->assertEquals(['Foo' => 2, 'Bar' => 1], $result->all()); + } +} From dc4fd7f1b5824f01501ff2c00fe5c70df855461b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 06:42:59 +0000 Subject: [PATCH 14/19] Add enum support to InteractsWithData and Query Builder - InteractsWithData: date() timezone param accepts UnitEnum - Query/Builder: castBinding() supports UnitEnum (not just BackedEnum) Both use enum_value() for consistent enum handling. --- src/core/src/Database/Query/Builder.php | 17 ++ src/support/src/Traits/InteractsWithData.php | 7 +- tests/Core/Database/Query/BuilderTest.php | 108 ++++++++++++ .../Support/Traits/InteractsWithDataTest.php | 164 ++++++++++++++++++ 4 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 tests/Core/Database/Query/BuilderTest.php create mode 100644 tests/Support/Traits/InteractsWithDataTest.php diff --git a/src/core/src/Database/Query/Builder.php b/src/core/src/Database/Query/Builder.php index 10830bf6b..bba488a57 100644 --- a/src/core/src/Database/Query/Builder.php +++ b/src/core/src/Database/Query/Builder.php @@ -8,6 +8,9 @@ use Hyperf\Database\Query\Builder as BaseBuilder; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\LazyCollection; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @method $this from(\Closure|\Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder|string $table, string|null $as = null) @@ -111,4 +114,18 @@ public function pluck($column, $key = null) { return new BaseCollection(parent::pluck($column, $key)->all()); } + + /** + * Cast the given binding value. + * + * Overrides Hyperf's implementation to support UnitEnum (not just BackedEnum). + */ + public function castBinding(mixed $value): mixed + { + if ($value instanceof UnitEnum) { + return enum_value($value); + } + + return $value; + } } diff --git a/src/support/src/Traits/InteractsWithData.php b/src/support/src/Traits/InteractsWithData.php index 8cc3ced64..08a146856 100644 --- a/src/support/src/Traits/InteractsWithData.php +++ b/src/support/src/Traits/InteractsWithData.php @@ -11,6 +11,9 @@ use Hypervel\Support\Str; use stdClass; use Stringable; +use UnitEnum; + +use function Hypervel\Support\enum_value; trait InteractsWithData { @@ -231,8 +234,10 @@ public function float(string $key, float $default = 0.0): float * * @throws \Carbon\Exceptions\InvalidFormatException */ - public function date(string $key, ?string $format = null, ?string $tz = null): ?Carbon + public function date(string $key, ?string $format = null, UnitEnum|string|null $tz = null): ?Carbon { + $tz = enum_value($tz); + if ($this->isNotFilled($key)) { return null; } diff --git a/tests/Core/Database/Query/BuilderTest.php b/tests/Core/Database/Query/BuilderTest.php new file mode 100644 index 000000000..9fee17618 --- /dev/null +++ b/tests/Core/Database/Query/BuilderTest.php @@ -0,0 +1,108 @@ +getBuilder(); + + $result = $builder->castBinding(BuilderTestStringEnum::Active); + + $this->assertSame('active', $result); + } + + public function testCastBindingWithIntBackedEnum(): void + { + $builder = $this->getBuilder(); + + $result = $builder->castBinding(BuilderTestIntEnum::Two); + + $this->assertSame(2, $result); + } + + public function testCastBindingWithUnitEnum(): void + { + $builder = $this->getBuilder(); + + $result = $builder->castBinding(BuilderTestUnitEnum::Published); + + // UnitEnum uses ->name via enum_value() + $this->assertSame('Published', $result); + } + + public function testCastBindingWithString(): void + { + $builder = $this->getBuilder(); + + $result = $builder->castBinding('test'); + + $this->assertSame('test', $result); + } + + public function testCastBindingWithInt(): void + { + $builder = $this->getBuilder(); + + $result = $builder->castBinding(42); + + $this->assertSame(42, $result); + } + + public function testCastBindingWithNull(): void + { + $builder = $this->getBuilder(); + + $result = $builder->castBinding(null); + + $this->assertNull($result); + } + + protected function getBuilder(): Builder + { + $grammar = m::mock(\Hyperf\Database\Query\Grammars\Grammar::class); + $processor = m::mock(\Hyperf\Database\Query\Processors\Processor::class); + $connection = m::mock(\Hyperf\Database\ConnectionInterface::class); + + $connection->shouldReceive('getQueryGrammar')->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + + return new Builder($connection); + } +} diff --git a/tests/Support/Traits/InteractsWithDataTest.php b/tests/Support/Traits/InteractsWithDataTest.php new file mode 100644 index 000000000..d40616315 --- /dev/null +++ b/tests/Support/Traits/InteractsWithDataTest.php @@ -0,0 +1,164 @@ +getApplication()); + Date::clearResolvedInstances(); + } + + protected function tearDown(): void + { + Date::clearResolvedInstances(); + + parent::tearDown(); + } + + public function testDateReturnsNullWhenKeyIsNotFilled(): void + { + $instance = new TestInteractsWithDataClass(['date' => '']); + + $this->assertNull($instance->date('date')); + } + + public function testDateParsesWithoutFormat(): void + { + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + $result = $instance->date('date'); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('2024-01-15 10:30:00', $result->format('Y-m-d H:i:s')); + } + + public function testDateParsesWithFormat(): void + { + $instance = new TestInteractsWithDataClass(['date' => '15/01/2024']); + + $result = $instance->date('date', 'd/m/Y'); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('2024-01-15', $result->format('Y-m-d')); + } + + public function testDateWithStringTimezone(): void + { + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + $result = $instance->date('date', null, 'America/New_York'); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testDateWithStringBackedEnumTimezone(): void + { + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + $result = $instance->date('date', null, InteractsWithDataTestStringEnum::NewYork); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testDateWithUnitEnumTimezone(): void + { + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + // UnitEnum uses ->name, so 'UTC' will be the timezone + $result = $instance->date('date', null, InteractsWithDataTestUnitEnum::UTC); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('UTC', $result->timezone->getName()); + } + + public function testDateWithIntBackedEnumTimezoneUsesEnumValue(): void + { + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + // Int-backed enum will return int (1), which Carbon interprets as a UTC offset + // This tests that enum_value() is called and passes the value to Carbon + $result = $instance->date('date', null, InteractsWithDataTestIntEnum::One); + + $this->assertInstanceOf(Carbon::class, $result); + // Carbon interprets int as UTC offset, so timezone offset will be +01:00 + $this->assertEquals('+01:00', $result->timezone->getName()); + } + + public function testDateWithNullTimezone(): void + { + $instance = new TestInteractsWithDataClass(['date' => '2024-01-15 10:30:00']); + + $result = $instance->date('date', null, null); + + $this->assertInstanceOf(Carbon::class, $result); + } +} + +class TestInteractsWithDataClass +{ + use InteractsWithData; + + public function __construct( + protected array $data = [] + ) { + } + + public function all(mixed $keys = null): array + { + return $this->data; + } + + protected function data(?string $key = null, mixed $default = null): mixed + { + if (is_null($key)) { + return $this->data; + } + + return $this->data[$key] ?? $default; + } + + public function collect(array|string|null $key = null): Collection + { + return new Collection(is_array($key) ? $this->only($key) : $this->data($key)); + } +} From ad15a58c807e38a96bdbc646b51cab29235a27dc Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 06:55:21 +0000 Subject: [PATCH 15/19] Add enum support to now() and today() helpers Both helpers now accept UnitEnum for timezone parameter and use enum_value() for consistent enum handling. --- src/foundation/src/helpers.php | 11 ++- tests/Foundation/HelpersTest.php | 148 +++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 tests/Foundation/HelpersTest.php diff --git a/src/foundation/src/helpers.php b/src/foundation/src/helpers.php index bf92860ea..da526a6d4 100644 --- a/src/foundation/src/helpers.php +++ b/src/foundation/src/helpers.php @@ -37,6 +37,7 @@ use Psr\Log\LoggerInterface; use function Hypervel\Filesystem\join_paths; +use function Hypervel\Support\enum_value; if (! function_exists('abort')) { /** @@ -488,9 +489,9 @@ function mix(string $path, string $manifestDirectory = ''): HtmlString|string /** * Create a new Carbon instance for the current time. */ - function now(\DateTimeZone|string|null $tz = null): Carbon + function now(\UnitEnum|\DateTimeZone|string|null $tz = null): Carbon { - return Carbon::now($tz); + return Carbon::now(enum_value($tz)); } } @@ -650,12 +651,10 @@ function session(array|string|null $key = null, mixed $default = null): mixed if (! function_exists('today')) { /** * Create a new Carbon instance for the current date. - * - * @param null|\DateTimeZone|string $tz */ - function today($tz = null): Carbon + function today(\UnitEnum|\DateTimeZone|string|null $tz = null): Carbon { - return Carbon::today($tz); + return Carbon::today(enum_value($tz)); } } diff --git a/tests/Foundation/HelpersTest.php b/tests/Foundation/HelpersTest.php new file mode 100644 index 000000000..20bf5a2d0 --- /dev/null +++ b/tests/Foundation/HelpersTest.php @@ -0,0 +1,148 @@ +assertInstanceOf(Carbon::class, $result); + } + + public function testNowWithStringTimezone(): void + { + $result = now('America/New_York'); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testNowWithDateTimeZone(): void + { + $tz = new DateTimeZone('America/New_York'); + $result = now($tz); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testNowWithStringBackedEnum(): void + { + $result = now(HelpersTestStringEnum::NewYork); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testNowWithUnitEnum(): void + { + $result = now(HelpersTestUnitEnum::UTC); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('UTC', $result->timezone->getName()); + } + + public function testNowWithIntBackedEnum(): void + { + // Int-backed enum returns int, Carbon interprets as UTC offset + $result = now(HelpersTestIntEnum::One); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('+01:00', $result->timezone->getName()); + } + + public function testNowWithNull(): void + { + $result = now(null); + + $this->assertInstanceOf(Carbon::class, $result); + } + + public function testTodayReturnsCarbon(): void + { + $result = today(); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('00:00:00', $result->format('H:i:s')); + } + + public function testTodayWithStringTimezone(): void + { + $result = today('America/New_York'); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + $this->assertEquals('00:00:00', $result->format('H:i:s')); + } + + public function testTodayWithDateTimeZone(): void + { + $tz = new DateTimeZone('America/New_York'); + $result = today($tz); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testTodayWithStringBackedEnum(): void + { + $result = today(HelpersTestStringEnum::NewYork); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('America/New_York', $result->timezone->getName()); + } + + public function testTodayWithUnitEnum(): void + { + $result = today(HelpersTestUnitEnum::UTC); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('UTC', $result->timezone->getName()); + } + + public function testTodayWithIntBackedEnum(): void + { + // Int-backed enum returns int, Carbon interprets as UTC offset + $result = today(HelpersTestIntEnum::One); + + $this->assertInstanceOf(Carbon::class, $result); + $this->assertEquals('+01:00', $result->timezone->getName()); + } + + public function testTodayWithNull(): void + { + $result = today(null); + + $this->assertInstanceOf(Carbon::class, $result); + } +} From 96068121ed5aca0a5c28017d7a08031c1789e579 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:50:58 +0000 Subject: [PATCH 16/19] Remove (string) casts from enum_value() calls Let int-backed enums fail with TypeError at typed boundaries instead of silently converting to strings like "1". This matches Laravel's behavior where enum_value() returns the raw value and type mismatches fail naturally. Also adds enum tests for TaggedCache, RedisTaggedCache, and RateLimited. --- src/auth/src/Access/Gate.php | 2 +- .../src/InteractsWithBroadcasting.php | 2 +- src/bus/src/PendingBatch.php | 8 +- src/bus/src/PendingChain.php | 8 +- src/bus/src/PendingDispatch.php | 10 +- src/bus/src/Queueable.php | 8 +- src/cache/src/RateLimiter.php | 2 +- src/cache/src/RedisTaggedCache.php | 10 +- src/cache/src/Repository.php | 14 +- src/cache/src/TaggedCache.php | 4 +- .../src/Scheduling/ManagesFrequencies.php | 2 +- src/console/src/Scheduling/Schedule.php | 6 +- src/core/src/Context/Context.php | 12 +- .../Database/Eloquent/Factories/Factory.php | 2 +- src/core/src/Database/Eloquent/Model.php | 2 +- .../Eloquent/Relations/MorphPivot.php | 2 +- .../src/Database/Eloquent/Relations/Pivot.php | 2 +- src/event/src/EventDispatcher.php | 2 +- src/event/src/QueuedClosure.php | 6 +- src/filesystem/src/FilesystemManager.php | 2 +- src/queue/src/Middleware/RateLimited.php | 2 +- src/redis/src/Redis.php | 2 +- src/session/src/Store.php | 4 +- tests/Auth/Access/GateEnumTest.php | 128 ++++++++---------- .../Middleware/AuthorizeMiddlewareTest.php | 90 ++++++++++++ .../InteractsWithBroadcastingTest.php | 39 +++++- tests/Broadcasting/PendingBroadcastTest.php | 30 ++-- tests/Bus/QueueableTest.php | 34 ++++- tests/Cache/CacheRedisTaggedCacheTest.php | 44 ++++++ tests/Cache/CacheRepositoryEnumTest.php | 108 +++++++-------- tests/Cache/CacheTaggedCacheTest.php | 88 ++++++++++++ tests/Cache/RateLimiterEnumTest.php | 15 ++ tests/Console/Scheduling/EventTest.php | 7 +- tests/Console/Scheduling/ScheduleTest.php | 14 +- tests/Core/ContextEnumTest.php | 9 +- .../Eloquent/Factories/FactoryTest.php | 6 +- .../Core/Database/Eloquent/ModelEnumTest.php | 7 +- .../Eloquent/Relations/MorphPivotEnumTest.php | 7 +- .../Eloquent/Relations/PivotEnumTest.php | 7 +- tests/Event/QueuedClosureTest.php | 18 +-- tests/Event/QueuedEventsTest.php | 7 +- tests/Filesystem/FilesystemManagerTest.php | 11 +- tests/Queue/RateLimitedTest.php | 110 +++++++++++++++ tests/Redis/RedisTest.php | 15 +- 44 files changed, 653 insertions(+), 255 deletions(-) create mode 100644 tests/Auth/Middleware/AuthorizeMiddlewareTest.php create mode 100644 tests/Queue/RateLimitedTest.php diff --git a/src/auth/src/Access/Gate.php b/src/auth/src/Access/Gate.php index eba440f8b..e8741f0c9 100644 --- a/src/auth/src/Access/Gate.php +++ b/src/auth/src/Access/Gate.php @@ -287,7 +287,7 @@ public function authorize(UnitEnum|string $ability, mixed $arguments = []): Resp public function inspect(UnitEnum|string $ability, mixed $arguments = []): Response { try { - $result = $this->raw((string) enum_value($ability), $arguments); + $result = $this->raw(enum_value($ability), $arguments); if ($result instanceof Response) { return $result; diff --git a/src/broadcasting/src/InteractsWithBroadcasting.php b/src/broadcasting/src/InteractsWithBroadcasting.php index dc9b32f24..783149f8b 100644 --- a/src/broadcasting/src/InteractsWithBroadcasting.php +++ b/src/broadcasting/src/InteractsWithBroadcasting.php @@ -21,7 +21,7 @@ trait InteractsWithBroadcasting */ public function broadcastVia(UnitEnum|array|string|null $connection = null): static { - $connection = is_null($connection) ? null : (string) enum_value($connection); + $connection = is_null($connection) ? null : enum_value($connection); $this->broadcastConnection = is_null($connection) ? [null] diff --git a/src/bus/src/PendingBatch.php b/src/bus/src/PendingBatch.php index 00fdfb01f..d166fffa7 100644 --- a/src/bus/src/PendingBatch.php +++ b/src/bus/src/PendingBatch.php @@ -4,9 +4,9 @@ namespace Hypervel\Bus; -use BackedEnum; use Closure; use Hyperf\Collection\Arr; +use UnitEnum; use Hyperf\Collection\Collection; use Hyperf\Conditionable\Conditionable; use Hyperf\Coroutine\Coroutine; @@ -212,11 +212,9 @@ public function connection(): ?string /** * Specify the queue that the batched jobs should run on. */ - public function onQueue(BackedEnum|string|null $queue): static + public function onQueue(UnitEnum|string|null $queue): static { - $value = enum_value($queue); - - $this->options['queue'] = is_null($value) ? null : (string) $value; + $this->options['queue'] = enum_value($queue); return $this; } diff --git a/src/bus/src/PendingChain.php b/src/bus/src/PendingChain.php index 463147a26..12ec630f6 100644 --- a/src/bus/src/PendingChain.php +++ b/src/bus/src/PendingChain.php @@ -4,10 +4,10 @@ namespace Hypervel\Bus; -use BackedEnum; use Closure; use DateInterval; use DateTimeInterface; +use UnitEnum; use Hyperf\Conditionable\Conditionable; use Hyperf\Context\ApplicationContext; use Hypervel\Bus\Contracts\Dispatcher; @@ -66,11 +66,9 @@ public function onConnection(?string $connection): static /** * Set the desired queue for the job. */ - public function onQueue(BackedEnum|string|null $queue): static + public function onQueue(UnitEnum|string|null $queue): static { - $value = enum_value($queue); - - $this->queue = is_null($value) ? null : (string) $value; + $this->queue = enum_value($queue); return $this; } diff --git a/src/bus/src/PendingDispatch.php b/src/bus/src/PendingDispatch.php index b7318bc80..4d070c32c 100644 --- a/src/bus/src/PendingDispatch.php +++ b/src/bus/src/PendingDispatch.php @@ -4,10 +4,10 @@ namespace Hypervel\Bus; -use BackedEnum; use DateInterval; use DateTimeInterface; use Hyperf\Context\ApplicationContext; +use UnitEnum; use Hypervel\Bus\Contracts\Dispatcher; use Hypervel\Cache\Contracts\Factory as CacheFactory; use Hypervel\Queue\Contracts\ShouldBeUnique; @@ -30,7 +30,7 @@ public function __construct( /** * Set the desired connection for the job. */ - public function onConnection(BackedEnum|string|null $connection): static + public function onConnection(UnitEnum|string|null $connection): static { $this->job->onConnection($connection); @@ -40,7 +40,7 @@ public function onConnection(BackedEnum|string|null $connection): static /** * Set the desired queue for the job. */ - public function onQueue(BackedEnum|string|null $queue): static + public function onQueue(UnitEnum|string|null $queue): static { $this->job->onQueue($queue); @@ -50,7 +50,7 @@ public function onQueue(BackedEnum|string|null $queue): static /** * Set the desired connection for the chain. */ - public function allOnConnection(BackedEnum|string|null $connection): static + public function allOnConnection(UnitEnum|string|null $connection): static { $this->job->allOnConnection($connection); @@ -60,7 +60,7 @@ public function allOnConnection(BackedEnum|string|null $connection): static /** * Set the desired queue for the chain. */ - public function allOnQueue(BackedEnum|string|null $queue): static + public function allOnQueue(UnitEnum|string|null $queue): static { $this->job->allOnQueue($queue); diff --git a/src/bus/src/Queueable.php b/src/bus/src/Queueable.php index 56560506b..a27e9fed5 100644 --- a/src/bus/src/Queueable.php +++ b/src/bus/src/Queueable.php @@ -71,7 +71,7 @@ public function onConnection(UnitEnum|string|null $connection): static { $value = enum_value($connection); - $this->connection = is_null($value) ? null : (string) $value; + $this->connection = is_null($value) ? null : $value; return $this; } @@ -83,7 +83,7 @@ public function onQueue(UnitEnum|string|null $queue): static { $value = enum_value($queue); - $this->queue = is_null($value) ? null : (string) $value; + $this->queue = is_null($value) ? null : $value; return $this; } @@ -95,7 +95,7 @@ public function allOnConnection(UnitEnum|string|null $connection): static { $value = enum_value($connection); - $resolvedConnection = is_null($value) ? null : (string) $value; + $resolvedConnection = is_null($value) ? null : $value; $this->chainConnection = $resolvedConnection; $this->connection = $resolvedConnection; @@ -110,7 +110,7 @@ public function allOnQueue(UnitEnum|string|null $queue): static { $value = enum_value($queue); - $resolvedQueue = is_null($value) ? null : (string) $value; + $resolvedQueue = is_null($value) ? null : $value; $this->chainQueue = $resolvedQueue; $this->queue = $resolvedQueue; diff --git a/src/cache/src/RateLimiter.php b/src/cache/src/RateLimiter.php index 2aa69890d..1f66940ab 100644 --- a/src/cache/src/RateLimiter.php +++ b/src/cache/src/RateLimiter.php @@ -56,7 +56,7 @@ public function limiter(UnitEnum|string $name): ?Closure */ private function resolveLimiterName(UnitEnum|string $name): string { - return (string) enum_value($name); + return enum_value($name); } /** diff --git a/src/cache/src/RedisTaggedCache.php b/src/cache/src/RedisTaggedCache.php index d0df9fd66..89cdcc311 100644 --- a/src/cache/src/RedisTaggedCache.php +++ b/src/cache/src/RedisTaggedCache.php @@ -32,7 +32,7 @@ class RedisTaggedCache extends TaggedCache */ public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { - $key = (string) enum_value($key); + $key = enum_value($key); $this->tags->addEntry( $this->itemKey($key), @@ -51,7 +51,7 @@ public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateT return $this->putMany($key, $value); } - $key = (string) enum_value($key); + $key = enum_value($key); if (is_null($ttl)) { return $this->forever($key, $value); @@ -70,7 +70,7 @@ public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateT */ public function increment(UnitEnum|string $key, int $value = 1): bool|int { - $key = (string) enum_value($key); + $key = enum_value($key); $this->tags->addEntry($this->itemKey($key), updateWhen: 'NX'); @@ -82,7 +82,7 @@ public function increment(UnitEnum|string $key, int $value = 1): bool|int */ public function decrement(UnitEnum|string $key, int $value = 1): bool|int { - $key = (string) enum_value($key); + $key = enum_value($key); $this->tags->addEntry($this->itemKey($key), updateWhen: 'NX'); @@ -94,7 +94,7 @@ public function decrement(UnitEnum|string $key, int $value = 1): bool|int */ public function forever(UnitEnum|string $key, mixed $value): bool { - $key = (string) enum_value($key); + $key = enum_value($key); $this->tags->addEntry($this->itemKey($key)); diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 4a658fe98..3a469848d 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -123,7 +123,7 @@ public function get(array|UnitEnum|string $key, mixed $default = null): mixed return $this->many($key); } - $key = (string) enum_value($key); + $key = enum_value($key); $this->event(new RetrievingKey($this->getName(), $key)); @@ -198,7 +198,7 @@ public function put(array|UnitEnum|string $key, mixed $value, DateInterval|DateT return $this->putMany($key, $value); } - $key = (string) enum_value($key); + $key = enum_value($key); if ($ttl === null) { return $this->forever($key, $value); @@ -270,7 +270,7 @@ public function setMultiple(iterable $values, DateInterval|DateTimeInterface|int */ public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { - $key = (string) enum_value($key); + $key = enum_value($key); $seconds = null; @@ -308,7 +308,7 @@ public function add(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInt */ public function increment(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->increment((string) enum_value($key), $value); + return $this->store->increment(enum_value($key), $value); } /** @@ -316,7 +316,7 @@ public function increment(UnitEnum|string $key, int $value = 1): bool|int */ public function decrement(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->decrement((string) enum_value($key), $value); + return $this->store->decrement(enum_value($key), $value); } /** @@ -324,7 +324,7 @@ public function decrement(UnitEnum|string $key, int $value = 1): bool|int */ public function forever(UnitEnum|string $key, mixed $value): bool { - $key = (string) enum_value($key); + $key = enum_value($key); $this->event(new WritingKey($this->getName(), $key, $value)); @@ -408,7 +408,7 @@ public function rememberForever(UnitEnum|string $key, Closure $callback): mixed */ public function forget(UnitEnum|string $key): bool { - $key = (string) enum_value($key); + $key = enum_value($key); $this->event(new ForgettingKey($this->getName(), $key)); diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php index ce684bd9f..a273136ef 100644 --- a/src/cache/src/TaggedCache.php +++ b/src/cache/src/TaggedCache.php @@ -51,7 +51,7 @@ public function putMany(array $values, DateInterval|DateTimeInterface|int|null $ */ public function increment(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->increment($this->itemKey((string) enum_value($key)), $value); + return $this->store->increment($this->itemKey(enum_value($key)), $value); } /** @@ -59,7 +59,7 @@ public function increment(UnitEnum|string $key, int $value = 1): bool|int */ public function decrement(UnitEnum|string $key, int $value = 1): bool|int { - return $this->store->decrement($this->itemKey((string) enum_value($key)), $value); + return $this->store->decrement($this->itemKey(enum_value($key)), $value); } /** diff --git a/src/console/src/Scheduling/ManagesFrequencies.php b/src/console/src/Scheduling/ManagesFrequencies.php index 56d923052..6f70ca29b 100644 --- a/src/console/src/Scheduling/ManagesFrequencies.php +++ b/src/console/src/Scheduling/ManagesFrequencies.php @@ -531,7 +531,7 @@ public function days(mixed $days): static public function timezone(DateTimeZone|UnitEnum|string $timezone): static { $this->timezone = $timezone instanceof UnitEnum - ? (string) enum_value($timezone) + ? enum_value($timezone) : $timezone; return $this; diff --git a/src/console/src/Scheduling/Schedule.php b/src/console/src/Scheduling/Schedule.php index 19c65fe7d..f79b428dd 100644 --- a/src/console/src/Scheduling/Schedule.php +++ b/src/console/src/Scheduling/Schedule.php @@ -165,8 +165,8 @@ public function job( ): CallbackEvent { $jobName = $job; - $queue = is_null($queue) ? null : (string) enum_value($queue); - $connection = is_null($connection) ? null : (string) enum_value($connection); + $queue = is_null($queue) ? null : enum_value($queue); + $connection = is_null($connection) ? null : enum_value($connection); if (! is_string($job)) { $jobName = method_exists($job, 'displayName') @@ -369,7 +369,7 @@ public function useCache(UnitEnum|string|null $store): static return $this; } - $store = (string) enum_value($store); + $store = enum_value($store); if ($this->eventMutex instanceof CacheAware) { $this->eventMutex->useStore($store); diff --git a/src/core/src/Context/Context.php b/src/core/src/Context/Context.php index 9b3c67257..07d0331cf 100644 --- a/src/core/src/Context/Context.php +++ b/src/core/src/Context/Context.php @@ -25,7 +25,7 @@ public function __call(string $method, array $arguments): mixed */ public static function set(UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed { - return parent::set((string) enum_value($id), $value, $coroutineId); + return parent::set(enum_value($id), $value, $coroutineId); } /** @@ -33,7 +33,7 @@ public static function set(UnitEnum|string $id, mixed $value, ?int $coroutineId */ public static function get(UnitEnum|string $id, mixed $default = null, ?int $coroutineId = null): mixed { - return parent::get((string) enum_value($id), $default, $coroutineId); + return parent::get(enum_value($id), $default, $coroutineId); } /** @@ -41,7 +41,7 @@ public static function get(UnitEnum|string $id, mixed $default = null, ?int $cor */ public static function has(UnitEnum|string $id, ?int $coroutineId = null): bool { - return parent::has((string) enum_value($id), $coroutineId); + return parent::has(enum_value($id), $coroutineId); } /** @@ -49,7 +49,7 @@ public static function has(UnitEnum|string $id, ?int $coroutineId = null): bool */ public static function destroy(UnitEnum|string $id, ?int $coroutineId = null): void { - parent::destroy((string) enum_value($id), $coroutineId); + parent::destroy(enum_value($id), $coroutineId); } /** @@ -57,7 +57,7 @@ public static function destroy(UnitEnum|string $id, ?int $coroutineId = null): v */ public static function override(UnitEnum|string $id, Closure $closure, ?int $coroutineId = null): mixed { - return parent::override((string) enum_value($id), $closure, $coroutineId); + return parent::override(enum_value($id), $closure, $coroutineId); } /** @@ -65,7 +65,7 @@ public static function override(UnitEnum|string $id, Closure $closure, ?int $cor */ public static function getOrSet(UnitEnum|string $id, mixed $value, ?int $coroutineId = null): mixed { - return parent::getOrSet((string) enum_value($id), $value, $coroutineId); + return parent::getOrSet(enum_value($id), $value, $coroutineId); } /** diff --git a/src/core/src/Database/Eloquent/Factories/Factory.php b/src/core/src/Database/Eloquent/Factories/Factory.php index ae00b9731..119d8735b 100644 --- a/src/core/src/Database/Eloquent/Factories/Factory.php +++ b/src/core/src/Database/Eloquent/Factories/Factory.php @@ -669,7 +669,7 @@ public function getConnectionName(): ?string { $value = enum_value($this->connection); - return is_null($value) ? null : (string) $value; + return is_null($value) ? null : $value; } /** diff --git a/src/core/src/Database/Eloquent/Model.php b/src/core/src/Database/Eloquent/Model.php index 614d9acbe..40244d4bc 100644 --- a/src/core/src/Database/Eloquent/Model.php +++ b/src/core/src/Database/Eloquent/Model.php @@ -110,7 +110,7 @@ public function setConnection($name): static { $value = enum_value($name); - $this->connection = is_null($value) ? null : (string) $value; + $this->connection = is_null($value) ? null : $value; return $this; } diff --git a/src/core/src/Database/Eloquent/Relations/MorphPivot.php b/src/core/src/Database/Eloquent/Relations/MorphPivot.php index 735d0c68d..122fde24d 100644 --- a/src/core/src/Database/Eloquent/Relations/MorphPivot.php +++ b/src/core/src/Database/Eloquent/Relations/MorphPivot.php @@ -32,7 +32,7 @@ public function setConnection($name): static { $value = enum_value($name); - $this->connection = is_null($value) ? null : (string) $value; + $this->connection = is_null($value) ? null : $value; return $this; } diff --git a/src/core/src/Database/Eloquent/Relations/Pivot.php b/src/core/src/Database/Eloquent/Relations/Pivot.php index 8cb0bc541..533087f2f 100644 --- a/src/core/src/Database/Eloquent/Relations/Pivot.php +++ b/src/core/src/Database/Eloquent/Relations/Pivot.php @@ -32,7 +32,7 @@ public function setConnection($name): static { $value = enum_value($name); - $this->connection = is_null($value) ? null : (string) $value; + $this->connection = is_null($value) ? null : $value; return $this; } diff --git a/src/event/src/EventDispatcher.php b/src/event/src/EventDispatcher.php index 592a22c6c..d6be8df42 100644 --- a/src/event/src/EventDispatcher.php +++ b/src/event/src/EventDispatcher.php @@ -470,7 +470,7 @@ protected function queueHandler(object|string $class, string $method, array $arg ? (isset($arguments[1]) ? $listener->viaQueue($arguments[1]) : $listener->viaQueue()) : $listener->queue ?? null; - $queue = is_null($queue) ? null : (string) enum_value($queue); + $queue = is_null($queue) ? null : enum_value($queue); $delay = method_exists($listener, 'withDelay') ? (isset($arguments[1]) ? $listener->withDelay($arguments[1]) : $listener->withDelay()) diff --git a/src/event/src/QueuedClosure.php b/src/event/src/QueuedClosure.php index 02738855e..b228622df 100644 --- a/src/event/src/QueuedClosure.php +++ b/src/event/src/QueuedClosure.php @@ -55,7 +55,7 @@ public function __construct(public Closure $closure) */ public function onConnection(UnitEnum|string|null $connection): static { - $this->connection = is_null($connection) ? null : (string) enum_value($connection); + $this->connection = is_null($connection) ? null : enum_value($connection); return $this; } @@ -65,7 +65,7 @@ public function onConnection(UnitEnum|string|null $connection): static */ public function onQueue(UnitEnum|string|null $queue): static { - $this->queue = is_null($queue) ? null : (string) enum_value($queue); + $this->queue = is_null($queue) ? null : enum_value($queue); return $this; } @@ -77,7 +77,7 @@ public function onQueue(UnitEnum|string|null $queue): static */ public function onGroup(UnitEnum|string $group): static { - $this->messageGroup = (string) enum_value($group); + $this->messageGroup = enum_value($group); return $this; } diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php index 28bce7d71..9109b214e 100644 --- a/src/filesystem/src/FilesystemManager.php +++ b/src/filesystem/src/FilesystemManager.php @@ -84,7 +84,7 @@ public function drive(UnitEnum|string|null $name = null): Filesystem */ public function disk(UnitEnum|string|null $name = null): FileSystem { - $name = (string) enum_value($name) ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->disks[$name] = $this->get($name); } diff --git a/src/queue/src/Middleware/RateLimited.php b/src/queue/src/Middleware/RateLimited.php index b311da357..b5c2d1acc 100644 --- a/src/queue/src/Middleware/RateLimited.php +++ b/src/queue/src/Middleware/RateLimited.php @@ -38,7 +38,7 @@ public function __construct(UnitEnum|string $limiterName) $this->limiter = ApplicationContext::getContainer() ->get(RateLimiter::class); - $this->limiterName = (string) enum_value($limiterName); + $this->limiterName = enum_value($limiterName); } /** diff --git a/src/redis/src/Redis.php b/src/redis/src/Redis.php index 8bc1f4fe1..be6ccc695 100644 --- a/src/redis/src/Redis.php +++ b/src/redis/src/Redis.php @@ -140,6 +140,6 @@ public function connection(UnitEnum|string $name = 'default'): RedisProxy { return ApplicationContext::getContainer() ->get(RedisFactory::class) - ->get((string) enum_value($name)); + ->get(enum_value($name)); } } diff --git a/src/session/src/Store.php b/src/session/src/Store.php index dafca9c1d..13c668ed4 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -270,7 +270,7 @@ public function get(UnitEnum|string $key, mixed $default = null): mixed public function pull(UnitEnum|string $key, mixed $default = null): mixed { $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, (string) enum_value($key), $default); + $result = Arr::pull($attributes, enum_value($key), $default); $this->setAttributes($attributes); @@ -444,7 +444,7 @@ public function flashInput(array $value): void public function remove(UnitEnum|string $key): mixed { $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, (string) enum_value($key)); + $result = Arr::pull($attributes, enum_value($key)); $this->setAttributes($attributes); diff --git a/tests/Auth/Access/GateEnumTest.php b/tests/Auth/Access/GateEnumTest.php index 516439315..127e25db5 100644 --- a/tests/Auth/Access/GateEnumTest.php +++ b/tests/Auth/Access/GateEnumTest.php @@ -7,27 +7,27 @@ use Hyperf\Di\Container; use Hyperf\Di\Definition\DefinitionSource; use Hypervel\Auth\Access\Gate; -use Hypervel\Auth\Middleware\Authorize; use Hypervel\Tests\Auth\Stub\AccessGateTestAuthenticatable; use Hypervel\Tests\Auth\Stub\AccessGateTestDummy; use Hypervel\Tests\Auth\Stub\AccessGateTestPolicyWithAllPermissions; use Hypervel\Tests\Auth\Stub\AccessGateTestPolicyWithNoPermissions; use Hypervel\Tests\TestCase; +use TypeError; -enum AbilitiesBackedEnum: string +enum GateEnumTestAbilitiesBackedEnum: string { case ViewDashboard = 'view-dashboard'; case Update = 'update'; case Edit = 'edit'; } -enum AbilitiesIntBackedEnum: int +enum GateEnumTestAbilitiesIntBackedEnum: int { case CreatePost = 1; case DeletePost = 2; } -enum AbilitiesUnitEnum +enum GateEnumTestAbilitiesUnitEnum { case ManageUsers; case ViewReports; @@ -47,7 +47,7 @@ public function testDefineWithBackedEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); // Can check with string (the enum value) $this->assertTrue($gate->allows('view-dashboard')); @@ -57,21 +57,31 @@ public function testDefineWithUnitEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); // UnitEnum uses ->name, so key is 'ManageUsers' $this->assertTrue($gate->allows('ManageUsers')); } - public function testDefineWithIntBackedEnum(): void + public function testDefineWithIntBackedEnumStoresUnderIntKey(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesIntBackedEnum::CreatePost, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesIntBackedEnum::CreatePost, fn ($user) => true); - // Int value 1 should be cast to string '1' + // Int value 1 is used as ability key - can check with string '1' $this->assertTrue($gate->allows('1')); - $this->assertTrue($gate->allows(AbilitiesIntBackedEnum::CreatePost)); + } + + public function testAllowsWithIntBackedEnumThrowsTypeError(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesIntBackedEnum::CreatePost, fn ($user) => true); + + // Int-backed enum causes TypeError because raw() expects string + $this->expectException(TypeError::class); + $gate->allows(GateEnumTestAbilitiesIntBackedEnum::CreatePost); } // ========================================================================= @@ -82,18 +92,18 @@ public function testAllowsWithBackedEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); - $this->assertTrue($gate->allows(AbilitiesBackedEnum::ViewDashboard)); + $this->assertTrue($gate->allows(GateEnumTestAbilitiesBackedEnum::ViewDashboard)); } public function testAllowsWithUnitEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); - $this->assertTrue($gate->allows(AbilitiesUnitEnum::ManageUsers)); + $this->assertTrue($gate->allows(GateEnumTestAbilitiesUnitEnum::ManageUsers)); } // ========================================================================= @@ -104,18 +114,18 @@ public function testDeniesWithBackedEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => false); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => false); - $this->assertTrue($gate->denies(AbilitiesBackedEnum::ViewDashboard)); + $this->assertTrue($gate->denies(GateEnumTestAbilitiesBackedEnum::ViewDashboard)); } public function testDeniesWithUnitEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => false); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => false); - $this->assertTrue($gate->denies(AbilitiesUnitEnum::ManageUsers)); + $this->assertTrue($gate->denies(GateEnumTestAbilitiesUnitEnum::ManageUsers)); } // ========================================================================= @@ -128,9 +138,9 @@ public function testCheckWithArrayContainingBackedEnum(): void $gate->define('allow_1', fn ($user) => true); $gate->define('allow_2', fn ($user) => true); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); - $this->assertTrue($gate->check(['allow_1', 'allow_2', AbilitiesBackedEnum::ViewDashboard])); + $this->assertTrue($gate->check(['allow_1', 'allow_2', GateEnumTestAbilitiesBackedEnum::ViewDashboard])); } public function testCheckWithArrayContainingUnitEnum(): void @@ -138,9 +148,9 @@ public function testCheckWithArrayContainingUnitEnum(): void $gate = $this->getBasicGate(); $gate->define('allow_1', fn ($user) => true); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); - $this->assertTrue($gate->check(['allow_1', AbilitiesUnitEnum::ManageUsers])); + $this->assertTrue($gate->check(['allow_1', GateEnumTestAbilitiesUnitEnum::ManageUsers])); } // ========================================================================= @@ -153,7 +163,7 @@ public function testAnyWithBackedEnum(): void $gate->policy(AccessGateTestDummy::class, AccessGateTestPolicyWithAllPermissions::class); - $this->assertTrue($gate->any(['edit', AbilitiesBackedEnum::Update], new AccessGateTestDummy())); + $this->assertTrue($gate->any(['edit', GateEnumTestAbilitiesBackedEnum::Update], new AccessGateTestDummy())); } public function testAnyWithUnitEnum(): void @@ -161,9 +171,9 @@ public function testAnyWithUnitEnum(): void $gate = $this->getBasicGate(); $gate->define('deny', fn ($user) => false); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); - $this->assertTrue($gate->any(['deny', AbilitiesUnitEnum::ManageUsers])); + $this->assertTrue($gate->any(['deny', GateEnumTestAbilitiesUnitEnum::ManageUsers])); } // ========================================================================= @@ -176,7 +186,7 @@ public function testNoneWithBackedEnum(): void $gate->policy(AccessGateTestDummy::class, AccessGateTestPolicyWithNoPermissions::class); - $this->assertTrue($gate->none(['edit', AbilitiesBackedEnum::Update], new AccessGateTestDummy())); + $this->assertTrue($gate->none(['edit', GateEnumTestAbilitiesBackedEnum::Update], new AccessGateTestDummy())); } public function testNoneWithUnitEnum(): void @@ -184,9 +194,9 @@ public function testNoneWithUnitEnum(): void $gate = $this->getBasicGate(); $gate->define('deny_1', fn ($user) => false); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => false); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => false); - $this->assertTrue($gate->none(['deny_1', AbilitiesUnitEnum::ManageUsers])); + $this->assertTrue($gate->none(['deny_1', GateEnumTestAbilitiesUnitEnum::ManageUsers])); } public function testNoneReturnsFalseWhenAnyAbilityAllows(): void @@ -207,31 +217,31 @@ public function testHasWithBackedEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); - $this->assertTrue($gate->has(AbilitiesBackedEnum::ViewDashboard)); - $this->assertFalse($gate->has(AbilitiesBackedEnum::Update)); + $this->assertTrue($gate->has(GateEnumTestAbilitiesBackedEnum::ViewDashboard)); + $this->assertFalse($gate->has(GateEnumTestAbilitiesBackedEnum::Update)); } public function testHasWithUnitEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); - $this->assertTrue($gate->has(AbilitiesUnitEnum::ManageUsers)); - $this->assertFalse($gate->has(AbilitiesUnitEnum::ViewReports)); + $this->assertTrue($gate->has(GateEnumTestAbilitiesUnitEnum::ManageUsers)); + $this->assertFalse($gate->has(GateEnumTestAbilitiesUnitEnum::ViewReports)); } public function testHasWithArrayContainingEnums(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); - $this->assertTrue($gate->has([AbilitiesBackedEnum::ViewDashboard, AbilitiesUnitEnum::ManageUsers])); - $this->assertFalse($gate->has([AbilitiesBackedEnum::ViewDashboard, AbilitiesBackedEnum::Update])); + $this->assertTrue($gate->has([GateEnumTestAbilitiesBackedEnum::ViewDashboard, GateEnumTestAbilitiesUnitEnum::ManageUsers])); + $this->assertFalse($gate->has([GateEnumTestAbilitiesBackedEnum::ViewDashboard, GateEnumTestAbilitiesBackedEnum::Update])); } // ========================================================================= @@ -242,9 +252,9 @@ public function testAuthorizeWithBackedEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); - $response = $gate->authorize(AbilitiesBackedEnum::ViewDashboard); + $response = $gate->authorize(GateEnumTestAbilitiesBackedEnum::ViewDashboard); $this->assertTrue($response->allowed()); } @@ -253,9 +263,9 @@ public function testAuthorizeWithUnitEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); - $response = $gate->authorize(AbilitiesUnitEnum::ManageUsers); + $response = $gate->authorize(GateEnumTestAbilitiesUnitEnum::ManageUsers); $this->assertTrue($response->allowed()); } @@ -268,9 +278,9 @@ public function testInspectWithBackedEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); - $response = $gate->inspect(AbilitiesBackedEnum::ViewDashboard); + $response = $gate->inspect(GateEnumTestAbilitiesBackedEnum::ViewDashboard); $this->assertTrue($response->allowed()); } @@ -279,9 +289,9 @@ public function testInspectWithUnitEnum(): void { $gate = $this->getBasicGate(); - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => false); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => false); - $response = $gate->inspect(AbilitiesUnitEnum::ManageUsers); + $response = $gate->inspect(GateEnumTestAbilitiesUnitEnum::ManageUsers); $this->assertFalse($response->allowed()); } @@ -295,7 +305,7 @@ public function testBackedEnumAndStringInteroperability(): void $gate = $this->getBasicGate(); // Define with enum - $gate->define(AbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); // Check with string (the enum value) $this->assertTrue($gate->allows('view-dashboard')); @@ -304,7 +314,7 @@ public function testBackedEnumAndStringInteroperability(): void $gate->define('update', fn ($user) => true); // Check with enum that has same value - $this->assertTrue($gate->allows(AbilitiesBackedEnum::Update)); + $this->assertTrue($gate->allows(GateEnumTestAbilitiesBackedEnum::Update)); } public function testUnitEnumAndStringInteroperability(): void @@ -312,7 +322,7 @@ public function testUnitEnumAndStringInteroperability(): void $gate = $this->getBasicGate(); // Define with enum - $gate->define(AbilitiesUnitEnum::ManageUsers, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); // Check with string (the enum name) $this->assertTrue($gate->allows('ManageUsers')); @@ -321,25 +331,7 @@ public function testUnitEnumAndStringInteroperability(): void $gate->define('ViewReports', fn ($user) => true); // Check with enum - $this->assertTrue($gate->allows(AbilitiesUnitEnum::ViewReports)); - } - - // ========================================================================= - // Authorize middleware - // ========================================================================= - - public function testAuthorizeMiddlewareUsingWithBackedEnum(): void - { - $result = Authorize::using(AbilitiesBackedEnum::ViewDashboard, 'App\Models\Post'); - - $this->assertSame(Authorize::class . ':view-dashboard,App\Models\Post', $result); - } - - public function testAuthorizeMiddlewareUsingWithUnitEnum(): void - { - $result = Authorize::using(AbilitiesUnitEnum::ManageUsers); - - $this->assertSame(Authorize::class . ':ManageUsers', $result); + $this->assertTrue($gate->allows(GateEnumTestAbilitiesUnitEnum::ViewReports)); } // ========================================================================= diff --git a/tests/Auth/Middleware/AuthorizeMiddlewareTest.php b/tests/Auth/Middleware/AuthorizeMiddlewareTest.php new file mode 100644 index 000000000..998b2a097 --- /dev/null +++ b/tests/Auth/Middleware/AuthorizeMiddlewareTest.php @@ -0,0 +1,90 @@ +assertSame(Authorize::class . ':view-dashboard', $result); + } + + public function testUsingWithStringAbilityAndModels(): void + { + $result = Authorize::using('update', 'App\Models\Post'); + + $this->assertSame(Authorize::class . ':update,App\Models\Post', $result); + } + + public function testUsingWithStringAbilityAndMultipleModels(): void + { + $result = Authorize::using('transfer', 'App\Models\Account', 'App\Models\User'); + + $this->assertSame(Authorize::class . ':transfer,App\Models\Account,App\Models\User', $result); + } + + public function testUsingWithBackedEnum(): void + { + $result = Authorize::using(AuthorizeMiddlewareTestBackedEnum::ViewDashboard); + + $this->assertSame(Authorize::class . ':view-dashboard', $result); + } + + public function testUsingWithBackedEnumAndModels(): void + { + $result = Authorize::using(AuthorizeMiddlewareTestBackedEnum::ManageUsers, 'App\Models\User'); + + $this->assertSame(Authorize::class . ':manage-users,App\Models\User', $result); + } + + public function testUsingWithUnitEnum(): void + { + $result = Authorize::using(AuthorizeMiddlewareTestUnitEnum::ManageUsers); + + $this->assertSame(Authorize::class . ':ManageUsers', $result); + } + + public function testUsingWithUnitEnumAndModels(): void + { + $result = Authorize::using(AuthorizeMiddlewareTestUnitEnum::ViewReports, 'App\Models\Report'); + + $this->assertSame(Authorize::class . ':ViewReports,App\Models\Report', $result); + } + + public function testUsingWithIntBackedEnum(): void + { + // Int-backed enum value (1) is used directly - caller should be aware this results in '1' as ability + $result = Authorize::using(AuthorizeMiddlewareTestIntBackedEnum::CreatePost); + + $this->assertSame(Authorize::class . ':1', $result); + } +} diff --git a/tests/Broadcasting/InteractsWithBroadcastingTest.php b/tests/Broadcasting/InteractsWithBroadcastingTest.php index 1396fa2b8..3e62ee2fc 100644 --- a/tests/Broadcasting/InteractsWithBroadcastingTest.php +++ b/tests/Broadcasting/InteractsWithBroadcastingTest.php @@ -4,7 +4,11 @@ namespace Hypervel\Tests\Broadcasting; +use Hypervel\Broadcasting\BroadcastEvent; +use Hypervel\Broadcasting\Channel; +use Hypervel\Broadcasting\Contracts\Factory as BroadcastingFactory; use Hypervel\Broadcasting\InteractsWithBroadcasting; +use Mockery as m; use PHPUnit\Framework\TestCase; enum InteractsWithBroadcastingTestConnectionStringEnum: string @@ -31,6 +35,12 @@ enum InteractsWithBroadcastingTestConnectionUnitEnum */ class InteractsWithBroadcastingTest extends TestCase { + protected function tearDown(): void + { + m::close(); + parent::tearDown(); + } + public function testBroadcastViaAcceptsStringBackedEnum(): void { $event = new TestBroadcastingEvent(); @@ -49,14 +59,14 @@ public function testBroadcastViaAcceptsUnitEnum(): void $this->assertSame(['redis'], $event->broadcastConnections()); } - public function testBroadcastViaAcceptsIntBackedEnum(): void + public function testBroadcastViaWithIntBackedEnumStoresIntValue(): void { $event = new TestBroadcastingEvent(); $event->broadcastVia(InteractsWithBroadcastingTestConnectionIntEnum::Connection1); - // Int value 1 should be cast to string '1' - $this->assertSame(['1'], $event->broadcastConnections()); + // Int value is stored as-is (no cast to string) - will fail downstream if string expected + $this->assertSame([1], $event->broadcastConnections()); } public function testBroadcastViaAcceptsNull(): void @@ -85,9 +95,32 @@ public function testBroadcastViaIsChainable(): void $this->assertSame($event, $result); } + + public function testBroadcastWithIntBackedEnumThrowsTypeErrorAtBroadcastTime(): void + { + $event = new TestBroadcastableEvent(); + $event->broadcastVia(InteractsWithBroadcastingTestConnectionIntEnum::Connection1); + + $broadcastEvent = new BroadcastEvent($event); + $manager = m::mock(BroadcastingFactory::class); + + // TypeError is thrown when BroadcastManager::connection() receives int instead of ?string + $this->expectException(\TypeError::class); + $broadcastEvent->handle($manager); + } } class TestBroadcastingEvent { use InteractsWithBroadcasting; } + +class TestBroadcastableEvent +{ + use InteractsWithBroadcasting; + + public function broadcastOn(): Channel + { + return new Channel('test-channel'); + } +} diff --git a/tests/Broadcasting/PendingBroadcastTest.php b/tests/Broadcasting/PendingBroadcastTest.php index c0c2cb61c..7742f4e4a 100644 --- a/tests/Broadcasting/PendingBroadcastTest.php +++ b/tests/Broadcasting/PendingBroadcastTest.php @@ -4,6 +4,9 @@ namespace Hypervel\Tests\Broadcasting; +use Hypervel\Broadcasting\BroadcastEvent; +use Hypervel\Broadcasting\Channel; +use Hypervel\Broadcasting\Contracts\Factory as BroadcastingFactory; use Hypervel\Broadcasting\InteractsWithBroadcasting; use Hypervel\Broadcasting\PendingBroadcast; use Mockery as m; @@ -66,18 +69,17 @@ public function testViaAcceptsUnitEnum(): void $this->assertSame(['redis'], $event->broadcastConnections()); } - public function testViaAcceptsIntBackedEnum(): void + public function testViaWithIntBackedEnumThrowsTypeErrorAtBroadcastTime(): void { - $dispatcher = m::mock(EventDispatcherInterface::class); - $dispatcher->shouldReceive('dispatch')->once(); - - $event = new TestPendingBroadcastEvent(); - $pending = new PendingBroadcast($dispatcher, $event); + $event = new TestPendingBroadcastableEvent(); + $event->broadcastVia(PendingBroadcastTestConnectionIntEnum::Connection1); - $pending->via(PendingBroadcastTestConnectionIntEnum::Connection1); + $broadcastEvent = new BroadcastEvent($event); + $manager = m::mock(BroadcastingFactory::class); - // Int value 1 should be cast to string '1' - $this->assertSame(['1'], $event->broadcastConnections()); + // TypeError is thrown when BroadcastManager::connection() receives int instead of ?string + $this->expectException(\TypeError::class); + $broadcastEvent->handle($manager); } public function testViaAcceptsNull(): void @@ -111,3 +113,13 @@ class TestPendingBroadcastEvent { use InteractsWithBroadcasting; } + +class TestPendingBroadcastableEvent +{ + use InteractsWithBroadcasting; + + public function broadcastOn(): Channel + { + return new Channel('test-channel'); + } +} diff --git a/tests/Bus/QueueableTest.php b/tests/Bus/QueueableTest.php index ff3cd12a4..c19bbba15 100644 --- a/tests/Bus/QueueableTest.php +++ b/tests/Bus/QueueableTest.php @@ -38,12 +38,27 @@ public static function connectionDataProvider(): array return [ 'uses string' => ['redis', 'redis'], 'uses string-backed enum' => [ConnectionEnum::SQS, 'sqs'], - 'uses int-backed enum' => [IntConnectionEnum::Redis, '2'], 'uses unit enum' => [UnitConnectionEnum::Sync, 'Sync'], 'uses null' => [null, null], ]; } + public function testOnConnectionWithIntBackedEnumThrowsTypeError(): void + { + $job = new FakeJob(); + + $this->expectException(\TypeError::class); + $job->onConnection(IntConnectionEnum::Redis); + } + + public function testAllOnConnectionWithIntBackedEnumThrowsTypeError(): void + { + $job = new FakeJob(); + + $this->expectException(\TypeError::class); + $job->allOnConnection(IntConnectionEnum::Redis); + } + #[DataProvider('queuesDataProvider')] public function testOnQueue(mixed $queue, ?string $expected): void { @@ -68,11 +83,26 @@ public static function queuesDataProvider(): array return [ 'uses string' => ['high', 'high'], 'uses string-backed enum' => [QueueEnum::HIGH, 'high'], - 'uses int-backed enum' => [IntQueueEnum::High, '2'], 'uses unit enum' => [UnitQueueEnum::Low, 'Low'], 'uses null' => [null, null], ]; } + + public function testOnQueueWithIntBackedEnumThrowsTypeError(): void + { + $job = new FakeJob(); + + $this->expectException(\TypeError::class); + $job->onQueue(IntQueueEnum::High); + } + + public function testAllOnQueueWithIntBackedEnumThrowsTypeError(): void + { + $job = new FakeJob(); + + $this->expectException(\TypeError::class); + $job->allOnQueue(IntQueueEnum::High); + } } class FakeJob diff --git a/tests/Cache/CacheRedisTaggedCacheTest.php b/tests/Cache/CacheRedisTaggedCacheTest.php index 5f87b76ab..e69c6a162 100644 --- a/tests/Cache/CacheRedisTaggedCacheTest.php +++ b/tests/Cache/CacheRedisTaggedCacheTest.php @@ -12,6 +12,21 @@ use Mockery as m; use Mockery\MockInterface; +enum RedisTaggedCacheTestKeyStringEnum: string +{ + case Counter = 'counter'; +} + +enum RedisTaggedCacheTestKeyIntEnum: int +{ + case Key1 = 1; +} + +enum RedisTaggedCacheTestKeyUnitEnum +{ + case hits; +} + /** * @internal * @coversNothing @@ -158,6 +173,35 @@ public function testPutWithArray() ], 5); } + public function testIncrementAcceptsStringBackedEnum(): void + { + $key = sha1('tag:votes:entries') . ':counter'; + $this->redisProxy->shouldReceive('zadd')->once()->with('prefix:tag:votes:entries', 'NX', -1, $key)->andReturn('OK'); + $this->redisProxy->shouldReceive('incrby')->once()->with("prefix:{$key}", 1)->andReturn(1); + + $result = $this->redis->tags(['votes'])->increment(RedisTaggedCacheTestKeyStringEnum::Counter); + + $this->assertSame(1, $result); + } + + public function testIncrementAcceptsUnitEnum(): void + { + $key = sha1('tag:votes:entries') . ':hits'; + $this->redisProxy->shouldReceive('zadd')->once()->with('prefix:tag:votes:entries', 'NX', -1, $key)->andReturn('OK'); + $this->redisProxy->shouldReceive('incrby')->once()->with("prefix:{$key}", 1)->andReturn(1); + + $result = $this->redis->tags(['votes'])->increment(RedisTaggedCacheTestKeyUnitEnum::hits); + + $this->assertSame(1, $result); + } + + public function testIncrementWithIntBackedEnumThrowsTypeError(): void + { + $this->expectException(\TypeError::class); + + $this->redis->tags(['votes'])->increment(RedisTaggedCacheTestKeyIntEnum::Key1); + } + private function mockRedis() { $this->redis = new RedisStore(m::mock(RedisFactory::class), 'prefix'); diff --git a/tests/Cache/CacheRepositoryEnumTest.php b/tests/Cache/CacheRepositoryEnumTest.php index b8fbb6c98..f13e08d54 100644 --- a/tests/Cache/CacheRepositoryEnumTest.php +++ b/tests/Cache/CacheRepositoryEnumTest.php @@ -12,31 +12,31 @@ use Mockery as m; use Psr\EventDispatcher\EventDispatcherInterface as Dispatcher; -enum CacheKeyBackedEnum: string +enum CacheRepositoryEnumTestKeyBackedEnum: string { case UserProfile = 'user-profile'; case Settings = 'settings'; } -enum CacheKeyIntBackedEnum: int +enum CacheRepositoryEnumTestKeyIntBackedEnum: int { case Counter = 1; case Stats = 2; } -enum CacheKeyUnitEnum +enum CacheRepositoryEnumTestKeyUnitEnum { case Dashboard; case Analytics; } -enum CacheTagBackedEnum: string +enum CacheRepositoryEnumTestTagBackedEnum: string { case Users = 'users'; case Posts = 'posts'; } -enum CacheTagUnitEnum +enum CacheRepositoryEnumTestTagUnitEnum { case Reports; case Exports; @@ -53,7 +53,7 @@ public function testGetWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('cached-value'); - $this->assertSame('cached-value', $repo->get(CacheKeyBackedEnum::UserProfile)); + $this->assertSame('cached-value', $repo->get(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); } public function testGetWithUnitEnum(): void @@ -61,16 +61,16 @@ public function testGetWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn('dashboard-data'); - $this->assertSame('dashboard-data', $repo->get(CacheKeyUnitEnum::Dashboard)); + $this->assertSame('dashboard-data', $repo->get(CacheRepositoryEnumTestKeyUnitEnum::Dashboard)); } - public function testGetWithIntBackedEnum(): void + public function testGetWithIntBackedEnumThrowsTypeError(): void { $repo = $this->getRepository(); - // Int value 1 should be cast to string '1' - $repo->getStore()->shouldReceive('get')->once()->with('1')->andReturn('counter-value'); - $this->assertSame('counter-value', $repo->get(CacheKeyIntBackedEnum::Counter)); + // Int-backed enum causes TypeError because store expects string key + $this->expectException(\TypeError::class); + $repo->get(CacheRepositoryEnumTestKeyIntBackedEnum::Counter); } public function testHasWithBackedEnum(): void @@ -78,7 +78,7 @@ public function testHasWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('value'); - $this->assertTrue($repo->has(CacheKeyBackedEnum::UserProfile)); + $this->assertTrue($repo->has(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); } public function testHasWithUnitEnum(): void @@ -86,7 +86,7 @@ public function testHasWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn(null); - $this->assertFalse($repo->has(CacheKeyUnitEnum::Dashboard)); + $this->assertFalse($repo->has(CacheRepositoryEnumTestKeyUnitEnum::Dashboard)); } public function testMissingWithBackedEnum(): void @@ -94,7 +94,7 @@ public function testMissingWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('get')->once()->with('settings')->andReturn(null); - $this->assertTrue($repo->missing(CacheKeyBackedEnum::Settings)); + $this->assertTrue($repo->missing(CacheRepositoryEnumTestKeyBackedEnum::Settings)); } public function testPutWithBackedEnum(): void @@ -102,7 +102,7 @@ public function testPutWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('put')->once()->with('user-profile', 'value', 60)->andReturn(true); - $this->assertTrue($repo->put(CacheKeyBackedEnum::UserProfile, 'value', 60)); + $this->assertTrue($repo->put(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'value', 60)); } public function testPutWithUnitEnum(): void @@ -110,7 +110,7 @@ public function testPutWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('put')->once()->with('Dashboard', 'data', 120)->andReturn(true); - $this->assertTrue($repo->put(CacheKeyUnitEnum::Dashboard, 'data', 120)); + $this->assertTrue($repo->put(CacheRepositoryEnumTestKeyUnitEnum::Dashboard, 'data', 120)); } public function testSetWithBackedEnum(): void @@ -118,7 +118,7 @@ public function testSetWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('put')->once()->with('settings', 'config', 300)->andReturn(true); - $this->assertTrue($repo->set(CacheKeyBackedEnum::Settings, 'config', 300)); + $this->assertTrue($repo->set(CacheRepositoryEnumTestKeyBackedEnum::Settings, 'config', 300)); } public function testAddWithBackedEnum(): void @@ -127,7 +127,7 @@ public function testAddWithBackedEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn(null); $repo->getStore()->shouldReceive('put')->once()->with('user-profile', 'new-value', 60)->andReturn(true); - $this->assertTrue($repo->add(CacheKeyBackedEnum::UserProfile, 'new-value', 60)); + $this->assertTrue($repo->add(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'new-value', 60)); } public function testAddWithUnitEnum(): void @@ -136,7 +136,7 @@ public function testAddWithUnitEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('Analytics')->andReturn(null); $repo->getStore()->shouldReceive('put')->once()->with('Analytics', 'data', 60)->andReturn(true); - $this->assertTrue($repo->add(CacheKeyUnitEnum::Analytics, 'data', 60)); + $this->assertTrue($repo->add(CacheRepositoryEnumTestKeyUnitEnum::Analytics, 'data', 60)); } public function testIncrementWithBackedEnum(): void @@ -144,7 +144,7 @@ public function testIncrementWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('increment')->once()->with('user-profile', 1)->andReturn(2); - $this->assertSame(2, $repo->increment(CacheKeyBackedEnum::UserProfile)); + $this->assertSame(2, $repo->increment(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); } public function testIncrementWithUnitEnum(): void @@ -152,7 +152,7 @@ public function testIncrementWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('increment')->once()->with('Dashboard', 5)->andReturn(10); - $this->assertSame(10, $repo->increment(CacheKeyUnitEnum::Dashboard, 5)); + $this->assertSame(10, $repo->increment(CacheRepositoryEnumTestKeyUnitEnum::Dashboard, 5)); } public function testDecrementWithBackedEnum(): void @@ -160,7 +160,7 @@ public function testDecrementWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('decrement')->once()->with('settings', 1)->andReturn(4); - $this->assertSame(4, $repo->decrement(CacheKeyBackedEnum::Settings)); + $this->assertSame(4, $repo->decrement(CacheRepositoryEnumTestKeyBackedEnum::Settings)); } public function testDecrementWithUnitEnum(): void @@ -168,7 +168,7 @@ public function testDecrementWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('decrement')->once()->with('Analytics', 3)->andReturn(7); - $this->assertSame(7, $repo->decrement(CacheKeyUnitEnum::Analytics, 3)); + $this->assertSame(7, $repo->decrement(CacheRepositoryEnumTestKeyUnitEnum::Analytics, 3)); } public function testForeverWithBackedEnum(): void @@ -176,7 +176,7 @@ public function testForeverWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('forever')->once()->with('user-profile', 'permanent')->andReturn(true); - $this->assertTrue($repo->forever(CacheKeyBackedEnum::UserProfile, 'permanent')); + $this->assertTrue($repo->forever(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'permanent')); } public function testForeverWithUnitEnum(): void @@ -184,7 +184,7 @@ public function testForeverWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('forever')->once()->with('Dashboard', 'forever-data')->andReturn(true); - $this->assertTrue($repo->forever(CacheKeyUnitEnum::Dashboard, 'forever-data')); + $this->assertTrue($repo->forever(CacheRepositoryEnumTestKeyUnitEnum::Dashboard, 'forever-data')); } public function testRememberWithBackedEnum(): void @@ -193,7 +193,7 @@ public function testRememberWithBackedEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('settings')->andReturn(null); $repo->getStore()->shouldReceive('put')->once()->with('settings', 'computed', 60)->andReturn(true); - $result = $repo->remember(CacheKeyBackedEnum::Settings, 60, fn () => 'computed'); + $result = $repo->remember(CacheRepositoryEnumTestKeyBackedEnum::Settings, 60, fn () => 'computed'); $this->assertSame('computed', $result); } @@ -204,7 +204,7 @@ public function testRememberWithUnitEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('Analytics')->andReturn(null); $repo->getStore()->shouldReceive('put')->once()->with('Analytics', 'analytics-data', 120)->andReturn(true); - $result = $repo->remember(CacheKeyUnitEnum::Analytics, 120, fn () => 'analytics-data'); + $result = $repo->remember(CacheRepositoryEnumTestKeyUnitEnum::Analytics, 120, fn () => 'analytics-data'); $this->assertSame('analytics-data', $result); } @@ -215,7 +215,7 @@ public function testRememberForeverWithBackedEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn(null); $repo->getStore()->shouldReceive('forever')->once()->with('user-profile', 'forever-value')->andReturn(true); - $result = $repo->rememberForever(CacheKeyBackedEnum::UserProfile, fn () => 'forever-value'); + $result = $repo->rememberForever(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, fn () => 'forever-value'); $this->assertSame('forever-value', $result); } @@ -226,7 +226,7 @@ public function testSearWithUnitEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn(null); $repo->getStore()->shouldReceive('forever')->once()->with('Dashboard', 'seared')->andReturn(true); - $result = $repo->sear(CacheKeyUnitEnum::Dashboard, fn () => 'seared'); + $result = $repo->sear(CacheRepositoryEnumTestKeyUnitEnum::Dashboard, fn () => 'seared'); $this->assertSame('seared', $result); } @@ -236,7 +236,7 @@ public function testForgetWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('forget')->once()->with('user-profile')->andReturn(true); - $this->assertTrue($repo->forget(CacheKeyBackedEnum::UserProfile)); + $this->assertTrue($repo->forget(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); } public function testForgetWithUnitEnum(): void @@ -244,7 +244,7 @@ public function testForgetWithUnitEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('forget')->once()->with('Dashboard')->andReturn(true); - $this->assertTrue($repo->forget(CacheKeyUnitEnum::Dashboard)); + $this->assertTrue($repo->forget(CacheRepositoryEnumTestKeyUnitEnum::Dashboard)); } public function testDeleteWithBackedEnum(): void @@ -252,7 +252,7 @@ public function testDeleteWithBackedEnum(): void $repo = $this->getRepository(); $repo->getStore()->shouldReceive('forget')->once()->with('settings')->andReturn(true); - $this->assertTrue($repo->delete(CacheKeyBackedEnum::Settings)); + $this->assertTrue($repo->delete(CacheRepositoryEnumTestKeyBackedEnum::Settings)); } public function testPullWithBackedEnum(): void @@ -261,7 +261,7 @@ public function testPullWithBackedEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('pulled-value'); $repo->getStore()->shouldReceive('forget')->once()->with('user-profile')->andReturn(true); - $this->assertSame('pulled-value', $repo->pull(CacheKeyBackedEnum::UserProfile)); + $this->assertSame('pulled-value', $repo->pull(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); } public function testPullWithUnitEnum(): void @@ -270,7 +270,7 @@ public function testPullWithUnitEnum(): void $repo->getStore()->shouldReceive('get')->once()->with('Analytics')->andReturn('analytics'); $repo->getStore()->shouldReceive('forget')->once()->with('Analytics')->andReturn(true); - $this->assertSame('analytics', $repo->pull(CacheKeyUnitEnum::Analytics)); + $this->assertSame('analytics', $repo->pull(CacheRepositoryEnumTestKeyUnitEnum::Analytics)); } public function testBackedEnumAndStringInteroperability(): void @@ -278,7 +278,7 @@ public function testBackedEnumAndStringInteroperability(): void $repo = new Repository(new ArrayStore()); // Store with enum - $repo->put(CacheKeyBackedEnum::UserProfile, 'enum-stored', 60); + $repo->put(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'enum-stored', 60); // Retrieve with string (the enum value) $this->assertSame('enum-stored', $repo->get('user-profile')); @@ -287,7 +287,7 @@ public function testBackedEnumAndStringInteroperability(): void $repo->put('settings', 'string-stored', 60); // Retrieve with enum - $this->assertSame('string-stored', $repo->get(CacheKeyBackedEnum::Settings)); + $this->assertSame('string-stored', $repo->get(CacheRepositoryEnumTestKeyBackedEnum::Settings)); } public function testUnitEnumAndStringInteroperability(): void @@ -295,7 +295,7 @@ public function testUnitEnumAndStringInteroperability(): void $repo = new Repository(new ArrayStore()); // Store with enum - $repo->put(CacheKeyUnitEnum::Dashboard, 'enum-stored', 60); + $repo->put(CacheRepositoryEnumTestKeyUnitEnum::Dashboard, 'enum-stored', 60); // Retrieve with string (the enum name) $this->assertSame('enum-stored', $repo->get('Dashboard')); @@ -304,14 +304,14 @@ public function testUnitEnumAndStringInteroperability(): void $repo->put('Analytics', 'string-stored', 60); // Retrieve with enum - $this->assertSame('string-stored', $repo->get(CacheKeyUnitEnum::Analytics)); + $this->assertSame('string-stored', $repo->get(CacheRepositoryEnumTestKeyUnitEnum::Analytics)); } public function testTagsWithBackedEnumArray(): void { $repo = new Repository(new ArrayStore()); - $tagged = $repo->tags([CacheTagBackedEnum::Users, CacheTagBackedEnum::Posts]); + $tagged = $repo->tags([CacheRepositoryEnumTestTagBackedEnum::Users, CacheRepositoryEnumTestTagBackedEnum::Posts]); $this->assertInstanceOf(TaggedCache::class, $tagged); $this->assertEquals(['users', 'posts'], $tagged->getTags()->getNames()); @@ -321,7 +321,7 @@ public function testTagsWithUnitEnumArray(): void { $repo = new Repository(new ArrayStore()); - $tagged = $repo->tags([CacheTagUnitEnum::Reports, CacheTagUnitEnum::Exports]); + $tagged = $repo->tags([CacheRepositoryEnumTestTagUnitEnum::Reports, CacheRepositoryEnumTestTagUnitEnum::Exports]); $this->assertInstanceOf(TaggedCache::class, $tagged); $this->assertEquals(['Reports', 'Exports'], $tagged->getTags()->getNames()); @@ -331,7 +331,7 @@ public function testTagsWithMixedEnumsAndStrings(): void { $repo = new Repository(new ArrayStore()); - $tagged = $repo->tags([CacheTagBackedEnum::Users, 'custom-tag', CacheTagUnitEnum::Reports]); + $tagged = $repo->tags([CacheRepositoryEnumTestTagBackedEnum::Users, 'custom-tag', CacheRepositoryEnumTestTagUnitEnum::Reports]); $this->assertInstanceOf(TaggedCache::class, $tagged); $this->assertEquals(['users', 'custom-tag', 'Reports'], $tagged->getTags()->getNames()); @@ -346,7 +346,7 @@ public function testTagsWithBackedEnumVariadicArgs(): void $taggedCache->shouldReceive('setDefaultCacheTime')->andReturnSelf(); $store->shouldReceive('tags')->once()->with(['users', 'posts'])->andReturn($taggedCache); - $repo->tags(CacheTagBackedEnum::Users, CacheTagBackedEnum::Posts); + $repo->tags(CacheRepositoryEnumTestTagBackedEnum::Users, CacheRepositoryEnumTestTagBackedEnum::Posts); } public function testTagsWithUnitEnumVariadicArgs(): void @@ -358,20 +358,20 @@ public function testTagsWithUnitEnumVariadicArgs(): void $taggedCache->shouldReceive('setDefaultCacheTime')->andReturnSelf(); $store->shouldReceive('tags')->once()->with(['Reports', 'Exports'])->andReturn($taggedCache); - $repo->tags(CacheTagUnitEnum::Reports, CacheTagUnitEnum::Exports); + $repo->tags(CacheRepositoryEnumTestTagUnitEnum::Reports, CacheRepositoryEnumTestTagUnitEnum::Exports); } public function testTaggedCacheOperationsWithEnumKeys(): void { $repo = new Repository(new ArrayStore()); - $tagged = $repo->tags([CacheTagBackedEnum::Users]); + $tagged = $repo->tags([CacheRepositoryEnumTestTagBackedEnum::Users]); // Put with enum key - $tagged->put(CacheKeyBackedEnum::UserProfile, 'tagged-value', 60); + $tagged->put(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'tagged-value', 60); // Get with enum key - $this->assertSame('tagged-value', $tagged->get(CacheKeyBackedEnum::UserProfile)); + $this->assertSame('tagged-value', $tagged->get(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); // Get with string key (interoperability) $this->assertSame('tagged-value', $tagged->get('user-profile')); @@ -382,27 +382,27 @@ public function testOffsetAccessWithBackedEnum(): void $repo = new Repository(new ArrayStore()); // offsetSet with enum - $repo[CacheKeyBackedEnum::UserProfile] = 'offset-value'; + $repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile] = 'offset-value'; // offsetGet with enum - $this->assertSame('offset-value', $repo[CacheKeyBackedEnum::UserProfile]); + $this->assertSame('offset-value', $repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile]); // offsetExists with enum - $this->assertTrue(isset($repo[CacheKeyBackedEnum::UserProfile])); + $this->assertTrue(isset($repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile])); // offsetUnset with enum - unset($repo[CacheKeyBackedEnum::UserProfile]); - $this->assertFalse(isset($repo[CacheKeyBackedEnum::UserProfile])); + unset($repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile]); + $this->assertFalse(isset($repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile])); } public function testOffsetAccessWithUnitEnum(): void { $repo = new Repository(new ArrayStore()); - $repo[CacheKeyUnitEnum::Dashboard] = 'dashboard-data'; + $repo[CacheRepositoryEnumTestKeyUnitEnum::Dashboard] = 'dashboard-data'; - $this->assertSame('dashboard-data', $repo[CacheKeyUnitEnum::Dashboard]); - $this->assertTrue(isset($repo[CacheKeyUnitEnum::Dashboard])); + $this->assertSame('dashboard-data', $repo[CacheRepositoryEnumTestKeyUnitEnum::Dashboard]); + $this->assertTrue(isset($repo[CacheRepositoryEnumTestKeyUnitEnum::Dashboard])); } protected function getRepository(): Repository diff --git a/tests/Cache/CacheTaggedCacheTest.php b/tests/Cache/CacheTaggedCacheTest.php index 0f4ccbe9f..667c4fd23 100644 --- a/tests/Cache/CacheTaggedCacheTest.php +++ b/tests/Cache/CacheTaggedCacheTest.php @@ -9,6 +9,24 @@ use Hypervel\Cache\ArrayStore; use Hypervel\Tests\TestCase; +enum TaggedCacheTestKeyStringEnum: string +{ + case Counter = 'counter'; + case Total = 'total'; +} + +enum TaggedCacheTestKeyIntEnum: int +{ + case Key1 = 1; + case Key2 = 2; +} + +enum TaggedCacheTestKeyUnitEnum +{ + case hits; + case misses; +} + /** * @internal * @coversNothing @@ -203,6 +221,76 @@ public function testTagsCacheForever() $this->assertSame('bar', $store->tags($tags)->get('foo')); } + public function testIncrementAcceptsStringBackedEnum(): void + { + $store = new ArrayStore(); + $taggableStore = $store->tags('bop'); + + $taggableStore->put(TaggedCacheTestKeyStringEnum::Counter, 5, 10); + + $value = $taggableStore->increment(TaggedCacheTestKeyStringEnum::Counter); + + $this->assertSame(6, $value); + $this->assertSame(6, $taggableStore->get('counter')); + } + + public function testIncrementAcceptsUnitEnum(): void + { + $store = new ArrayStore(); + $taggableStore = $store->tags('bop'); + + $taggableStore->put('hits', 10, 10); + + $value = $taggableStore->increment(TaggedCacheTestKeyUnitEnum::hits); + + $this->assertSame(11, $value); + } + + public function testIncrementWithIntBackedEnumThrowsTypeError(): void + { + $store = new ArrayStore(); + $taggableStore = $store->tags('bop'); + + // Int-backed enum causes TypeError because itemKey() expects string + $this->expectException(\TypeError::class); + $taggableStore->increment(TaggedCacheTestKeyIntEnum::Key1); + } + + public function testDecrementAcceptsStringBackedEnum(): void + { + $store = new ArrayStore(); + $taggableStore = $store->tags('bop'); + + $taggableStore->put(TaggedCacheTestKeyStringEnum::Counter, 50, 10); + + $value = $taggableStore->decrement(TaggedCacheTestKeyStringEnum::Counter); + + $this->assertSame(49, $value); + $this->assertSame(49, $taggableStore->get('counter')); + } + + public function testDecrementAcceptsUnitEnum(): void + { + $store = new ArrayStore(); + $taggableStore = $store->tags('bop'); + + $taggableStore->put('misses', 20, 10); + + $value = $taggableStore->decrement(TaggedCacheTestKeyUnitEnum::misses); + + $this->assertSame(19, $value); + } + + public function testDecrementWithIntBackedEnumThrowsTypeError(): void + { + $store = new ArrayStore(); + $taggableStore = $store->tags('bop'); + + // Int-backed enum causes TypeError because itemKey() expects string + $this->expectException(\TypeError::class); + $taggableStore->decrement(TaggedCacheTestKeyIntEnum::Key1); + } + private function getTestCacheStoreWithTagValues(): ArrayStore { $store = new ArrayStore(); diff --git a/tests/Cache/RateLimiterEnumTest.php b/tests/Cache/RateLimiterEnumTest.php index 2942ebadf..19d8a2a87 100644 --- a/tests/Cache/RateLimiterEnumTest.php +++ b/tests/Cache/RateLimiterEnumTest.php @@ -17,6 +17,12 @@ enum BackedEnumNamedRateLimiter: string case Web = 'web'; } +enum IntBackedEnumNamedRateLimiter: int +{ + case First = 1; + case Second = 2; +} + enum UnitEnumNamedRateLimiter { case ThirdParty; @@ -135,4 +141,13 @@ public function testMultipleEnumLimitersCanCoexist(): void $this->assertSame('third-party-limit', $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty)()); $this->assertSame('custom-limit', $rateLimiter->limiter('custom')()); } + + public function testForWithIntBackedEnumThrowsTypeError(): void + { + $rateLimiter = new RateLimiter(m::mock(Cache::class)); + + // Int-backed enum causes TypeError because resolveLimiterName() returns string + $this->expectException(\TypeError::class); + $rateLimiter->for(IntBackedEnumNamedRateLimiter::First, fn () => 'limit'); + } } diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 7db196a1d..7e71ebfc1 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -196,14 +196,13 @@ public function testTimezoneAcceptsUnitEnum(): void $this->assertSame('UTC', $event->timezone); } - public function testTimezoneAcceptsIntBackedEnum(): void + public function testTimezoneWithIntBackedEnumThrowsTypeError(): void { $event = new Event(m::mock(EventMutex::class), 'php -i'); + // Int-backed enum causes TypeError because $timezone property is DateTimeZone|string|null + $this->expectException(\TypeError::class); $event->timezone(EventTestTimezoneIntEnum::Zone1); - - // Int value 1 should be cast to string '1' - $this->assertSame('1', $event->timezone); } public function testTimezoneAcceptsDateTimeZoneObject(): void diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index 1fe8137f0..01d35fa1a 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -131,11 +131,12 @@ public function testJobAcceptsUnitEnumForQueueAndConnection(): void self::assertSame(JobToTestWithSchedule::class, $scheduledJob->description); } - public function testJobAcceptsIntBackedEnumForQueueAndConnection(): void + public function testJobWithIntBackedEnumStoresIntValue(): void { $schedule = new Schedule(); - // Int-backed enums should be cast to string + // Int-backed enum values are stored as-is (no cast to string) + // TypeError will occur when the job is dispatched and dispatchToQueue() receives int $scheduledJob = $schedule->job( JobToTestWithSchedule::class, ScheduleTestQueueIntEnum::Priority1, @@ -160,19 +161,18 @@ public function testUseCacheAcceptsStringBackedEnum(): void $schedule->useCache(ScheduleTestCacheStoreEnum::Redis); } - public function testUseCacheAcceptsIntBackedEnum(): void + public function testUseCacheWithIntBackedEnumThrowsTypeError(): void { $eventMutex = m::mock(EventMutex::class, CacheAware::class); - // Int value 1 should be cast to string '1' - $eventMutex->shouldReceive('useStore')->once()->with('1'); - $schedulingMutex = m::mock(SchedulingMutex::class, CacheAware::class); - $schedulingMutex->shouldReceive('useStore')->once()->with('1'); $this->container->instance(EventMutex::class, $eventMutex); $this->container->instance(SchedulingMutex::class, $schedulingMutex); $schedule = new Schedule(); + + // TypeError is thrown when useStore() receives int instead of string + $this->expectException(\TypeError::class); $schedule->useCache(ScheduleTestCacheStoreIntEnum::Store1); } } diff --git a/tests/Core/ContextEnumTest.php b/tests/Core/ContextEnumTest.php index dd68c7be2..585ea4e67 100644 --- a/tests/Core/ContextEnumTest.php +++ b/tests/Core/ContextEnumTest.php @@ -59,14 +59,11 @@ public function testSetAndGetWithUnitEnum(): void $this->assertSame('en-US', Context::get(ContextKeyUnitEnum::Locale)); } - public function testSetAndGetWithIntBackedEnum(): void + public function testSetWithIntBackedEnumThrowsTypeError(): void { + // Int-backed enum causes TypeError because parent::set() expects string key + $this->expectException(\TypeError::class); Context::set(ContextKeyIntBackedEnum::UserId, 'user-123'); - - $this->assertSame('user-123', Context::get(ContextKeyIntBackedEnum::UserId)); - - // Verify it's stored with string key (int value cast to string) - $this->assertSame('user-123', Context::get('1')); } public function testHasWithBackedEnum(): void diff --git a/tests/Core/Database/Eloquent/Factories/FactoryTest.php b/tests/Core/Database/Eloquent/Factories/FactoryTest.php index 333f80a5f..cef1885d5 100644 --- a/tests/Core/Database/Eloquent/Factories/FactoryTest.php +++ b/tests/Core/Database/Eloquent/Factories/FactoryTest.php @@ -874,11 +874,13 @@ public function testConnectionAcceptsStringBackedEnum() $this->assertSame('testing', $factory->getConnectionName()); } - public function testConnectionAcceptsIntBackedEnum() + public function testConnectionWithIntBackedEnumThrowsTypeError() { $factory = FactoryTestUserFactory::new()->connection(FactoryTestIntBackedConnection::Testing); - $this->assertSame('2', $factory->getConnectionName()); + // Int-backed enum causes TypeError because getConnectionName() returns ?string + $this->expectException(\TypeError::class); + $factory->getConnectionName(); } public function testConnectionAcceptsUnitEnum() diff --git a/tests/Core/Database/Eloquent/ModelEnumTest.php b/tests/Core/Database/Eloquent/ModelEnumTest.php index db35750f4..2e9d32761 100644 --- a/tests/Core/Database/Eloquent/ModelEnumTest.php +++ b/tests/Core/Database/Eloquent/ModelEnumTest.php @@ -39,12 +39,13 @@ public function testSetConnectionAcceptsStringBackedEnum(): void $this->assertSame('testing', $model->getConnectionName()); } - public function testSetConnectionAcceptsIntBackedEnum(): void + public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void { $model = new ModelEnumTestModel(); - $model->setConnection(ModelTestIntBackedConnection::Testing); - $this->assertSame('2', $model->getConnectionName()); + // Int-backed enum causes TypeError because $connection property is ?string + $this->expectException(\TypeError::class); + $model->setConnection(ModelTestIntBackedConnection::Testing); } public function testSetConnectionAcceptsUnitEnum(): void diff --git a/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php index 95732b11b..40b7a21e7 100644 --- a/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php +++ b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php @@ -39,12 +39,13 @@ public function testSetConnectionAcceptsStringBackedEnum(): void $this->assertSame('testing', $pivot->getConnectionName()); } - public function testSetConnectionAcceptsIntBackedEnum(): void + public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void { $pivot = new MorphPivot(); - $pivot->setConnection(MorphPivotTestIntBackedConnection::Testing); - $this->assertSame('2', $pivot->getConnectionName()); + // Int-backed enum causes TypeError because $connection property is ?string + $this->expectException(\TypeError::class); + $pivot->setConnection(MorphPivotTestIntBackedConnection::Testing); } public function testSetConnectionAcceptsUnitEnum(): void diff --git a/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php index f7d61525d..52c0b6912 100644 --- a/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php +++ b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php @@ -39,12 +39,13 @@ public function testSetConnectionAcceptsStringBackedEnum(): void $this->assertSame('testing', $pivot->getConnectionName()); } - public function testSetConnectionAcceptsIntBackedEnum(): void + public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void { $pivot = new Pivot(); - $pivot->setConnection(PivotTestIntBackedConnection::Testing); - $this->assertSame('2', $pivot->getConnectionName()); + // Int-backed enum causes TypeError because $connection property is ?string + $this->expectException(\TypeError::class); + $pivot->setConnection(PivotTestIntBackedConnection::Testing); } public function testSetConnectionAcceptsUnitEnum(): void diff --git a/tests/Event/QueuedClosureTest.php b/tests/Event/QueuedClosureTest.php index 5a835e1f1..87d335b7e 100644 --- a/tests/Event/QueuedClosureTest.php +++ b/tests/Event/QueuedClosureTest.php @@ -49,14 +49,12 @@ public function testOnConnectionAcceptsUnitEnum(): void $this->assertSame('sync', $closure->connection); } - public function testOnConnectionAcceptsIntBackedEnum(): void + public function testOnConnectionWithIntBackedEnumThrowsTypeError(): void { $closure = new QueuedClosure(fn () => null); + $this->expectException(\TypeError::class); $closure->onConnection(QueuedClosureTestConnectionIntEnum::Connection1); - - // Int value 1 should be cast to string '1' - $this->assertSame('1', $closure->connection); } public function testOnConnectionAcceptsNull(): void @@ -86,14 +84,12 @@ public function testOnQueueAcceptsUnitEnum(): void $this->assertSame('database', $closure->queue); } - public function testOnQueueAcceptsIntBackedEnum(): void + public function testOnQueueWithIntBackedEnumThrowsTypeError(): void { $closure = new QueuedClosure(fn () => null); + $this->expectException(\TypeError::class); $closure->onQueue(QueuedClosureTestConnectionIntEnum::Connection2); - - // Int value 2 should be cast to string '2' - $this->assertSame('2', $closure->queue); } public function testOnQueueAcceptsNull(): void @@ -123,14 +119,12 @@ public function testOnGroupAcceptsUnitEnum(): void $this->assertSame('sync', $closure->messageGroup); } - public function testOnGroupAcceptsIntBackedEnum(): void + public function testOnGroupWithIntBackedEnumThrowsTypeError(): void { $closure = new QueuedClosure(fn () => null); + $this->expectException(\TypeError::class); $closure->onGroup(QueuedClosureTestConnectionIntEnum::Connection1); - - // Int value 1 should be cast to string '1' - $this->assertSame('1', $closure->messageGroup); } public function testOnQueueSetsQueueProperty(): void diff --git a/tests/Event/QueuedEventsTest.php b/tests/Event/QueuedEventsTest.php index bb0d9798b..fc5fdffe7 100644 --- a/tests/Event/QueuedEventsTest.php +++ b/tests/Event/QueuedEventsTest.php @@ -296,7 +296,7 @@ public function testQueueAcceptsUnitEnumViaProperty(): void $d->dispatch('some.event', ['foo', 'bar']); } - public function testQueueAcceptsIntBackedEnumViaProperty(): void + public function testQueueWithIntBackedEnumViaPropertyThrowsTypeError(): void { $this->container ->shouldReceive('get') @@ -308,13 +308,14 @@ public function testQueueAcceptsIntBackedEnumViaProperty(): void $queue = m::mock(QueueFactoryContract::class); $connection = m::mock(QueueContract::class); - // Int value 1 should be cast to string '1' - $connection->shouldReceive('pushOn')->with('1', m::type(CallQueuedListener::class))->once(); $queue->shouldReceive('connection')->with(null)->once()->andReturn($connection); $d->setQueueResolver(fn () => $queue); $d->listen('some.event', TestDispatcherIntEnumQueueProperty::class . '@handle'); + + // TypeError is thrown when pushOn() receives int instead of ?string + $this->expectException(\TypeError::class); $d->dispatch('some.event', ['foo', 'bar']); } diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index cbd07568a..01f14e8f0 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -241,12 +241,11 @@ public function testDiskAcceptsUnitEnum(): void $this->assertInstanceOf(Filesystem::class, $disk); } - public function testDiskAcceptsIntBackedEnum(): void + public function testDiskWithIntBackedEnumThrowsTypeError(): void { $container = $this->getContainer([ 'disks' => [ - // Int value 1 should be cast to string '1' - '1' => [ + 'local' => [ 'driver' => 'local', 'root' => __DIR__ . '/tmp', ], @@ -254,9 +253,9 @@ public function testDiskAcceptsIntBackedEnum(): void ]); $filesystem = new FilesystemManager($container); - $disk = $filesystem->disk(FilesystemTestIntBackedDisk::Local); - - $this->assertInstanceOf(Filesystem::class, $disk); + // Int-backed enum causes TypeError because get() expects string + $this->expectException(\TypeError::class); + $filesystem->disk(FilesystemTestIntBackedDisk::Local); } public function testDriveAcceptsStringBackedEnum(): void diff --git a/tests/Queue/RateLimitedTest.php b/tests/Queue/RateLimitedTest.php new file mode 100644 index 000000000..58ffb072f --- /dev/null +++ b/tests/Queue/RateLimitedTest.php @@ -0,0 +1,110 @@ +mockRateLimiter(); + + new RateLimited('default'); + + $this->assertTrue(true); + } + + public function testConstructorAcceptsStringBackedEnum(): void + { + $this->mockRateLimiter(); + + new RateLimited(RateLimitedTestStringEnum::Default); + + $this->assertTrue(true); + } + + public function testConstructorAcceptsUnitEnum(): void + { + $this->mockRateLimiter(); + + new RateLimited(RateLimitedTestUnitEnum::uploads); + + $this->assertTrue(true); + } + + public function testConstructorWithIntBackedEnumThrowsTypeError(): void + { + $this->mockRateLimiter(); + + $this->expectException(\TypeError::class); + + new RateLimited(RateLimitedTestIntEnum::Primary); + } + + public function testDontReleaseSetsShouldReleaseToFalse(): void + { + $this->mockRateLimiter(); + + $middleware = new RateLimited('default'); + + $this->assertTrue($middleware->shouldRelease); + + $result = $middleware->dontRelease(); + + $this->assertFalse($middleware->shouldRelease); + $this->assertSame($middleware, $result); + } + + /** + * Create a mock RateLimiter and set up the container. + */ + protected function mockRateLimiter(): RateLimiter&MockInterface + { + $limiter = Mockery::mock(RateLimiter::class); + + $container = new Container( + new DefinitionSource([ + RateLimiter::class => fn () => $limiter, + ]) + ); + + ApplicationContext::setContainer($container); + + return $limiter; + } +} diff --git a/tests/Redis/RedisTest.php b/tests/Redis/RedisTest.php index 3d542386b..7efae19a2 100644 --- a/tests/Redis/RedisTest.php +++ b/tests/Redis/RedisTest.php @@ -285,16 +285,9 @@ public function testConnectionAcceptsUnitEnum(): void $this->assertSame($mockRedisProxy, $result); } - public function testConnectionAcceptsIntBackedEnum(): void + public function testConnectionWithIntBackedEnumThrowsTypeError(): void { - $mockRedisProxy = Mockery::mock(RedisProxy::class); - $mockRedisFactory = Mockery::mock(RedisFactory::class); - // Int value 1 should be cast to string '1' - $mockRedisFactory->shouldReceive('get') - ->with('1') - ->once() - ->andReturn($mockRedisProxy); $mockContainer = Mockery::mock(\Hypervel\Container\Contracts\Container::class); $mockContainer->shouldReceive('get') @@ -305,9 +298,9 @@ public function testConnectionAcceptsIntBackedEnum(): void $redis = new Redis(Mockery::mock(PoolFactory::class)); - $result = $redis->connection(RedisTestIntBackedConnection::Primary); - - $this->assertSame($mockRedisProxy, $result); + // Int-backed enum causes TypeError because RedisFactory::get() expects string + $this->expectException(\TypeError::class); + $redis->connection(RedisTestIntBackedConnection::Primary); } /** From 9c38d3cd454e6b50be7809eda321bd9c74e75549 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:00:48 +0000 Subject: [PATCH 17/19] Use enum_value() for Sanctum token abilities Replace Str::from() and Str::fromAll() with enum_value() for consistency with other enum-supporting APIs across the codebase. Also updates type hints from BackedEnum to UnitEnum to match the established pattern. --- src/bus/src/PendingBatch.php | 2 +- src/bus/src/PendingChain.php | 2 +- src/bus/src/PendingDispatch.php | 2 +- src/sanctum/src/Contracts/HasAbilities.php | 6 +++--- src/sanctum/src/Contracts/HasApiTokens.php | 8 ++++---- src/sanctum/src/HasApiTokens.php | 13 +++++++------ src/sanctum/src/PersonalAccessToken.php | 11 ++++++----- src/sanctum/src/TransientToken.php | 6 +++--- .../Broadcasting/InteractsWithBroadcastingTest.php | 3 ++- tests/Broadcasting/PendingBroadcastTest.php | 3 ++- tests/Bus/QueueableTest.php | 9 +++++---- tests/Cache/CacheRedisTaggedCacheTest.php | 3 ++- tests/Cache/CacheRepositoryEnumTest.php | 3 ++- tests/Cache/CacheTaggedCacheTest.php | 5 +++-- tests/Cache/RateLimiterEnumTest.php | 3 ++- tests/Console/Scheduling/EventTest.php | 3 ++- tests/Console/Scheduling/ScheduleTest.php | 3 ++- tests/Core/ContextEnumTest.php | 3 ++- .../Database/Eloquent/Factories/FactoryTest.php | 3 ++- tests/Core/Database/Eloquent/ModelEnumTest.php | 3 ++- .../Eloquent/Relations/MorphPivotEnumTest.php | 3 ++- .../Database/Eloquent/Relations/PivotEnumTest.php | 3 ++- tests/Event/QueuedClosureTest.php | 7 ++++--- tests/Event/QueuedEventsTest.php | 3 ++- tests/Filesystem/FilesystemManagerTest.php | 3 ++- tests/Queue/RateLimitedTest.php | 3 ++- tests/Redis/RedisTest.php | 3 ++- 27 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/bus/src/PendingBatch.php b/src/bus/src/PendingBatch.php index d166fffa7..554d1b0a0 100644 --- a/src/bus/src/PendingBatch.php +++ b/src/bus/src/PendingBatch.php @@ -6,7 +6,6 @@ use Closure; use Hyperf\Collection\Arr; -use UnitEnum; use Hyperf\Collection\Collection; use Hyperf\Conditionable\Conditionable; use Hyperf\Coroutine\Coroutine; @@ -17,6 +16,7 @@ use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Throwable; +use UnitEnum; use function Hyperf\Support\value; use function Hypervel\Support\enum_value; diff --git a/src/bus/src/PendingChain.php b/src/bus/src/PendingChain.php index 12ec630f6..dad761372 100644 --- a/src/bus/src/PendingChain.php +++ b/src/bus/src/PendingChain.php @@ -7,12 +7,12 @@ use Closure; use DateInterval; use DateTimeInterface; -use UnitEnum; use Hyperf\Conditionable\Conditionable; use Hyperf\Context\ApplicationContext; use Hypervel\Bus\Contracts\Dispatcher; use Hypervel\Queue\CallQueuedClosure; use Laravel\SerializableClosure\SerializableClosure; +use UnitEnum; use function Hyperf\Support\value; use function Hypervel\Support\enum_value; diff --git a/src/bus/src/PendingDispatch.php b/src/bus/src/PendingDispatch.php index 4d070c32c..6649cd1b7 100644 --- a/src/bus/src/PendingDispatch.php +++ b/src/bus/src/PendingDispatch.php @@ -7,10 +7,10 @@ use DateInterval; use DateTimeInterface; use Hyperf\Context\ApplicationContext; -use UnitEnum; use Hypervel\Bus\Contracts\Dispatcher; use Hypervel\Cache\Contracts\Factory as CacheFactory; use Hypervel\Queue\Contracts\ShouldBeUnique; +use UnitEnum; class PendingDispatch { diff --git a/src/sanctum/src/Contracts/HasAbilities.php b/src/sanctum/src/Contracts/HasAbilities.php index c14fe88a4..e2c7840cc 100644 --- a/src/sanctum/src/Contracts/HasAbilities.php +++ b/src/sanctum/src/Contracts/HasAbilities.php @@ -4,17 +4,17 @@ namespace Hypervel\Sanctum\Contracts; -use BackedEnum; +use UnitEnum; interface HasAbilities { /** * Determine if the token has a given ability. */ - public function can(BackedEnum|string $ability): bool; + public function can(UnitEnum|string $ability): bool; /** * Determine if the token is missing a given ability. */ - public function cant(BackedEnum|string $ability): bool; + public function cant(UnitEnum|string $ability): bool; } diff --git a/src/sanctum/src/Contracts/HasApiTokens.php b/src/sanctum/src/Contracts/HasApiTokens.php index 7f2d7c535..ca41b2dc2 100644 --- a/src/sanctum/src/Contracts/HasApiTokens.php +++ b/src/sanctum/src/Contracts/HasApiTokens.php @@ -4,9 +4,9 @@ namespace Hypervel\Sanctum\Contracts; -use BackedEnum; use DateTimeInterface; use Hyperf\Database\Model\Relations\MorphMany; +use UnitEnum; interface HasApiTokens { @@ -18,17 +18,17 @@ public function tokens(): MorphMany; /** * Determine if the current API token has a given ability. */ - public function tokenCan(BackedEnum|string $ability): bool; + public function tokenCan(UnitEnum|string $ability): bool; /** * Determine if the current API token is missing a given ability. */ - public function tokenCant(BackedEnum|string $ability): bool; + public function tokenCant(UnitEnum|string $ability): bool; /** * Create a new personal access token for the user. * - * @param array $abilities + * @param array $abilities */ public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null): \Hypervel\Sanctum\NewAccessToken; diff --git a/src/sanctum/src/HasApiTokens.php b/src/sanctum/src/HasApiTokens.php index 03eabc2d6..fab12e442 100644 --- a/src/sanctum/src/HasApiTokens.php +++ b/src/sanctum/src/HasApiTokens.php @@ -4,11 +4,12 @@ namespace Hypervel\Sanctum; -use BackedEnum; use DateTimeInterface; use Hyperf\Database\Model\Relations\MorphMany; use Hypervel\Sanctum\Contracts\HasAbilities; -use Hypervel\Support\Str; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @template TToken of \Hypervel\Sanctum\Contracts\HasAbilities = \Hypervel\Sanctum\PersonalAccessToken @@ -35,7 +36,7 @@ public function tokens(): MorphMany /** * Determine if the current API token has a given ability. */ - public function tokenCan(BackedEnum|string $ability): bool + public function tokenCan(UnitEnum|string $ability): bool { return $this->accessToken && $this->accessToken->can($ability); } @@ -43,7 +44,7 @@ public function tokenCan(BackedEnum|string $ability): bool /** * Determine if the current API token does not have a given ability. */ - public function tokenCant(BackedEnum|string $ability): bool + public function tokenCant(UnitEnum|string $ability): bool { return ! $this->tokenCan($ability); } @@ -51,11 +52,11 @@ public function tokenCant(BackedEnum|string $ability): bool /** * Create a new personal access token for the user. * - * @param array $abilities + * @param array $abilities */ public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null): NewAccessToken { - $abilities = Str::fromAll($abilities); + $abilities = array_map(enum_value(...), $abilities); $plainTextToken = $this->generateTokenString(); diff --git a/src/sanctum/src/PersonalAccessToken.php b/src/sanctum/src/PersonalAccessToken.php index e0a761464..d6b647db5 100644 --- a/src/sanctum/src/PersonalAccessToken.php +++ b/src/sanctum/src/PersonalAccessToken.php @@ -4,7 +4,6 @@ namespace Hypervel\Sanctum; -use BackedEnum; use Hyperf\Database\Model\Events\Deleting; use Hyperf\Database\Model\Events\Updating; use Hyperf\Database\Model\Relations\MorphTo; @@ -14,7 +13,9 @@ use Hypervel\Context\ApplicationContext; use Hypervel\Database\Eloquent\Model; use Hypervel\Sanctum\Contracts\HasAbilities; -use Hypervel\Support\Str; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** * @property int|string $id @@ -154,9 +155,9 @@ public static function findTokenable(PersonalAccessToken $accessToken): ?Authent /** * Determine if the token has a given ability. */ - public function can(BackedEnum|string $ability): bool + public function can(UnitEnum|string $ability): bool { - $ability = Str::from($ability); + $ability = enum_value($ability); return in_array('*', $this->abilities) || array_key_exists($ability, array_flip($this->abilities)); @@ -165,7 +166,7 @@ public function can(BackedEnum|string $ability): bool /** * Determine if the token is missing a given ability. */ - public function cant(BackedEnum|string $ability): bool + public function cant(UnitEnum|string $ability): bool { return ! $this->can($ability); } diff --git a/src/sanctum/src/TransientToken.php b/src/sanctum/src/TransientToken.php index c975bfccf..40e485f74 100644 --- a/src/sanctum/src/TransientToken.php +++ b/src/sanctum/src/TransientToken.php @@ -4,15 +4,15 @@ namespace Hypervel\Sanctum; -use BackedEnum; use Hypervel\Sanctum\Contracts\HasAbilities; +use UnitEnum; class TransientToken implements HasAbilities { /** * Determine if the token has a given ability. */ - public function can(BackedEnum|string $ability): bool + public function can(UnitEnum|string $ability): bool { return true; } @@ -20,7 +20,7 @@ public function can(BackedEnum|string $ability): bool /** * Determine if the token is missing a given ability. */ - public function cant(BackedEnum|string $ability): bool + public function cant(UnitEnum|string $ability): bool { return false; } diff --git a/tests/Broadcasting/InteractsWithBroadcastingTest.php b/tests/Broadcasting/InteractsWithBroadcastingTest.php index 3e62ee2fc..d0bdc0483 100644 --- a/tests/Broadcasting/InteractsWithBroadcastingTest.php +++ b/tests/Broadcasting/InteractsWithBroadcastingTest.php @@ -10,6 +10,7 @@ use Hypervel\Broadcasting\InteractsWithBroadcasting; use Mockery as m; use PHPUnit\Framework\TestCase; +use TypeError; enum InteractsWithBroadcastingTestConnectionStringEnum: string { @@ -105,7 +106,7 @@ public function testBroadcastWithIntBackedEnumThrowsTypeErrorAtBroadcastTime(): $manager = m::mock(BroadcastingFactory::class); // TypeError is thrown when BroadcastManager::connection() receives int instead of ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $broadcastEvent->handle($manager); } } diff --git a/tests/Broadcasting/PendingBroadcastTest.php b/tests/Broadcasting/PendingBroadcastTest.php index 7742f4e4a..ddaccf978 100644 --- a/tests/Broadcasting/PendingBroadcastTest.php +++ b/tests/Broadcasting/PendingBroadcastTest.php @@ -12,6 +12,7 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; +use TypeError; enum PendingBroadcastTestConnectionStringEnum: string { @@ -78,7 +79,7 @@ public function testViaWithIntBackedEnumThrowsTypeErrorAtBroadcastTime(): void $manager = m::mock(BroadcastingFactory::class); // TypeError is thrown when BroadcastManager::connection() receives int instead of ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $broadcastEvent->handle($manager); } diff --git a/tests/Bus/QueueableTest.php b/tests/Bus/QueueableTest.php index c19bbba15..d5005df6c 100644 --- a/tests/Bus/QueueableTest.php +++ b/tests/Bus/QueueableTest.php @@ -7,6 +7,7 @@ use Hypervel\Bus\Queueable; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use TypeError; /** * @internal @@ -47,7 +48,7 @@ public function testOnConnectionWithIntBackedEnumThrowsTypeError(): void { $job = new FakeJob(); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $job->onConnection(IntConnectionEnum::Redis); } @@ -55,7 +56,7 @@ public function testAllOnConnectionWithIntBackedEnumThrowsTypeError(): void { $job = new FakeJob(); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $job->allOnConnection(IntConnectionEnum::Redis); } @@ -92,7 +93,7 @@ public function testOnQueueWithIntBackedEnumThrowsTypeError(): void { $job = new FakeJob(); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $job->onQueue(IntQueueEnum::High); } @@ -100,7 +101,7 @@ public function testAllOnQueueWithIntBackedEnumThrowsTypeError(): void { $job = new FakeJob(); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $job->allOnQueue(IntQueueEnum::High); } } diff --git a/tests/Cache/CacheRedisTaggedCacheTest.php b/tests/Cache/CacheRedisTaggedCacheTest.php index e69c6a162..d4e575111 100644 --- a/tests/Cache/CacheRedisTaggedCacheTest.php +++ b/tests/Cache/CacheRedisTaggedCacheTest.php @@ -11,6 +11,7 @@ use Hypervel\Tests\TestCase; use Mockery as m; use Mockery\MockInterface; +use TypeError; enum RedisTaggedCacheTestKeyStringEnum: string { @@ -197,7 +198,7 @@ public function testIncrementAcceptsUnitEnum(): void public function testIncrementWithIntBackedEnumThrowsTypeError(): void { - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $this->redis->tags(['votes'])->increment(RedisTaggedCacheTestKeyIntEnum::Key1); } diff --git a/tests/Cache/CacheRepositoryEnumTest.php b/tests/Cache/CacheRepositoryEnumTest.php index f13e08d54..c04dbe149 100644 --- a/tests/Cache/CacheRepositoryEnumTest.php +++ b/tests/Cache/CacheRepositoryEnumTest.php @@ -11,6 +11,7 @@ use Hypervel\Tests\TestCase; use Mockery as m; use Psr\EventDispatcher\EventDispatcherInterface as Dispatcher; +use TypeError; enum CacheRepositoryEnumTestKeyBackedEnum: string { @@ -69,7 +70,7 @@ public function testGetWithIntBackedEnumThrowsTypeError(): void $repo = $this->getRepository(); // Int-backed enum causes TypeError because store expects string key - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $repo->get(CacheRepositoryEnumTestKeyIntBackedEnum::Counter); } diff --git a/tests/Cache/CacheTaggedCacheTest.php b/tests/Cache/CacheTaggedCacheTest.php index 667c4fd23..2d212f0a4 100644 --- a/tests/Cache/CacheTaggedCacheTest.php +++ b/tests/Cache/CacheTaggedCacheTest.php @@ -8,6 +8,7 @@ use DateTime; use Hypervel\Cache\ArrayStore; use Hypervel\Tests\TestCase; +use TypeError; enum TaggedCacheTestKeyStringEnum: string { @@ -252,7 +253,7 @@ public function testIncrementWithIntBackedEnumThrowsTypeError(): void $taggableStore = $store->tags('bop'); // Int-backed enum causes TypeError because itemKey() expects string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $taggableStore->increment(TaggedCacheTestKeyIntEnum::Key1); } @@ -287,7 +288,7 @@ public function testDecrementWithIntBackedEnumThrowsTypeError(): void $taggableStore = $store->tags('bop'); // Int-backed enum causes TypeError because itemKey() expects string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $taggableStore->decrement(TaggedCacheTestKeyIntEnum::Key1); } diff --git a/tests/Cache/RateLimiterEnumTest.php b/tests/Cache/RateLimiterEnumTest.php index 19d8a2a87..6f123a3d2 100644 --- a/tests/Cache/RateLimiterEnumTest.php +++ b/tests/Cache/RateLimiterEnumTest.php @@ -10,6 +10,7 @@ use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use ReflectionProperty; +use TypeError; enum BackedEnumNamedRateLimiter: string { @@ -147,7 +148,7 @@ public function testForWithIntBackedEnumThrowsTypeError(): void $rateLimiter = new RateLimiter(m::mock(Cache::class)); // Int-backed enum causes TypeError because resolveLimiterName() returns string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $rateLimiter->for(IntBackedEnumNamedRateLimiter::First, fn () => 'limit'); } } diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 7e71ebfc1..6268b87cb 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -17,6 +17,7 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +use TypeError; enum EventTestTimezoneStringEnum: string { @@ -201,7 +202,7 @@ public function testTimezoneWithIntBackedEnumThrowsTypeError(): void $event = new Event(m::mock(EventMutex::class), 'php -i'); // Int-backed enum causes TypeError because $timezone property is DateTimeZone|string|null - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $event->timezone(EventTestTimezoneIntEnum::Zone1); } diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index 01d35fa1a..a6803e95f 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -15,6 +15,7 @@ use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use TypeError; enum ScheduleTestQueueStringEnum: string { @@ -172,7 +173,7 @@ public function testUseCacheWithIntBackedEnumThrowsTypeError(): void $schedule = new Schedule(); // TypeError is thrown when useStore() receives int instead of string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $schedule->useCache(ScheduleTestCacheStoreIntEnum::Store1); } } diff --git a/tests/Core/ContextEnumTest.php b/tests/Core/ContextEnumTest.php index 585ea4e67..c55ae126e 100644 --- a/tests/Core/ContextEnumTest.php +++ b/tests/Core/ContextEnumTest.php @@ -6,6 +6,7 @@ use Hypervel\Context\Context; use PHPUnit\Framework\TestCase; +use TypeError; enum ContextKeyBackedEnum: string { @@ -62,7 +63,7 @@ public function testSetAndGetWithUnitEnum(): void public function testSetWithIntBackedEnumThrowsTypeError(): void { // Int-backed enum causes TypeError because parent::set() expects string key - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); Context::set(ContextKeyIntBackedEnum::UserId, 'user-123'); } diff --git a/tests/Core/Database/Eloquent/Factories/FactoryTest.php b/tests/Core/Database/Eloquent/Factories/FactoryTest.php index cef1885d5..f124e3523 100644 --- a/tests/Core/Database/Eloquent/Factories/FactoryTest.php +++ b/tests/Core/Database/Eloquent/Factories/FactoryTest.php @@ -20,6 +20,7 @@ use Hypervel\Tests\Core\Database\Fixtures\Models\Price; use Mockery as m; use ReflectionClass; +use TypeError; enum FactoryTestStringBackedConnection: string { @@ -879,7 +880,7 @@ public function testConnectionWithIntBackedEnumThrowsTypeError() $factory = FactoryTestUserFactory::new()->connection(FactoryTestIntBackedConnection::Testing); // Int-backed enum causes TypeError because getConnectionName() returns ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $factory->getConnectionName(); } diff --git a/tests/Core/Database/Eloquent/ModelEnumTest.php b/tests/Core/Database/Eloquent/ModelEnumTest.php index 2e9d32761..ef4ebc2dc 100644 --- a/tests/Core/Database/Eloquent/ModelEnumTest.php +++ b/tests/Core/Database/Eloquent/ModelEnumTest.php @@ -6,6 +6,7 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Testbench\TestCase; +use TypeError; enum ModelTestStringBackedConnection: string { @@ -44,7 +45,7 @@ public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void $model = new ModelEnumTestModel(); // Int-backed enum causes TypeError because $connection property is ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $model->setConnection(ModelTestIntBackedConnection::Testing); } diff --git a/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php index 40b7a21e7..9729566eb 100644 --- a/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php +++ b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php @@ -6,6 +6,7 @@ use Hypervel\Database\Eloquent\Relations\MorphPivot; use Hypervel\Testbench\TestCase; +use TypeError; enum MorphPivotTestStringBackedConnection: string { @@ -44,7 +45,7 @@ public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void $pivot = new MorphPivot(); // Int-backed enum causes TypeError because $connection property is ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $pivot->setConnection(MorphPivotTestIntBackedConnection::Testing); } diff --git a/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php index 52c0b6912..815afdfbb 100644 --- a/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php +++ b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php @@ -6,6 +6,7 @@ use Hypervel\Database\Eloquent\Relations\Pivot; use Hypervel\Testbench\TestCase; +use TypeError; enum PivotTestStringBackedConnection: string { @@ -44,7 +45,7 @@ public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void $pivot = new Pivot(); // Int-backed enum causes TypeError because $connection property is ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $pivot->setConnection(PivotTestIntBackedConnection::Testing); } diff --git a/tests/Event/QueuedClosureTest.php b/tests/Event/QueuedClosureTest.php index 87d335b7e..88779388c 100644 --- a/tests/Event/QueuedClosureTest.php +++ b/tests/Event/QueuedClosureTest.php @@ -6,6 +6,7 @@ use Hypervel\Event\QueuedClosure; use PHPUnit\Framework\TestCase; +use TypeError; enum QueuedClosureTestConnectionStringEnum: string { @@ -53,7 +54,7 @@ public function testOnConnectionWithIntBackedEnumThrowsTypeError(): void { $closure = new QueuedClosure(fn () => null); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $closure->onConnection(QueuedClosureTestConnectionIntEnum::Connection1); } @@ -88,7 +89,7 @@ public function testOnQueueWithIntBackedEnumThrowsTypeError(): void { $closure = new QueuedClosure(fn () => null); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $closure->onQueue(QueuedClosureTestConnectionIntEnum::Connection2); } @@ -123,7 +124,7 @@ public function testOnGroupWithIntBackedEnumThrowsTypeError(): void { $closure = new QueuedClosure(fn () => null); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $closure->onGroup(QueuedClosureTestConnectionIntEnum::Connection1); } diff --git a/tests/Event/QueuedEventsTest.php b/tests/Event/QueuedEventsTest.php index fc5fdffe7..24d1bdd5f 100644 --- a/tests/Event/QueuedEventsTest.php +++ b/tests/Event/QueuedEventsTest.php @@ -22,6 +22,7 @@ use Mockery as m; use Mockery\MockInterface; use Psr\Container\ContainerInterface; +use TypeError; use function Hypervel\Event\queueable; @@ -315,7 +316,7 @@ public function testQueueWithIntBackedEnumViaPropertyThrowsTypeError(): void $d->listen('some.event', TestDispatcherIntEnumQueueProperty::class . '@handle'); // TypeError is thrown when pushOn() receives int instead of ?string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $d->dispatch('some.event', ['foo', 'bar']); } diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 01f14e8f0..5402f47f4 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -18,6 +18,7 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; +use TypeError; enum FilesystemTestStringBackedDisk: string { @@ -254,7 +255,7 @@ public function testDiskWithIntBackedEnumThrowsTypeError(): void $filesystem = new FilesystemManager($container); // Int-backed enum causes TypeError because get() expects string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $filesystem->disk(FilesystemTestIntBackedDisk::Local); } diff --git a/tests/Queue/RateLimitedTest.php b/tests/Queue/RateLimitedTest.php index 58ffb072f..44b0e50c7 100644 --- a/tests/Queue/RateLimitedTest.php +++ b/tests/Queue/RateLimitedTest.php @@ -12,6 +12,7 @@ use Mockery; use Mockery\MockInterface; use PHPUnit\Framework\TestCase; +use TypeError; enum RateLimitedTestStringEnum: string { @@ -71,7 +72,7 @@ public function testConstructorWithIntBackedEnumThrowsTypeError(): void { $this->mockRateLimiter(); - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); new RateLimited(RateLimitedTestIntEnum::Primary); } diff --git a/tests/Redis/RedisTest.php b/tests/Redis/RedisTest.php index 7efae19a2..62320947f 100644 --- a/tests/Redis/RedisTest.php +++ b/tests/Redis/RedisTest.php @@ -20,6 +20,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Redis as PhpRedis; use Throwable; +use TypeError; enum RedisTestStringBackedConnection: string { @@ -299,7 +300,7 @@ public function testConnectionWithIntBackedEnumThrowsTypeError(): void $redis = new Redis(Mockery::mock(PoolFactory::class)); // Int-backed enum causes TypeError because RedisFactory::get() expects string - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $redis->connection(RedisTestIntBackedConnection::Primary); } From 795cad78da5a76fd46cb9151cf8a23d76ff420b5 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:47:22 +0000 Subject: [PATCH 18/19] Add enum support to Cookie, Js, ThrottleRequests, and PendingBatch - Cookie: has(), get(), make(), expire(), unqueue(), forever(), forget() - Js: Changed BackedEnum to UnitEnum, use enum_value() - ThrottleRequests::using(): Accept enum limiter names - PendingBatch/PendingChain::onConnection(): Accept enum connection names - Updated facade docblocks via facade-documenter - Added comprehensive enum tests --- src/bus/src/PendingBatch.php | 4 +- src/bus/src/PendingChain.php | 4 +- src/cookie/src/Contracts/Cookie.php | 15 +- src/cookie/src/CookieManager.php | 23 +-- .../src/Middleware/ThrottleRequests.php | 7 +- src/support/src/Facades/Cache.php | 2 +- src/support/src/Facades/Cookie.php | 14 +- src/support/src/Facades/Gate.php | 18 +-- src/support/src/Facades/RateLimiter.php | 4 +- src/support/src/Facades/Redis.php | 2 +- src/support/src/Facades/Schedule.php | 6 +- src/support/src/Facades/Session.php | 34 ++-- src/support/src/Facades/Storage.php | 15 +- src/support/src/Js.php | 6 +- tests/Bus/BusPendingBatchTest.php | 49 ++++++ tests/Cookie/CookieManagerTest.php | 153 ++++++++++++++++++ .../Middleware/ThrottleRequestsTest.php | 61 +++++++ tests/Support/JsTest.php | 89 ++++++++++ 18 files changed, 435 insertions(+), 71 deletions(-) create mode 100644 tests/Router/Middleware/ThrottleRequestsTest.php create mode 100644 tests/Support/JsTest.php diff --git a/src/bus/src/PendingBatch.php b/src/bus/src/PendingBatch.php index 554d1b0a0..085684c74 100644 --- a/src/bus/src/PendingBatch.php +++ b/src/bus/src/PendingBatch.php @@ -194,9 +194,9 @@ public function name(string $name): static /** * Specify the queue connection that the batched jobs should run on. */ - public function onConnection(string $connection): static + public function onConnection(UnitEnum|string $connection): static { - $this->options['connection'] = $connection; + $this->options['connection'] = enum_value($connection); return $this; } diff --git a/src/bus/src/PendingChain.php b/src/bus/src/PendingChain.php index dad761372..bcb884033 100644 --- a/src/bus/src/PendingChain.php +++ b/src/bus/src/PendingChain.php @@ -56,9 +56,9 @@ public function __construct( /** * Set the desired connection for the job. */ - public function onConnection(?string $connection): static + public function onConnection(UnitEnum|string|null $connection): static { - $this->connection = $connection; + $this->connection = enum_value($connection); return $this; } diff --git a/src/cookie/src/Contracts/Cookie.php b/src/cookie/src/Contracts/Cookie.php index 438b79ed4..5f056bac2 100644 --- a/src/cookie/src/Contracts/Cookie.php +++ b/src/cookie/src/Contracts/Cookie.php @@ -5,24 +5,25 @@ namespace Hypervel\Cookie\Contracts; use Hyperf\HttpMessage\Cookie\Cookie as HyperfCookie; +use UnitEnum; interface Cookie { - public function has(string $key): bool; + public function has(UnitEnum|string $key): bool; - public function get(string $key, ?string $default = null): ?string; + public function get(UnitEnum|string $key, ?string $default = null): ?string; - public function make(string $name, string $value, int $minutes = 0, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): HyperfCookie; + public function make(UnitEnum|string $name, string $value, int $minutes = 0, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): HyperfCookie; public function queue(...$parameters): void; - public function expire(string $name, string $path = '', string $domain = ''): void; + public function expire(UnitEnum|string $name, string $path = '', string $domain = ''): void; - public function unqueue(string $name, string $path = ''): void; + public function unqueue(UnitEnum|string $name, string $path = ''): void; public function getQueuedCookies(): array; - public function forever(string $name, string $value, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): HyperfCookie; + public function forever(UnitEnum|string $name, string $value, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): HyperfCookie; - public function forget(string $name, string $path = '', string $domain = ''): HyperfCookie; + public function forget(UnitEnum|string $name, string $path = '', string $domain = ''): HyperfCookie; } diff --git a/src/cookie/src/CookieManager.php b/src/cookie/src/CookieManager.php index 819fe1462..ce1b6074f 100644 --- a/src/cookie/src/CookieManager.php +++ b/src/cookie/src/CookieManager.php @@ -9,6 +9,9 @@ use Hyperf\HttpServer\Contract\RequestInterface; use Hyperf\Support\Traits\InteractsWithTime; use Hypervel\Cookie\Contracts\Cookie as CookieContract; +use UnitEnum; + +use function Hypervel\Support\enum_value; class CookieManager implements CookieContract { @@ -19,25 +22,25 @@ public function __construct( ) { } - public function has(string $key): bool + public function has(UnitEnum|string $key): bool { return ! is_null($this->get($key)); } - public function get(string $key, ?string $default = null): ?string + public function get(UnitEnum|string $key, ?string $default = null): ?string { if (! RequestContext::has()) { return null; } - return $this->request->cookie($key, $default); + return $this->request->cookie(enum_value($key), $default); } - public function make(string $name, string $value, int $minutes = 0, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): Cookie + public function make(UnitEnum|string $name, string $value, int $minutes = 0, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): Cookie { $time = ($minutes == 0) ? 0 : $this->availableAt($minutes * 60); - return new Cookie($name, $value, $time, $path, $domain, $secure, $httpOnly, $raw, $sameSite); + return new Cookie(enum_value($name), $value, $time, $path, $domain, $secure, $httpOnly, $raw, $sameSite); } public function queue(...$parameters): void @@ -51,13 +54,15 @@ public function queue(...$parameters): void $this->appendToQueue($cookie); } - public function expire(string $name, string $path = '', string $domain = ''): void + public function expire(UnitEnum|string $name, string $path = '', string $domain = ''): void { $this->queue($this->forget($name, $path, $domain)); } - public function unqueue(string $name, string $path = ''): void + public function unqueue(UnitEnum|string $name, string $path = ''): void { + $name = enum_value($name); + $cookies = $this->getQueuedCookies(); if ($path === '') { unset($cookies[$name]); @@ -93,12 +98,12 @@ protected function setQueueCookies(array $cookies): array return Context::set('http.cookies.queue', $cookies); } - public function forever(string $name, string $value, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): Cookie + public function forever(UnitEnum|string $name, string $value, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): Cookie { return $this->make($name, $value, 2628000, $path, $domain, $secure, $httpOnly, $raw, $sameSite); } - public function forget(string $name, string $path = '', string $domain = ''): Cookie + public function forget(UnitEnum|string $name, string $path = '', string $domain = ''): Cookie { return $this->make($name, '', -2628000, $path, $domain); } diff --git a/src/router/src/Middleware/ThrottleRequests.php b/src/router/src/Middleware/ThrottleRequests.php index 2bfafbfa6..c8900a8a9 100644 --- a/src/router/src/Middleware/ThrottleRequests.php +++ b/src/router/src/Middleware/ThrottleRequests.php @@ -19,6 +19,9 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use RuntimeException; +use UnitEnum; + +use function Hypervel\Support\enum_value; class ThrottleRequests implements MiddlewareInterface { @@ -45,9 +48,9 @@ public function __construct(RateLimiter $limiter) /** * Specify the named rate limiter to use for the middleware. */ - public static function using(string $name): string + public static function using(UnitEnum|string $name): string { - return static::class . ':' . $name; + return static::class . ':' . enum_value($name); } /** diff --git a/src/support/src/Facades/Cache.php b/src/support/src/Facades/Cache.php index 16c132fd1..1f5868f6b 100644 --- a/src/support/src/Facades/Cache.php +++ b/src/support/src/Facades/Cache.php @@ -43,7 +43,7 @@ * @method static bool putMany(array $values, int $seconds) * @method static bool flush() * @method static string getPrefix() - * @method static bool missing(string $key) + * @method static bool missing(\UnitEnum|string $key) * @method static bool supportsTags() * @method static int|null getDefaultCacheTime() * @method static \Hypervel\Cache\Repository setDefaultCacheTime(int|null $seconds) diff --git a/src/support/src/Facades/Cookie.php b/src/support/src/Facades/Cookie.php index b369ad228..74d61308a 100644 --- a/src/support/src/Facades/Cookie.php +++ b/src/support/src/Facades/Cookie.php @@ -7,15 +7,15 @@ use Hypervel\Cookie\Contracts\Cookie as CookieContract; /** - * @method static bool has(string $key) - * @method static string|null get(string $key, string|null $default = null) - * @method static \Hypervel\Cookie\Cookie make(string $name, string $value, int $minutes = 0, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, string|null $sameSite = null) + * @method static bool has(\UnitEnum|string $key) + * @method static string|null get(\UnitEnum|string $key, string|null $default = null) + * @method static \Hypervel\Cookie\Cookie make(\UnitEnum|string $name, string $value, int $minutes = 0, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, string|null $sameSite = null) * @method static void queue(mixed ...$parameters) - * @method static void expire(string $name, string $path = '', string $domain = '') - * @method static void unqueue(string $name, string $path = '') + * @method static void expire(\UnitEnum|string $name, string $path = '', string $domain = '') + * @method static void unqueue(\UnitEnum|string $name, string $path = '') * @method static array getQueuedCookies() - * @method static \Hypervel\Cookie\Cookie forever(string $name, string $value, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, string|null $sameSite = null) - * @method static \Hypervel\Cookie\Cookie forget(string $name, string $path = '', string $domain = '') + * @method static \Hypervel\Cookie\Cookie forever(\UnitEnum|string $name, string $value, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = true, bool $raw = false, string|null $sameSite = null) + * @method static \Hypervel\Cookie\Cookie forget(\UnitEnum|string $name, string $path = '', string $domain = '') * * @see \Hypervel\Cookie\CookieManager */ diff --git a/src/support/src/Facades/Gate.php b/src/support/src/Facades/Gate.php index ab7462b61..a71dadca3 100644 --- a/src/support/src/Facades/Gate.php +++ b/src/support/src/Facades/Gate.php @@ -7,21 +7,21 @@ use Hypervel\Auth\Contracts\Gate as GateContract; /** - * @method static bool has(array|string $ability) + * @method static bool has(\UnitEnum|array|string $ability) * @method static \Hypervel\Auth\Access\Response allowIf(\Closure|\Hypervel\Auth\Access\Response|bool $condition, string|null $message = null, string|null $code = null) * @method static \Hypervel\Auth\Access\Response denyIf(\Closure|\Hypervel\Auth\Access\Response|bool $condition, string|null $message = null, string|null $code = null) - * @method static \Hypervel\Auth\Access\Gate define(string $ability, callable|array|string $callback) + * @method static \Hypervel\Auth\Access\Gate define(\UnitEnum|string $ability, callable|array|string $callback) * @method static \Hypervel\Auth\Access\Gate resource(string $name, string $class, array|null $abilities = null) * @method static \Hypervel\Auth\Access\Gate policy(string $class, string $policy) * @method static \Hypervel\Auth\Access\Gate before(callable $callback) * @method static \Hypervel\Auth\Access\Gate after(callable $callback) - * @method static bool allows(string $ability, mixed $arguments = []) - * @method static bool denies(string $ability, mixed $arguments = []) - * @method static bool check(\Traversable|array|string $abilities, mixed $arguments = []) - * @method static bool any(\Traversable|array|string $abilities, mixed $arguments = []) - * @method static bool none(\Traversable|array|string $abilities, mixed $arguments = []) - * @method static \Hypervel\Auth\Access\Response authorize(string $ability, mixed $arguments = []) - * @method static \Hypervel\Auth\Access\Response inspect(string $ability, mixed $arguments = []) + * @method static bool allows(\UnitEnum|string $ability, mixed $arguments = []) + * @method static bool denies(\UnitEnum|string $ability, mixed $arguments = []) + * @method static bool check(\Traversable|\UnitEnum|array|string $abilities, mixed $arguments = []) + * @method static bool any(\Traversable|\UnitEnum|array|string $abilities, mixed $arguments = []) + * @method static bool none(\Traversable|\UnitEnum|array|string $abilities, mixed $arguments = []) + * @method static \Hypervel\Auth\Access\Response authorize(\UnitEnum|string $ability, mixed $arguments = []) + * @method static \Hypervel\Auth\Access\Response inspect(\UnitEnum|string $ability, mixed $arguments = []) * @method static mixed raw(string $ability, mixed $arguments = []) * @method static mixed|void getPolicyFor(object|string $class) * @method static mixed resolvePolicy(string $class) diff --git a/src/support/src/Facades/RateLimiter.php b/src/support/src/Facades/RateLimiter.php index 6891912cb..0a50d7a22 100644 --- a/src/support/src/Facades/RateLimiter.php +++ b/src/support/src/Facades/RateLimiter.php @@ -5,8 +5,8 @@ namespace Hypervel\Support\Facades; /** - * @method static \Hypervel\Cache\RateLimiter for(string $name, \Closure $callback) - * @method static \Closure|null limiter(string $name) + * @method static \Hypervel\Cache\RateLimiter for(\UnitEnum|string $name, \Closure $callback) + * @method static \Closure|null limiter(\UnitEnum|string $name) * @method static mixed attempt(string $key, int $maxAttempts, \Closure $callback, int $decaySeconds = 60) * @method static bool tooManyAttempts(string $key, int $maxAttempts) * @method static int hit(string $key, int $decaySeconds = 60) diff --git a/src/support/src/Facades/Redis.php b/src/support/src/Facades/Redis.php index ed557cb6d..286f0abaf 100644 --- a/src/support/src/Facades/Redis.php +++ b/src/support/src/Facades/Redis.php @@ -7,7 +7,7 @@ use Hypervel\Redis\Redis as RedisClient; /** - * @method static \Hypervel\Redis\RedisProxy connection(string $name = 'default') + * @method static \Hypervel\Redis\RedisProxy connection(\UnitEnum|string $name = 'default') * @method static void release() * @method static \Hypervel\Redis\RedisConnection shouldTransform(bool $shouldTransform = true) * @method static bool getShouldTransform() diff --git a/src/support/src/Facades/Schedule.php b/src/support/src/Facades/Schedule.php index f2e078a00..5291083d6 100644 --- a/src/support/src/Facades/Schedule.php +++ b/src/support/src/Facades/Schedule.php @@ -9,14 +9,14 @@ /** * @method static \Hypervel\Console\Scheduling\CallbackEvent call(callable|string $callback, array $parameters = []) * @method static \Hypervel\Console\Scheduling\Event command(string $command, array $parameters = []) - * @method static \Hypervel\Console\Scheduling\CallbackEvent job(object|string $job, string|null $queue = null, string|null $connection = null) + * @method static \Hypervel\Console\Scheduling\CallbackEvent job(object|string $job, \UnitEnum|string|null $queue = null, \UnitEnum|string|null $connection = null) * @method static \Hypervel\Console\Scheduling\Event exec(string $command, array $parameters = [], bool $isSystem = true) * @method static void group(\Closure $events) * @method static string compileArrayInput(string|int $key, array $value) * @method static bool serverShouldRun(\Hypervel\Console\Scheduling\Event $event, \DateTimeInterface $time) * @method static \Hyperf\Collection\Collection dueEvents(\Hypervel\Foundation\Contracts\Application $app) * @method static array events() - * @method static \Hypervel\Console\Scheduling\Schedule useCache(string|null $store) + * @method static \Hypervel\Console\Scheduling\Schedule useCache(\UnitEnum|string|null $store) * @method static mixed macroCall(string $method, array $parameters) * @method static void macro(string $name, callable|object $macro) * @method static void mixin(object $mixin, bool $replace = true) @@ -82,7 +82,7 @@ * @method static \Hypervel\Console\Scheduling\PendingEventAttributes yearly() * @method static \Hypervel\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') * @method static \Hypervel\Console\Scheduling\PendingEventAttributes days(array|mixed $days) - * @method static \Hypervel\Console\Scheduling\PendingEventAttributes timezone(\DateTimeZone|string $timezone) + * @method static \Hypervel\Console\Scheduling\PendingEventAttributes timezone(\DateTimeZone|\UnitEnum|string $timezone) * * @see \Hypervel\Console\Scheduling\Schedule */ diff --git a/src/support/src/Facades/Session.php b/src/support/src/Facades/Session.php index 6c2f7fcb6..29c57eb5b 100644 --- a/src/support/src/Facades/Session.php +++ b/src/support/src/Facades/Session.php @@ -27,27 +27,27 @@ * @method static array all() * @method static array only(array $keys) * @method static array except(array $keys) - * @method static bool exists(array|string $key) - * @method static bool missing(array|string $key) - * @method static bool has(array|string $key) - * @method static bool hasAny(array|string $key) - * @method static mixed get(string $key, mixed $default = null) - * @method static mixed pull(string $key, mixed $default = null) - * @method static bool hasOldInput(string|null $key = null) - * @method static mixed getOldInput(string|null $key = null, mixed $default = null) + * @method static bool exists(\UnitEnum|array|string $key) + * @method static bool missing(\UnitEnum|array|string $key) + * @method static bool has(\UnitEnum|array|string $key) + * @method static bool hasAny(\UnitEnum|array|string $key) + * @method static mixed get(\UnitEnum|string $key, mixed $default = null) + * @method static mixed pull(\UnitEnum|string $key, mixed $default = null) + * @method static bool hasOldInput(\UnitEnum|string|null $key = null) + * @method static mixed getOldInput(\UnitEnum|string|null $key = null, mixed $default = null) * @method static void replace(array $attributes) - * @method static void put(array|string $key, mixed $value = null) - * @method static mixed remember(string $key, \Closure $callback) - * @method static void push(string $key, mixed $value) - * @method static mixed increment(string $key, int $amount = 1) - * @method static int decrement(string $key, int $amount = 1) - * @method static void flash(string $key, mixed $value = true) - * @method static void now(string $key, mixed $value) + * @method static void put(\UnitEnum|array|string $key, mixed $value = null) + * @method static mixed remember(\UnitEnum|string $key, \Closure $callback) + * @method static void push(\UnitEnum|string $key, mixed $value) + * @method static mixed increment(\UnitEnum|string $key, int $amount = 1) + * @method static int decrement(\UnitEnum|string $key, int $amount = 1) + * @method static void flash(\UnitEnum|string $key, mixed $value = true) + * @method static void now(\UnitEnum|string $key, mixed $value) * @method static void reflash() * @method static void keep(array|mixed $keys = null) * @method static void flashInput(array $value) - * @method static mixed remove(string $key) - * @method static void forget(array|string $keys) + * @method static mixed remove(\UnitEnum|string $key) + * @method static void forget(\UnitEnum|array|string $keys) * @method static void flush() * @method static bool invalidate() * @method static bool regenerate(bool $destroy = false) diff --git a/src/support/src/Facades/Storage.php b/src/support/src/Facades/Storage.php index 50b05ba18..0e51566ec 100644 --- a/src/support/src/Facades/Storage.php +++ b/src/support/src/Facades/Storage.php @@ -8,10 +8,13 @@ use Hyperf\Contract\ConfigInterface; use Hypervel\Filesystem\Filesystem; use Hypervel\Filesystem\FilesystemManager; +use UnitEnum; + +use function Hypervel\Support\enum_value; /** - * @method static \Hypervel\Filesystem\Contracts\Filesystem drive(string|null $name = null) - * @method static \Hypervel\Filesystem\Contracts\Filesystem disk(string|null $name = null) + * @method static \Hypervel\Filesystem\Contracts\Filesystem drive(\UnitEnum|string|null $name = null) + * @method static \Hypervel\Filesystem\Contracts\Filesystem disk(\UnitEnum|string|null $name = null) * @method static \Hypervel\Filesystem\Contracts\Cloud cloud() * @method static \Hypervel\Filesystem\Contracts\Filesystem build(array|string $config) * @method static \Hypervel\Filesystem\Contracts\Filesystem createLocalDriver(array $config, string $name = 'local') @@ -125,9 +128,9 @@ class Storage extends Facade * * @return \Hypervel\Filesystem\Contracts\Filesystem */ - public static function fake(?string $disk = null, array $config = []) + public static function fake(UnitEnum|string|null $disk = null, array $config = []) { - $disk = $disk ?: ApplicationContext::getContainer() + $disk = enum_value($disk) ?: ApplicationContext::getContainer() ->get(ConfigInterface::class) ->get('filesystems.default'); @@ -149,9 +152,9 @@ public static function fake(?string $disk = null, array $config = []) * * @return \Hypervel\Filesystem\Contracts\Filesystem */ - public static function persistentFake(?string $disk = null, array $config = []) + public static function persistentFake(UnitEnum|string|null $disk = null, array $config = []) { - $disk = $disk ?: ApplicationContext::getContainer() + $disk = enum_value($disk) ?: ApplicationContext::getContainer() ->get(ConfigInterface::class) ->get('filesystems.default'); diff --git a/src/support/src/Js.php b/src/support/src/Js.php index bbfb089f0..7a068aef8 100644 --- a/src/support/src/Js.php +++ b/src/support/src/Js.php @@ -4,7 +4,6 @@ namespace Hypervel\Support; -use BackedEnum; use Hyperf\Contract\Arrayable; use Hyperf\Contract\Jsonable; use Hyperf\Stringable\Str; @@ -12,6 +11,7 @@ use JsonException; use JsonSerializable; use Stringable; +use UnitEnum; class Js implements Htmlable, Stringable { @@ -58,8 +58,8 @@ protected function convertDataToJavaScriptExpression(mixed $data, int $flags = 0 return $data->toHtml(); } - if ($data instanceof BackedEnum) { - $data = $data->value; + if ($data instanceof UnitEnum) { + $data = enum_value($data); } $json = static::encode($data, $flags, $depth); diff --git a/tests/Bus/BusPendingBatchTest.php b/tests/Bus/BusPendingBatchTest.php index 4ef350a00..7f0eb8aca 100644 --- a/tests/Bus/BusPendingBatchTest.php +++ b/tests/Bus/BusPendingBatchTest.php @@ -15,6 +15,24 @@ use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; use RuntimeException; +use TypeError; + +enum PendingBatchTestConnectionEnum: string +{ + case Redis = 'redis'; + case Database = 'database'; +} + +enum PendingBatchTestConnectionUnitEnum +{ + case sync; + case async; +} + +enum PendingBatchTestConnectionIntEnum: int +{ + case Primary = 1; +} /** * @internal @@ -219,6 +237,37 @@ public function testBatchBeforeEventIsCalled() $this->assertTrue($beforeCalled); } + public function testOnConnectionAcceptsStringBackedEnum(): void + { + $container = $this->getContainer(); + $pendingBatch = new PendingBatch($container, new Collection([])); + + $pendingBatch->onConnection(PendingBatchTestConnectionEnum::Redis); + + $this->assertSame('redis', $pendingBatch->connection()); + } + + public function testOnConnectionAcceptsUnitEnum(): void + { + $container = $this->getContainer(); + $pendingBatch = new PendingBatch($container, new Collection([])); + + $pendingBatch->onConnection(PendingBatchTestConnectionUnitEnum::sync); + + $this->assertSame('sync', $pendingBatch->connection()); + } + + public function testOnConnectionWithIntBackedEnumThrowsTypeError(): void + { + $this->expectException(TypeError::class); + + $container = $this->getContainer(); + $pendingBatch = new PendingBatch($container, new Collection([])); + + $pendingBatch->onConnection(PendingBatchTestConnectionIntEnum::Primary); + $pendingBatch->connection(); // TypeError thrown here on return type mismatch + } + protected function getContainer(array $bindings = []): Container { return new Container( diff --git a/tests/Cookie/CookieManagerTest.php b/tests/Cookie/CookieManagerTest.php index 11664e84e..07abcff93 100644 --- a/tests/Cookie/CookieManagerTest.php +++ b/tests/Cookie/CookieManagerTest.php @@ -11,6 +11,24 @@ use Hypervel\Tests\TestCase; use Mockery as m; use Swow\Psr7\Message\ServerRequestPlusInterface; +use TypeError; + +enum CookieManagerTestNameEnum: string +{ + case Session = 'session_id'; + case Remember = 'remember_token'; +} + +enum CookieManagerTestNameUnitEnum +{ + case theme; + case locale; +} + +enum CookieManagerTestNameIntEnum: int +{ + case First = 1; +} /** * @internal @@ -114,4 +132,139 @@ public function tesetUnqueueWithPath() $manager->unqueue('foo', 'bar'); } + + // ========================================================================= + // Enum Support Tests + // ========================================================================= + + public function testHasAcceptsStringBackedEnum(): void + { + $request = m::mock(RequestInterface::class); + $request->shouldReceive('cookie')->with('session_id', null)->andReturn('abc123'); + + RequestContext::set(m::mock(ServerRequestPlusInterface::class), null); + + $manager = new CookieManager($request); + + $this->assertTrue($manager->has(CookieManagerTestNameEnum::Session)); + } + + public function testHasAcceptsUnitEnum(): void + { + $request = m::mock(RequestInterface::class); + $request->shouldReceive('cookie')->with('theme', null)->andReturn('dark'); + + RequestContext::set(m::mock(ServerRequestPlusInterface::class), null); + + $manager = new CookieManager($request); + + $this->assertTrue($manager->has(CookieManagerTestNameUnitEnum::theme)); + } + + public function testGetAcceptsStringBackedEnum(): void + { + $request = m::mock(RequestInterface::class); + $request->shouldReceive('cookie')->with('session_id', null)->andReturn('abc123'); + + RequestContext::set(m::mock(ServerRequestPlusInterface::class), null); + + $manager = new CookieManager($request); + + $this->assertSame('abc123', $manager->get(CookieManagerTestNameEnum::Session)); + } + + public function testGetAcceptsUnitEnum(): void + { + $request = m::mock(RequestInterface::class); + $request->shouldReceive('cookie')->with('theme', null)->andReturn('dark'); + + RequestContext::set(m::mock(ServerRequestPlusInterface::class), null); + + $manager = new CookieManager($request); + + $this->assertSame('dark', $manager->get(CookieManagerTestNameUnitEnum::theme)); + } + + public function testMakeAcceptsStringBackedEnum(): void + { + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->make(CookieManagerTestNameEnum::Session, 'abc123'); + + $this->assertInstanceOf(Cookie::class, $cookie); + $this->assertSame('session_id', $cookie->getName()); + $this->assertSame('abc123', $cookie->getValue()); + } + + public function testMakeAcceptsUnitEnum(): void + { + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->make(CookieManagerTestNameUnitEnum::theme, 'dark'); + + $this->assertInstanceOf(Cookie::class, $cookie); + $this->assertSame('theme', $cookie->getName()); + $this->assertSame('dark', $cookie->getValue()); + } + + public function testForeverAcceptsStringBackedEnum(): void + { + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->forever(CookieManagerTestNameEnum::Remember, 'token123'); + + $this->assertInstanceOf(Cookie::class, $cookie); + $this->assertSame('remember_token', $cookie->getName()); + $this->assertSame('token123', $cookie->getValue()); + } + + public function testForeverAcceptsUnitEnum(): void + { + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->forever(CookieManagerTestNameUnitEnum::locale, 'en'); + + $this->assertInstanceOf(Cookie::class, $cookie); + $this->assertSame('locale', $cookie->getName()); + $this->assertSame('en', $cookie->getValue()); + } + + public function testForgetAcceptsStringBackedEnum(): void + { + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->forget(CookieManagerTestNameEnum::Session); + + $this->assertInstanceOf(Cookie::class, $cookie); + $this->assertSame('session_id', $cookie->getName()); + $this->assertSame('', $cookie->getValue()); + } + + public function testForgetAcceptsUnitEnum(): void + { + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->forget(CookieManagerTestNameUnitEnum::theme); + + $this->assertInstanceOf(Cookie::class, $cookie); + $this->assertSame('theme', $cookie->getName()); + $this->assertSame('', $cookie->getValue()); + } + + public function testMakeWithIntBackedEnumThrowsTypeError(): void + { + $this->expectException(TypeError::class); + + $request = m::mock(RequestInterface::class); + + $manager = new CookieManager($request); + $cookie = $manager->make(CookieManagerTestNameIntEnum::First, 'value'); + $cookie->getName(); // TypeError thrown here + } } diff --git a/tests/Router/Middleware/ThrottleRequestsTest.php b/tests/Router/Middleware/ThrottleRequestsTest.php new file mode 100644 index 000000000..0059c7239 --- /dev/null +++ b/tests/Router/Middleware/ThrottleRequestsTest.php @@ -0,0 +1,61 @@ +assertSame(ThrottleRequests::class . ':api', $result); + } + + public function testUsingWithStringBackedEnum(): void + { + $result = ThrottleRequests::using(ThrottleRequestsTestLimiterEnum::Api); + + $this->assertSame(ThrottleRequests::class . ':api', $result); + } + + public function testUsingWithUnitEnum(): void + { + $result = ThrottleRequests::using(ThrottleRequestsTestLimiterUnitEnum::uploads); + + $this->assertSame(ThrottleRequests::class . ':uploads', $result); + } + + public function testUsingWithIntBackedEnumCoercesToString(): void + { + // PHP implicitly converts int to string in concatenation + $result = ThrottleRequests::using(ThrottleRequestsTestLimiterIntEnum::Default); + + $this->assertSame(ThrottleRequests::class . ':1', $result); + } +} diff --git a/tests/Support/JsTest.php b/tests/Support/JsTest.php new file mode 100644 index 000000000..200258061 --- /dev/null +++ b/tests/Support/JsTest.php @@ -0,0 +1,89 @@ +assertSame("'active'", (string) $js); + } + + public function testFromWithUnitEnum(): void + { + $js = Js::from(JsTestUnitEnum::pending); + + $this->assertSame("'pending'", (string) $js); + } + + public function testFromWithIntBackedEnum(): void + { + $js = Js::from(JsTestIntEnum::One); + + $this->assertSame('1', (string) $js); + } + + public function testFromWithString(): void + { + $js = Js::from('hello'); + + $this->assertSame("'hello'", (string) $js); + } + + public function testFromWithInteger(): void + { + $js = Js::from(42); + + $this->assertSame('42', (string) $js); + } + + public function testFromWithArray(): void + { + $js = Js::from(['foo' => 'bar']); + + $this->assertStringContainsString('JSON.parse', (string) $js); + } + + public function testFromWithNull(): void + { + $js = Js::from(null); + + $this->assertSame('null', (string) $js); + } + + public function testFromWithBoolean(): void + { + $js = Js::from(true); + + $this->assertSame('true', (string) $js); + } +} From ea3c509a0bcce1a29e4bd82a5140dfb1f13b2e11 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 19 Jan 2026 04:42:54 +0000 Subject: [PATCH 19/19] Add missing Str import to HasApiTokens --- src/sanctum/src/HasApiTokens.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sanctum/src/HasApiTokens.php b/src/sanctum/src/HasApiTokens.php index fab12e442..83c57e909 100644 --- a/src/sanctum/src/HasApiTokens.php +++ b/src/sanctum/src/HasApiTokens.php @@ -7,6 +7,7 @@ use DateTimeInterface; use Hyperf\Database\Model\Relations\MorphMany; use Hypervel\Sanctum\Contracts\HasAbilities; +use Hypervel\Support\Str; use UnitEnum; use function Hypervel\Support\enum_value;