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/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..e8741f0c9 100644 --- a/src/auth/src/Access/Gate.php +++ b/src/auth/src/Access/Gate.php @@ -20,6 +20,9 @@ use ReflectionException; use ReflectionFunction; use ReflectionParameter; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Gate implements GateContract { @@ -58,12 +61,12 @@ public function __construct( /** * Determine if a given ability has been defined. */ - public function has(array|string $ability): bool + public function has(array|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 +123,10 @@ protected function authorizeOnDemand(bool|Closure|Response $condition, ?string $ * * @throws InvalidArgumentException */ - public function define(string $ability, array|callable|string $callback): static + public function define(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 +232,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(UnitEnum|string $ability, mixed $arguments = []): bool { return $this->check($ability, $arguments); } @@ -235,7 +240,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(UnitEnum|string $ability, mixed $arguments = []): bool { return ! $this->allows($ability, $arguments); } @@ -243,7 +248,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|UnitEnum|string $abilities, mixed $arguments = []): bool { return collect($abilities)->every( fn ($ability) => $this->inspect($ability, $arguments)->allowed() @@ -253,7 +258,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|UnitEnum|string $abilities, mixed $arguments = []): bool { return collect($abilities)->contains(fn ($ability) => $this->check($ability, $arguments)); } @@ -261,7 +266,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|UnitEnum|string $abilities, mixed $arguments = []): bool { return ! $this->any($abilities, $arguments); } @@ -271,7 +276,7 @@ public function none(iterable|string $abilities, mixed $arguments = []): bool * * @throws AuthorizationException */ - public function authorize(string $ability, mixed $arguments = []): Response + public function authorize(UnitEnum|string $ability, mixed $arguments = []): Response { return $this->inspect($ability, $arguments)->authorize(); } @@ -279,10 +284,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(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..a3ea09739 100644 --- a/src/auth/src/Contracts/Gate.php +++ b/src/auth/src/Contracts/Gate.php @@ -7,18 +7,19 @@ 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|UnitEnum|string $ability): bool; /** * Define a new ability. */ - public function define(string $ability, callable|string $callback): static; + public function define(UnitEnum|string $ability, callable|string $callback): static; /** * Define abilities for a resource. @@ -43,34 +44,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(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(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|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|UnitEnum|string $abilities, mixed $arguments = []): bool; + + /** + * Determine if all of the given abilities should be denied for the current user. + */ + 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(string $ability, mixed $arguments = []): Response; + public function authorize(UnitEnum|string $ability, mixed $arguments = []): Response; /** * Inspect the user for the given ability. */ - public function inspect(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 ec842d893..fa07c1aa5 100644 --- a/src/auth/src/Middleware/Authorize.php +++ b/src/auth/src/Middleware/Authorize.php @@ -13,6 +13,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 +31,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(UnitEnum|string $ability, string ...$models): string { - return static::class . ':' . implode(',', [$ability, ...$models]); + return static::class . ':' . implode(',', [enum_value($ability), ...$models]); } /** diff --git a/src/broadcasting/src/InteractsWithBroadcasting.php b/src/broadcasting/src/InteractsWithBroadcasting.php index 418e7a591..783149f8b 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 : 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/bus/src/PendingBatch.php b/src/bus/src/PendingBatch.php index 0f5524bee..085684c74 100644 --- a/src/bus/src/PendingBatch.php +++ b/src/bus/src/PendingBatch.php @@ -4,7 +4,6 @@ namespace Hypervel\Bus; -use BackedEnum; use Closure; use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; @@ -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; @@ -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; } @@ -212,7 +212,7 @@ 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 { $this->options['queue'] = enum_value($queue); diff --git a/src/bus/src/PendingChain.php b/src/bus/src/PendingChain.php index 8b86c5e30..bcb884033 100644 --- a/src/bus/src/PendingChain.php +++ b/src/bus/src/PendingChain.php @@ -4,7 +4,6 @@ namespace Hypervel\Bus; -use BackedEnum; use Closure; use DateInterval; use DateTimeInterface; @@ -13,6 +12,7 @@ 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; @@ -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; } @@ -66,7 +66,7 @@ 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 { $this->queue = enum_value($queue); diff --git a/src/bus/src/PendingDispatch.php b/src/bus/src/PendingDispatch.php index b7318bc80..6649cd1b7 100644 --- a/src/bus/src/PendingDispatch.php +++ b/src/bus/src/PendingDispatch.php @@ -4,13 +4,13 @@ namespace Hypervel\Bus; -use BackedEnum; use DateInterval; use DateTimeInterface; use Hyperf\Context\ApplicationContext; use Hypervel\Bus\Contracts\Dispatcher; use Hypervel\Cache\Contracts\Factory as CacheFactory; use Hypervel\Queue\Contracts\ShouldBeUnique; +use UnitEnum; class PendingDispatch { @@ -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 8b2693cd4..a27e9fed5 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 : $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 : $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 : $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 : $value; $this->chainQueue = $resolvedQueue; $this->queue = $resolvedQueue; diff --git a/src/cache/src/RateLimiter.php b/src/cache/src/RateLimiter.php index 8a61238b9..1f66940ab 100644 --- a/src/cache/src/RateLimiter.php +++ b/src/cache/src/RateLimiter.php @@ -7,6 +7,9 @@ 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 +36,9 @@ public function __construct(Cache $cache) /** * Register a named limiter configuration. */ - public function for(string $name, Closure $callback): static + public function for(UnitEnum|string $name, Closure $callback): static { - $this->limiters[$name] = $callback; + $this->limiters[$this->resolveLimiterName($name)] = $callback; return $this; } @@ -43,9 +46,17 @@ public function for(string $name, Closure $callback): static /** * Get the given named rate limiter. */ - public function limiter(string $name): ?Closure + public function limiter(UnitEnum|string $name): ?Closure + { + return $this->limiters[$this->resolveLimiterName($name)] ?? null; + } + + /** + * Resolve the rate limiter name. + */ + private function resolveLimiterName(UnitEnum|string $name): string { - return $this->limiters[$name] ?? null; + return enum_value($name); } /** diff --git a/src/cache/src/RedisTaggedCache.php b/src/cache/src/RedisTaggedCache.php index ae678f86b..89cdcc311 100644 --- a/src/cache/src/RedisTaggedCache.php +++ b/src/cache/src/RedisTaggedCache.php @@ -7,6 +7,9 @@ use DateInterval; use DateTimeInterface; use Hypervel\Cache\Contracts\Store; +use UnitEnum; + +use function Hypervel\Support\enum_value; class RedisTaggedCache extends TaggedCache { @@ -27,8 +30,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(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 +45,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|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 +68,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(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 +80,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(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 +92,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(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..3a469848d 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -29,6 +29,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 +95,7 @@ public function __clone() /** * Determine if an item exists in the cache. */ - public function has(array|string $key): bool + public function has(array|UnitEnum|string $key): bool { return ! is_null($this->get($key)); } @@ -100,7 +103,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(UnitEnum|string $key): bool { return ! $this->has($key); } @@ -114,12 +117,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|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 +182,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(UnitEnum|string $key, mixed $default = null): mixed { return tap($this->get($key, $default), function () use ($key) { $this->forget($key); @@ -187,12 +192,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|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 +225,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(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { return $this->put($key, $value, $ttl); } @@ -261,8 +268,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(UnitEnum|string $key, mixed $value, DateInterval|DateTimeInterface|int|null $ttl = null): bool { + $key = enum_value($key); + $seconds = null; if ($ttl !== null) { @@ -297,24 +306,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(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(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(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 +348,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(UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl, Closure $callback): mixed { $value = $this->get($key); @@ -362,7 +373,7 @@ public function remember(string $key, DateInterval|DateTimeInterface|int|null $t * * @return TCacheValue */ - public function sear(string $key, Closure $callback): mixed + public function sear(UnitEnum|string $key, Closure $callback): mixed { return $this->rememberForever($key, $callback); } @@ -376,7 +387,7 @@ public function sear(string $key, Closure $callback): mixed * * @return TCacheValue */ - public function rememberForever(string $key, Closure $callback): mixed + public function rememberForever(UnitEnum|string $key, Closure $callback): mixed { $value = $this->get($key); @@ -395,8 +406,10 @@ public function rememberForever(string $key, Closure $callback): mixed /** * Remove an item from the cache. */ - public function forget(string $key): bool + public function forget(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 +421,7 @@ public function forget(string $key): bool }); } - public function delete(string $key): bool + public function delete(UnitEnum|string $key): bool { return $this->forget($key); } @@ -452,8 +465,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 +539,7 @@ public function getName(): ?string /** * Determine if a cached value exists. * - * @param string $key + * @param string|UnitEnum $key */ public function offsetExists($key): bool { @@ -533,7 +549,7 @@ public function offsetExists($key): bool /** * Retrieve an item from the cache by key. * - * @param string $key + * @param string|UnitEnum $key */ public function offsetGet($key): mixed { @@ -543,7 +559,7 @@ public function offsetGet($key): mixed /** * Store an item in the cache for the default time. * - * @param string $key + * @param string|UnitEnum $key * @param mixed $value */ public function offsetSet($key, $value): void @@ -554,7 +570,7 @@ public function offsetSet($key, $value): void /** * Remove an item from the cache. * - * @param string $key + * @param string|UnitEnum $key */ public function offsetUnset($key): void { diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php index ba70fb5df..a273136ef 100644 --- a/src/cache/src/TaggedCache.php +++ b/src/cache/src/TaggedCache.php @@ -9,6 +9,9 @@ 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 +49,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(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(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/src/console/src/Scheduling/ManagesFrequencies.php b/src/console/src/Scheduling/ManagesFrequencies.php index 8c3c3c34f..6f70ca29b 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 + ? enum_value($timezone) + : $timezone; return $this; } diff --git a/src/console/src/Scheduling/Schedule.php b/src/console/src/Scheduling/Schedule.php index 1b9493ebe..f79b428dd 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 : enum_value($queue); + $connection = is_null($connection) ? null : 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 = enum_value($store); + if ($this->eventMutex instanceof CacheAware) { $this->eventMutex->useStore($store); } 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/core/src/Context/Context.php b/src/core/src/Context/Context.php index 9489ecdee..07d0331cf 100644 --- a/src/core/src/Context/Context.php +++ b/src/core/src/Context/Context.php @@ -4,8 +4,12 @@ namespace Hypervel\Context; +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 +20,54 @@ public function __call(string $method, array $arguments): mixed return static::{$method}(...$arguments); } + /** + * Set a value in the context. + */ + public static function set(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(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(UnitEnum|string $id, ?int $coroutineId = null): bool + { + return parent::has(enum_value($id), $coroutineId); + } + + /** + * Remove a value from the context. + */ + public static function destroy(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(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(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/src/core/src/Database/Eloquent/Factories/Factory.php b/src/core/src/Database/Eloquent/Factories/Factory.php index 959fe449f..119d8735b 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 : $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..40244d4bc 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 : $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..122fde24d 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 : $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..533087f2f 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 : $value; + + return $this; + } + /** * Delete the pivot model record from the database. * 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/event/src/EventDispatcher.php b/src/event/src/EventDispatcher.php index 70f834afa..d6be8df42 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 : 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..b228622df 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 : 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 : 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 = enum_value($group); return $this; } 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/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/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/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..b5c2d1acc 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,12 +33,12 @@ 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); - $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 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/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/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..83c57e909 100644 --- a/src/sanctum/src/HasApiTokens.php +++ b/src/sanctum/src/HasApiTokens.php @@ -4,11 +4,13 @@ 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 +37,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 +45,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 +53,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/src/session/src/Contracts/Session.php b/src/session/src/Contracts/Session.php index 4717c1d53..a9fdbc160 100644 --- a/src/session/src/Contracts/Session.php +++ b/src/session/src/Contracts/Session.php @@ -5,6 +5,7 @@ namespace Hypervel\Session\Contracts; use SessionHandlerInterface; +use UnitEnum; 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|UnitEnum|string $key): bool; /** * Checks if a key is present and not null. */ - public function has(array|string $key): bool; + public function has(array|UnitEnum|string $key): bool; /** * Get an item from the session. */ - public function get(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(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|string $key, mixed $value = null): void; + public function put(array|UnitEnum|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(UnitEnum|string $key): mixed; /** * Remove one or many items from the session. */ - public function forget(array|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 10c7107e7..13c668ed4 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -6,15 +6,17 @@ 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; +use UnitEnum; + +use function Hypervel\Support\enum_value; class Store implements Session { @@ -203,9 +205,7 @@ public function all(): array */ public function only(array $keys): array { - $attributes = $this->getAttributes(); - - return Arr::only($attributes, $keys); + return Arr::only($this->getAttributes(), array_map(enum_value(...), $keys)); } /** @@ -213,19 +213,17 @@ public function only(array $keys): array */ public function except(array $keys): array { - $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|string $key): bool + public function exists(array|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; }); } @@ -233,7 +231,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|UnitEnum|string $key): bool { return ! $this->exists($key); } @@ -241,9 +239,9 @@ 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|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)); }); } @@ -251,9 +249,9 @@ 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|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; } @@ -261,20 +259,18 @@ 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(UnitEnum|string $key, mixed $default = null): mixed { - $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(string $key, mixed $default = null): mixed + public function pull(UnitEnum|string $key, mixed $default = null): mixed { $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, $key, $default); + $result = Arr::pull($attributes, enum_value($key), $default); $this->setAttributes($attributes); @@ -284,7 +280,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(UnitEnum|string|null $key = null): bool { $old = $this->getOldInput($key); @@ -294,9 +290,9 @@ 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(UnitEnum|string|null $key = null, mixed $default = null): mixed { - return Arr::get($this->get('_old_input', []), $key, $default); + return Arr::get($this->get('_old_input', []), enum_value($key), $default); } /** @@ -310,15 +306,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|UnitEnum|string $key, mixed $value = null): void { if (! is_array($key)) { - $key = [$key => $value]; + $key = [enum_value($key) => $value]; } $attributes = $this->getAttributes(); foreach ($key as $arrayKey => $arrayValue) { - Arr::set($attributes, $arrayKey, $arrayValue); + Arr::set($attributes, enum_value($arrayKey), $arrayValue); } $this->setAttributes($attributes); @@ -327,7 +323,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(UnitEnum|string $key, Closure $callback): mixed { if (! is_null($value = $this->get($key))) { return $value; @@ -341,7 +337,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(UnitEnum|string $key, mixed $value): void { $array = $this->get($key, []); @@ -353,7 +349,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(UnitEnum|string $key, int $amount = 1): mixed { $this->put($key, $value = $this->get($key, 0) + $amount); @@ -363,7 +359,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(UnitEnum|string $key, int $amount = 1): int { return $this->increment($key, $amount * -1); } @@ -371,8 +367,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(UnitEnum|string $key, mixed $value = true): void { + $key = enum_value($key); + $this->put($key, $value); $this->push('_flash.new', $key); @@ -383,8 +381,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(UnitEnum|string $key, mixed $value): void { + $key = enum_value($key); + $this->put($key, $value); $this->push('_flash.old', $key); @@ -441,10 +441,10 @@ public function flashInput(array $value): void /** * Remove an item from the session, returning its value. */ - public function remove(string $key): mixed + public function remove(UnitEnum|string $key): mixed { $attributes = $this->getAttributes(); - $result = Arr::pull($attributes, $key); + $result = Arr::pull($attributes, enum_value($key)); $this->setAttributes($attributes); @@ -454,10 +454,10 @@ 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|UnitEnum|string $keys): void { $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/Collection.php b/src/support/src/Collection.php index e23980f94..73f9285c8 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,156 @@ 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); + } + + /** + * 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. + * + * @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/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/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/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/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/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/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/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) { diff --git a/tests/Auth/Access/GateEnumTest.php b/tests/Auth/Access/GateEnumTest.php new file mode 100644 index 000000000..127e25db5 --- /dev/null +++ b/tests/Auth/Access/GateEnumTest.php @@ -0,0 +1,350 @@ +getBasicGate(); + + $gate->define(GateEnumTestAbilitiesBackedEnum::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(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + // UnitEnum uses ->name, so key is 'ManageUsers' + $this->assertTrue($gate->allows('ManageUsers')); + } + + public function testDefineWithIntBackedEnumStoresUnderIntKey(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesIntBackedEnum::CreatePost, fn ($user) => true); + + // Int value 1 is used as ability key - can check with string '1' + $this->assertTrue($gate->allows('1')); + } + + 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); + } + + // ========================================================================= + // allows() with enums + // ========================================================================= + + public function testAllowsWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $this->assertTrue($gate->allows(GateEnumTestAbilitiesBackedEnum::ViewDashboard)); + } + + public function testAllowsWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->allows(GateEnumTestAbilitiesUnitEnum::ManageUsers)); + } + + // ========================================================================= + // denies() with enums + // ========================================================================= + + public function testDeniesWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => false); + + $this->assertTrue($gate->denies(GateEnumTestAbilitiesBackedEnum::ViewDashboard)); + } + + public function testDeniesWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => false); + + $this->assertTrue($gate->denies(GateEnumTestAbilitiesUnitEnum::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(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $this->assertTrue($gate->check(['allow_1', 'allow_2', GateEnumTestAbilitiesBackedEnum::ViewDashboard])); + } + + public function testCheckWithArrayContainingUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('allow_1', fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->check(['allow_1', GateEnumTestAbilitiesUnitEnum::ManageUsers])); + } + + // ========================================================================= + // any() with enums + // ========================================================================= + + public function testAnyWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->policy(AccessGateTestDummy::class, AccessGateTestPolicyWithAllPermissions::class); + + $this->assertTrue($gate->any(['edit', GateEnumTestAbilitiesBackedEnum::Update], new AccessGateTestDummy())); + } + + public function testAnyWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('deny', fn ($user) => false); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->any(['deny', GateEnumTestAbilitiesUnitEnum::ManageUsers])); + } + + // ========================================================================= + // none() with enums + // ========================================================================= + + public function testNoneWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->policy(AccessGateTestDummy::class, AccessGateTestPolicyWithNoPermissions::class); + + $this->assertTrue($gate->none(['edit', GateEnumTestAbilitiesBackedEnum::Update], new AccessGateTestDummy())); + } + + public function testNoneWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define('deny_1', fn ($user) => false); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => false); + + $this->assertTrue($gate->none(['deny_1', GateEnumTestAbilitiesUnitEnum::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(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $this->assertTrue($gate->has(GateEnumTestAbilitiesBackedEnum::ViewDashboard)); + $this->assertFalse($gate->has(GateEnumTestAbilitiesBackedEnum::Update)); + } + + public function testHasWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->has(GateEnumTestAbilitiesUnitEnum::ManageUsers)); + $this->assertFalse($gate->has(GateEnumTestAbilitiesUnitEnum::ViewReports)); + } + + public function testHasWithArrayContainingEnums(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $this->assertTrue($gate->has([GateEnumTestAbilitiesBackedEnum::ViewDashboard, GateEnumTestAbilitiesUnitEnum::ManageUsers])); + $this->assertFalse($gate->has([GateEnumTestAbilitiesBackedEnum::ViewDashboard, GateEnumTestAbilitiesBackedEnum::Update])); + } + + // ========================================================================= + // authorize() with enums + // ========================================================================= + + public function testAuthorizeWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $response = $gate->authorize(GateEnumTestAbilitiesBackedEnum::ViewDashboard); + + $this->assertTrue($response->allowed()); + } + + public function testAuthorizeWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => true); + + $response = $gate->authorize(GateEnumTestAbilitiesUnitEnum::ManageUsers); + + $this->assertTrue($response->allowed()); + } + + // ========================================================================= + // inspect() with enums + // ========================================================================= + + public function testInspectWithBackedEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesBackedEnum::ViewDashboard, fn ($user) => true); + + $response = $gate->inspect(GateEnumTestAbilitiesBackedEnum::ViewDashboard); + + $this->assertTrue($response->allowed()); + } + + public function testInspectWithUnitEnum(): void + { + $gate = $this->getBasicGate(); + + $gate->define(GateEnumTestAbilitiesUnitEnum::ManageUsers, fn ($user) => false); + + $response = $gate->inspect(GateEnumTestAbilitiesUnitEnum::ManageUsers); + + $this->assertFalse($response->allowed()); + } + + // ========================================================================= + // Interoperability tests + // ========================================================================= + + public function testBackedEnumAndStringInteroperability(): void + { + $gate = $this->getBasicGate(); + + // Define with enum + $gate->define(GateEnumTestAbilitiesBackedEnum::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(GateEnumTestAbilitiesBackedEnum::Update)); + } + + public function testUnitEnumAndStringInteroperability(): void + { + $gate = $this->getBasicGate(); + + // Define with enum + $gate->define(GateEnumTestAbilitiesUnitEnum::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(GateEnumTestAbilitiesUnitEnum::ViewReports)); + } + + // ========================================================================= + // Helper methods + // ========================================================================= + + protected function getBasicGate(bool $isGuest = false): Gate + { + $container = new Container(new DefinitionSource([])); + + return new Gate( + $container, + fn () => $isGuest ? null : new AccessGateTestAuthenticatable() + ); + } +} 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 new file mode 100644 index 000000000..d0bdc0483 --- /dev/null +++ b/tests/Broadcasting/InteractsWithBroadcastingTest.php @@ -0,0 +1,127 @@ +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 testBroadcastViaWithIntBackedEnumStoresIntValue(): void + { + $event = new TestBroadcastingEvent(); + + $event->broadcastVia(InteractsWithBroadcastingTestConnectionIntEnum::Connection1); + + // 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 + { + $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); + } + + 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 new file mode 100644 index 000000000..ddaccf978 --- /dev/null +++ b/tests/Broadcasting/PendingBroadcastTest.php @@ -0,0 +1,126 @@ +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 testViaWithIntBackedEnumThrowsTypeErrorAtBroadcastTime(): void + { + $event = new TestPendingBroadcastableEvent(); + $event->broadcastVia(PendingBroadcastTestConnectionIntEnum::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); + } + + 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; +} + +class TestPendingBroadcastableEvent +{ + use InteractsWithBroadcasting; + + public function broadcastOn(): Channel + { + return new Channel('test-channel'); + } +} 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/Bus/QueueableTest.php b/tests/Bus/QueueableTest.php index 2481a64f5..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 @@ -37,12 +38,28 @@ 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 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 { @@ -66,11 +83,27 @@ 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 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 @@ -84,8 +117,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/Cache/CacheRedisTaggedCacheTest.php b/tests/Cache/CacheRedisTaggedCacheTest.php index 5f87b76ab..d4e575111 100644 --- a/tests/Cache/CacheRedisTaggedCacheTest.php +++ b/tests/Cache/CacheRedisTaggedCacheTest.php @@ -11,6 +11,22 @@ use Hypervel\Tests\TestCase; use Mockery as m; use Mockery\MockInterface; +use TypeError; + +enum RedisTaggedCacheTestKeyStringEnum: string +{ + case Counter = 'counter'; +} + +enum RedisTaggedCacheTestKeyIntEnum: int +{ + case Key1 = 1; +} + +enum RedisTaggedCacheTestKeyUnitEnum +{ + case hits; +} /** * @internal @@ -158,6 +174,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 new file mode 100644 index 000000000..c04dbe149 --- /dev/null +++ b/tests/Cache/CacheRepositoryEnumTest.php @@ -0,0 +1,419 @@ +getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('cached-value'); + + $this->assertSame('cached-value', $repo->get(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); + } + + public function testGetWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn('dashboard-data'); + + $this->assertSame('dashboard-data', $repo->get(CacheRepositoryEnumTestKeyUnitEnum::Dashboard)); + } + + public function testGetWithIntBackedEnumThrowsTypeError(): void + { + $repo = $this->getRepository(); + + // Int-backed enum causes TypeError because store expects string key + $this->expectException(TypeError::class); + $repo->get(CacheRepositoryEnumTestKeyIntBackedEnum::Counter); + } + + public function testHasWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('user-profile')->andReturn('value'); + + $this->assertTrue($repo->has(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); + } + + public function testHasWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('Dashboard')->andReturn(null); + + $this->assertFalse($repo->has(CacheRepositoryEnumTestKeyUnitEnum::Dashboard)); + } + + public function testMissingWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('settings')->andReturn(null); + + $this->assertTrue($repo->missing(CacheRepositoryEnumTestKeyBackedEnum::Settings)); + } + + public function testPutWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('put')->once()->with('user-profile', 'value', 60)->andReturn(true); + + $this->assertTrue($repo->put(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyUnitEnum::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(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyUnitEnum::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(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); + } + + public function testIncrementWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('increment')->once()->with('Dashboard', 5)->andReturn(10); + + $this->assertSame(10, $repo->increment(CacheRepositoryEnumTestKeyUnitEnum::Dashboard, 5)); + } + + public function testDecrementWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('decrement')->once()->with('settings', 1)->andReturn(4); + + $this->assertSame(4, $repo->decrement(CacheRepositoryEnumTestKeyBackedEnum::Settings)); + } + + public function testDecrementWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('decrement')->once()->with('Analytics', 3)->andReturn(7); + + $this->assertSame(7, $repo->decrement(CacheRepositoryEnumTestKeyUnitEnum::Analytics, 3)); + } + + public function testForeverWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forever')->once()->with('user-profile', 'permanent')->andReturn(true); + + $this->assertTrue($repo->forever(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'permanent')); + } + + public function testForeverWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forever')->once()->with('Dashboard', 'forever-data')->andReturn(true); + + $this->assertTrue($repo->forever(CacheRepositoryEnumTestKeyUnitEnum::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(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyUnitEnum::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(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyUnitEnum::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(CacheRepositoryEnumTestKeyBackedEnum::UserProfile)); + } + + public function testForgetWithUnitEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forget')->once()->with('Dashboard')->andReturn(true); + + $this->assertTrue($repo->forget(CacheRepositoryEnumTestKeyUnitEnum::Dashboard)); + } + + public function testDeleteWithBackedEnum(): void + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('forget')->once()->with('settings')->andReturn(true); + + $this->assertTrue($repo->delete(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyUnitEnum::Analytics)); + } + + public function testBackedEnumAndStringInteroperability(): void + { + $repo = new Repository(new ArrayStore()); + + // Store with enum + $repo->put(CacheRepositoryEnumTestKeyBackedEnum::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(CacheRepositoryEnumTestKeyBackedEnum::Settings)); + } + + public function testUnitEnumAndStringInteroperability(): void + { + $repo = new Repository(new ArrayStore()); + + // Store with enum + $repo->put(CacheRepositoryEnumTestKeyUnitEnum::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(CacheRepositoryEnumTestKeyUnitEnum::Analytics)); + } + + public function testTagsWithBackedEnumArray(): void + { + $repo = new Repository(new ArrayStore()); + + $tagged = $repo->tags([CacheRepositoryEnumTestTagBackedEnum::Users, CacheRepositoryEnumTestTagBackedEnum::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([CacheRepositoryEnumTestTagUnitEnum::Reports, CacheRepositoryEnumTestTagUnitEnum::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([CacheRepositoryEnumTestTagBackedEnum::Users, 'custom-tag', CacheRepositoryEnumTestTagUnitEnum::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(CacheRepositoryEnumTestTagBackedEnum::Users, CacheRepositoryEnumTestTagBackedEnum::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(CacheRepositoryEnumTestTagUnitEnum::Reports, CacheRepositoryEnumTestTagUnitEnum::Exports); + } + + public function testTaggedCacheOperationsWithEnumKeys(): void + { + $repo = new Repository(new ArrayStore()); + + $tagged = $repo->tags([CacheRepositoryEnumTestTagBackedEnum::Users]); + + // Put with enum key + $tagged->put(CacheRepositoryEnumTestKeyBackedEnum::UserProfile, 'tagged-value', 60); + + // Get with enum key + $this->assertSame('tagged-value', $tagged->get(CacheRepositoryEnumTestKeyBackedEnum::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[CacheRepositoryEnumTestKeyBackedEnum::UserProfile] = 'offset-value'; + + // offsetGet with enum + $this->assertSame('offset-value', $repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile]); + + // offsetExists with enum + $this->assertTrue(isset($repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile])); + + // offsetUnset with enum + unset($repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile]); + $this->assertFalse(isset($repo[CacheRepositoryEnumTestKeyBackedEnum::UserProfile])); + } + + public function testOffsetAccessWithUnitEnum(): void + { + $repo = new Repository(new ArrayStore()); + + $repo[CacheRepositoryEnumTestKeyUnitEnum::Dashboard] = 'dashboard-data'; + + $this->assertSame('dashboard-data', $repo[CacheRepositoryEnumTestKeyUnitEnum::Dashboard]); + $this->assertTrue(isset($repo[CacheRepositoryEnumTestKeyUnitEnum::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; + } +} diff --git a/tests/Cache/CacheTaggedCacheTest.php b/tests/Cache/CacheTaggedCacheTest.php index 0f4ccbe9f..2d212f0a4 100644 --- a/tests/Cache/CacheTaggedCacheTest.php +++ b/tests/Cache/CacheTaggedCacheTest.php @@ -8,6 +8,25 @@ use DateTime; use Hypervel\Cache\ArrayStore; use Hypervel\Tests\TestCase; +use TypeError; + +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 @@ -203,6 +222,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 new file mode 100644 index 000000000..6f123a3d2 --- /dev/null +++ b/tests/Cache/RateLimiterEnumTest.php @@ -0,0 +1,154 @@ +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')()); + } + + 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 6e354ebcc..6268b87cb 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; @@ -16,6 +17,25 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +use TypeError; + +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 @@ -156,4 +176,44 @@ 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 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); + } + + 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..a6803e95f 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; @@ -14,6 +15,37 @@ use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use TypeError; + +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 @@ -72,6 +104,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 testJobWithIntBackedEnumStoresIntValue(): void + { + $schedule = new Schedule(); + + // 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, + 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 testUseCacheWithIntBackedEnumThrowsTypeError(): void + { + $eventMutex = m::mock(EventMutex::class, CacheAware::class); + $schedulingMutex = m::mock(SchedulingMutex::class, CacheAware::class); + + $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); + } } class JobToTestWithSchedule implements ShouldQueue 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/Core/ContextEnumTest.php b/tests/Core/ContextEnumTest.php new file mode 100644 index 000000000..c55ae126e --- /dev/null +++ b/tests/Core/ContextEnumTest.php @@ -0,0 +1,235 @@ +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 testSetWithIntBackedEnumThrowsTypeError(): void + { + // Int-backed enum causes TypeError because parent::set() expects string key + $this->expectException(TypeError::class); + Context::set(ContextKeyIntBackedEnum::UserId, 'user-123'); + } + + 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)); + } +} diff --git a/tests/Core/Database/Eloquent/Factories/FactoryTest.php b/tests/Core/Database/Eloquent/Factories/FactoryTest.php index 3fea068dd..f124e3523 100644 --- a/tests/Core/Database/Eloquent/Factories/FactoryTest.php +++ b/tests/Core/Database/Eloquent/Factories/FactoryTest.php @@ -20,6 +20,25 @@ use Hypervel\Tests\Core\Database\Fixtures\Models\Price; use Mockery as m; use ReflectionClass; +use TypeError; + +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 @@ -848,6 +867,43 @@ 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 testConnectionWithIntBackedEnumThrowsTypeError() + { + $factory = FactoryTestUserFactory::new()->connection(FactoryTestIntBackedConnection::Testing); + + // Int-backed enum causes TypeError because getConnectionName() returns ?string + $this->expectException(TypeError::class); + $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..ef4ebc2dc --- /dev/null +++ b/tests/Core/Database/Eloquent/ModelEnumTest.php @@ -0,0 +1,80 @@ +setConnection(ModelTestStringBackedConnection::Testing); + + $this->assertSame('testing', $model->getConnectionName()); + } + + public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void + { + $model = new ModelEnumTestModel(); + + // Int-backed enum causes TypeError because $connection property is ?string + $this->expectException(TypeError::class); + $model->setConnection(ModelTestIntBackedConnection::Testing); + } + + 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..9729566eb --- /dev/null +++ b/tests/Core/Database/Eloquent/Relations/MorphPivotEnumTest.php @@ -0,0 +1,75 @@ +setConnection(MorphPivotTestStringBackedConnection::Testing); + + $this->assertSame('testing', $pivot->getConnectionName()); + } + + public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void + { + $pivot = new MorphPivot(); + + // Int-backed enum causes TypeError because $connection property is ?string + $this->expectException(TypeError::class); + $pivot->setConnection(MorphPivotTestIntBackedConnection::Testing); + } + + 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..815afdfbb --- /dev/null +++ b/tests/Core/Database/Eloquent/Relations/PivotEnumTest.php @@ -0,0 +1,75 @@ +setConnection(PivotTestStringBackedConnection::Testing); + + $this->assertSame('testing', $pivot->getConnectionName()); + } + + public function testSetConnectionWithIntBackedEnumThrowsTypeError(): void + { + $pivot = new Pivot(); + + // Int-backed enum causes TypeError because $connection property is ?string + $this->expectException(TypeError::class); + $pivot->setConnection(PivotTestIntBackedConnection::Testing); + } + + 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/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/Event/QueuedClosureTest.php b/tests/Event/QueuedClosureTest.php new file mode 100644 index 000000000..88779388c --- /dev/null +++ b/tests/Event/QueuedClosureTest.php @@ -0,0 +1,166 @@ + 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 testOnConnectionWithIntBackedEnumThrowsTypeError(): void + { + $closure = new QueuedClosure(fn () => null); + + $this->expectException(TypeError::class); + $closure->onConnection(QueuedClosureTestConnectionIntEnum::Connection1); + } + + 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 testOnQueueWithIntBackedEnumThrowsTypeError(): void + { + $closure = new QueuedClosure(fn () => null); + + $this->expectException(TypeError::class); + $closure->onQueue(QueuedClosureTestConnectionIntEnum::Connection2); + } + + 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 testOnGroupWithIntBackedEnumThrowsTypeError(): void + { + $closure = new QueuedClosure(fn () => null); + + $this->expectException(TypeError::class); + $closure->onGroup(QueuedClosureTestConnectionIntEnum::Connection1); + } + + 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..24d1bdd5f 100644 --- a/tests/Event/QueuedEventsTest.php +++ b/tests/Event/QueuedEventsTest.php @@ -22,9 +22,28 @@ use Mockery as m; use Mockery\MockInterface; use Psr\Container\ContainerInterface; +use TypeError; 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 +253,95 @@ 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 testQueueWithIntBackedEnumViaPropertyThrowsTypeError(): 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); + $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']); + } + + 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 +481,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/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index b5a686b5b..5402f47f4 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -18,6 +18,22 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; +use TypeError; + +enum FilesystemTestStringBackedDisk: string +{ + case Local = 'local'; +} + +enum FilesystemTestIntBackedDisk: int +{ + case Local = 1; +} + +enum FilesystemTestUnitDisk +{ + case local; +} /** * @internal @@ -192,6 +208,74 @@ 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 testDiskWithIntBackedEnumThrowsTypeError(): void + { + $container = $this->getContainer([ + 'disks' => [ + 'local' => [ + 'driver' => 'local', + 'root' => __DIR__ . '/tmp', + ], + ], + ]); + $filesystem = new FilesystemManager($container); + + // Int-backed enum causes TypeError because get() expects string + $this->expectException(TypeError::class); + $filesystem->disk(FilesystemTestIntBackedDisk::Local); + } + + 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/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); + } +} diff --git a/tests/Queue/RateLimitedTest.php b/tests/Queue/RateLimitedTest.php new file mode 100644 index 000000000..44b0e50c7 --- /dev/null +++ b/tests/Queue/RateLimitedTest.php @@ -0,0 +1,111 @@ +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 46e88ee74..62320947f 100644 --- a/tests/Redis/RedisTest.php +++ b/tests/Redis/RedisTest.php @@ -13,11 +13,32 @@ 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; +use TypeError; + +enum RedisTestStringBackedConnection: string +{ + case Default = 'default'; + case Cache = 'cache'; +} + +enum RedisTestIntBackedConnection: int +{ + case Primary = 1; + case Replica = 2; +} + +enum RedisTestUnitConnection +{ + case default; + case cache; +} /** * @internal @@ -217,6 +238,72 @@ 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); + } + + public function testConnectionWithIntBackedEnumThrowsTypeError(): void + { + $mockRedisFactory = Mockery::mock(RedisFactory::class); + + $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)); + + // Int-backed enum causes TypeError because RedisFactory::get() expects string + $this->expectException(TypeError::class); + $redis->connection(RedisTestIntBackedConnection::Primary); + } + /** * Create a mock Redis connection with configurable behavior. */ 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/Session/SessionStoreBackedEnumTest.php b/tests/Session/SessionStoreBackedEnumTest.php new file mode 100644 index 000000000..1b60768f0 --- /dev/null +++ b/tests/Session/SessionStoreBackedEnumTest.php @@ -0,0 +1,855 @@ +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)); + } + + // ========================================================================= + // 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 + // ========================================================================= + + protected function getSession(string $serialization = 'php'): Store + { + $store = new Store( + 'test-session', + m::mock(SessionHandlerInterface::class), + $serialization + ); + + $store->setId('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + + return $store; + } +} diff --git a/tests/Support/CollectionTest.php b/tests/Support/CollectionTest.php new file mode 100644 index 000000000..865253f8b --- /dev/null +++ b/tests/Support/CollectionTest.php @@ -0,0 +1,587 @@ + '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']); + } + + 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 +{ + public function __construct(private string $value) + { + } + + 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/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); + } +} 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()); + } +} 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)); + } +} 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');